commit 896146e0d9ad1947087156513bb4bb46940ed3c0 Author: xzeldon Date: Thu Jul 29 23:47:30 2021 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53f7466 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d091a9b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Timofey Gelazoniya + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3009d5 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Personal Website + +See: [zeldon.ru](https://zeldon.ru). + +My personal website. An [MIT](https://github.com/xzeldon/zeldon-site/blob/main/LICENSE) licensed, easy modifiable. + +## Dependencies + +Only [Vite](https://vitejs.dev/) for development and bundling. + +## Set up + +To download the repository and install dependencies, run the following commands: + +```bash +git clone git://github.com/xzeldon/zeldon-site.git # replace [xzeldon] with your github username if you fork first. +cd zeldon-site +npm install +``` + +## Running + +Run the following command to build the application and serve it with fast refresh: + +```bash +npm run dev +``` + +Your web browser should automatically open to `::` default: [http://localhost:3000/](http://localhost:3000/). + +## Static export + +To statically export the site, run this command: +```bash +npm run build +``` + +This generates a static export of the website as `zeldon-site/dist/`. Copy this and self-host or deploy to a CDN. + +## Acknowledgements + +* Background animation: [WebGL-Fluid-Simulation](https://github.com/PavelDoGreat/WebGL-Fluid-Simulation) by [@PavelDoGreat](https://github.com/PavelDoGreat). +* Special thanks to [@xm1ller](https://github.com/xm1ller) for tirelessly answering all of my css questions. \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..2bf2bc7 --- /dev/null +++ b/index.html @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + zeldØƞ + + + + + + + + + + + + + +
+
+ +
+ + +
+
+
+ + + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..e41a6bb --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "zeldon-site", + "version": "1.0.0", + "description": "My personal website — Build with Vanilla JS.", + "license": "MIT", + "author": { + "name": "Timofey (xzeldon)", + "email": "contact@zeldon.ru", + "url": "https://zeldon.ru" + }, + "contributors": [ + { + "name": "Nikita (xm1ller)", + "email": "xm1ller.digital@gmail.com" + } + ], + "homepage": "https://zeldon.ru", + "keywords": [ + "website", + "personal", + "site", + "homepage" + ], + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview" + }, + "devDependencies": { + "vite": "^2.3.7" + } +} \ No newline at end of file diff --git a/public/LDR_LLL1_0.png b/public/LDR_LLL1_0.png new file mode 100644 index 0000000..d1920c6 Binary files /dev/null and b/public/LDR_LLL1_0.png differ diff --git a/public/favicon/apple-touch-icon.png b/public/favicon/apple-touch-icon.png new file mode 100644 index 0000000..3f97d46 Binary files /dev/null and b/public/favicon/apple-touch-icon.png differ diff --git a/public/favicon/favicon.ico b/public/favicon/favicon.ico new file mode 100644 index 0000000..2711eeb Binary files /dev/null and b/public/favicon/favicon.ico differ diff --git a/public/favicon/favicon.svg b/public/favicon/favicon.svg new file mode 100644 index 0000000..94e4596 --- /dev/null +++ b/public/favicon/favicon.svg @@ -0,0 +1,28 @@ + + + + + + Z + Z + Z + Z + Z + Z + Z + diff --git a/public/fluid.js b/public/fluid.js new file mode 100644 index 0000000..fcf3e09 --- /dev/null +++ b/public/fluid.js @@ -0,0 +1,1637 @@ +/* +MIT License + +Copyright (c) 2017 Pavel Dobryakov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +'use strict'; + +// Simulation section +const canvas = document.getElementsByTagName('canvas')[0]; +resizeCanvas(); + +let config = { + SIM_RESOLUTION: 128, + DYE_RESOLUTION: 1024, + CAPTURE_RESOLUTION: 512, + DENSITY_DISSIPATION: 1, + VELOCITY_DISSIPATION: 0.2, + PRESSURE: 0.8, + PRESSURE_ITERATIONS: 20, + CURL: 30, + SPLAT_RADIUS: 0.25, + SPLAT_FORCE: 6000, + SHADING: true, + COLORFUL: true, + COLOR_UPDATE_SPEED: 10, + PAUSED: false, + BACK_COLOR: { r: 0, g: 0, b: 0 }, + TRANSPARENT: false, + BLOOM: true, + BLOOM_ITERATIONS: 8, + BLOOM_RESOLUTION: 256, + BLOOM_INTENSITY: 0.8, + BLOOM_THRESHOLD: 0.6, + BLOOM_SOFT_KNEE: 0.7, + SUNRAYS: true, + SUNRAYS_RESOLUTION: 196, + SUNRAYS_WEIGHT: 1.0, +}; + +class pointerPrototype +{ + constructor() + { + this.id = -1; + this.texcoordX = 0; + this.texcoordY = 0; + this.prevTexcoordX = 0; + this.prevTexcoordY = 0; + this.deltaX = 0; + this.deltaY = 0; + this.down = false; + this.moved = false; + this.color = [30, 0, 300]; + } +} + +let pointers = []; +let splatStack = []; +pointers.push(new pointerPrototype()); + +const { gl, ext } = getWebGLContext(canvas); + +if (isMobile()) +{ + config.DYE_RESOLUTION = 512; +} +if (!ext.supportLinearFiltering) +{ + config.DYE_RESOLUTION = 512; + config.SHADING = false; + config.BLOOM = false; + config.SUNRAYS = false; +} + +function getWebGLContext(canvas) +{ + const params = { alpha: true, depth: false, stencil: false, antialias: false, preserveDrawingBuffer: false }; + + let gl = canvas.getContext('webgl2', params); + const isWebGL2 = !!gl; + if (!isWebGL2) + gl = canvas.getContext('webgl', params) || canvas.getContext('experimental-webgl', params); + + let halfFloat; + let supportLinearFiltering; + if (isWebGL2) + { + gl.getExtension('EXT_color_buffer_float'); + supportLinearFiltering = gl.getExtension('OES_texture_float_linear'); + } else + { + halfFloat = gl.getExtension('OES_texture_half_float'); + supportLinearFiltering = gl.getExtension('OES_texture_half_float_linear'); + } + + gl.clearColor(0.0, 0.0, 0.0, 1.0); + + const halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat.HALF_FLOAT_OES; + let formatRGBA; + let formatRG; + let formatR; + + if (isWebGL2) + { + formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType); + formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType); + formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType); + } + else + { + formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); + formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); + formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); + } + + return { + gl, + ext: { + formatRGBA, + formatRG, + formatR, + halfFloatTexType, + supportLinearFiltering + } + }; +} + +function getSupportedFormat(gl, internalFormat, format, type) +{ + if (!supportRenderTextureFormat(gl, internalFormat, format, type)) + { + switch (internalFormat) + { + case gl.R16F: + return getSupportedFormat(gl, gl.RG16F, gl.RG, type); + case gl.RG16F: + return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type); + default: + return null; + } + } + + return { + internalFormat, + format + }; +} + +function supportRenderTextureFormat(gl, internalFormat, format, type) +{ + let texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null); + + let fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + + let status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); + return status == gl.FRAMEBUFFER_COMPLETE; +} + +function isMobile() +{ + return /Mobi|Android/i.test(navigator.userAgent); +} + +function captureScreenshot() +{ + let res = getResolution(config.CAPTURE_RESOLUTION); + let target = createFBO(res.width, res.height, ext.formatRGBA.internalFormat, ext.formatRGBA.format, ext.halfFloatTexType, gl.NEAREST); + render(target); + + let texture = framebufferToTexture(target); + texture = normalizeTexture(texture, target.width, target.height); + + let captureCanvas = textureToCanvas(texture, target.width, target.height); + let datauri = captureCanvas.toDataURL(); + downloadURI('fluid.png', datauri); + URL.revokeObjectURL(datauri); +} + +function framebufferToTexture(target) +{ + gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo); + let length = target.width * target.height * 4; + let texture = new Float32Array(length); + gl.readPixels(0, 0, target.width, target.height, gl.RGBA, gl.FLOAT, texture); + return texture; +} + +function normalizeTexture(texture, width, height) +{ + let result = new Uint8Array(texture.length); + let id = 0; + for (let i = height - 1; i >= 0; i--) + { + for (let j = 0; j < width; j++) + { + let nid = i * width * 4 + j * 4; + result[nid + 0] = clamp01(texture[id + 0]) * 255; + result[nid + 1] = clamp01(texture[id + 1]) * 255; + result[nid + 2] = clamp01(texture[id + 2]) * 255; + result[nid + 3] = clamp01(texture[id + 3]) * 255; + id += 4; + } + } + return result; +} + +function clamp01(input) +{ + return Math.min(Math.max(input, 0), 1); +} + +function textureToCanvas(texture, width, height) +{ + let captureCanvas = document.createElement('canvas'); + let ctx = captureCanvas.getContext('2d'); + captureCanvas.width = width; + captureCanvas.height = height; + + let imageData = ctx.createImageData(width, height); + imageData.data.set(texture); + ctx.putImageData(imageData, 0, 0); + + return captureCanvas; +} + +class Material +{ + constructor(vertexShader, fragmentShaderSource) + { + this.vertexShader = vertexShader; + this.fragmentShaderSource = fragmentShaderSource; + this.programs = []; + this.activeProgram = null; + this.uniforms = []; + } + + setKeywords(keywords) + { + let hash = 0; + for (let i = 0; i < keywords.length; i++) + hash += hashCode(keywords[i]); + + let program = this.programs[hash]; + if (program == null) + { + let fragmentShader = compileShader(gl.FRAGMENT_SHADER, this.fragmentShaderSource, keywords); + program = createProgram(this.vertexShader, fragmentShader); + this.programs[hash] = program; + } + + if (program == this.activeProgram) return; + + this.uniforms = getUniforms(program); + this.activeProgram = program; + } + + bind() + { + gl.useProgram(this.activeProgram); + } +} + +class Program +{ + constructor(vertexShader, fragmentShader) + { + this.uniforms = {}; + this.program = createProgram(vertexShader, fragmentShader); + this.uniforms = getUniforms(this.program); + } + + bind() + { + gl.useProgram(this.program); + } +} + +function createProgram(vertexShader, fragmentShader) +{ + let program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) + console.trace(gl.getProgramInfoLog(program)); + + return program; +} + +function getUniforms(program) +{ + let uniforms = []; + let uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); + for (let i = 0; i < uniformCount; i++) + { + let uniformName = gl.getActiveUniform(program, i).name; + uniforms[uniformName] = gl.getUniformLocation(program, uniformName); + } + return uniforms; +} + +function compileShader(type, source, keywords) +{ + source = addKeywords(source, keywords); + + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) + console.trace(gl.getShaderInfoLog(shader)); + + return shader; +}; + +function addKeywords(source, keywords) +{ + if (keywords == null) return source; + let keywordsString = ''; + keywords.forEach(keyword => + { + keywordsString += '#define ' + keyword + '\n'; + }); + return keywordsString + source; +} + +const baseVertexShader = compileShader(gl.VERTEX_SHADER, ` + precision highp float; + + attribute vec2 aPosition; + varying vec2 vUv; + varying vec2 vL; + varying vec2 vR; + varying vec2 vT; + varying vec2 vB; + uniform vec2 texelSize; + + void main () { + vUv = aPosition * 0.5 + 0.5; + vL = vUv - vec2(texelSize.x, 0.0); + vR = vUv + vec2(texelSize.x, 0.0); + vT = vUv + vec2(0.0, texelSize.y); + vB = vUv - vec2(0.0, texelSize.y); + gl_Position = vec4(aPosition, 0.0, 1.0); + } +`); + +const blurVertexShader = compileShader(gl.VERTEX_SHADER, ` + precision highp float; + + attribute vec2 aPosition; + varying vec2 vUv; + varying vec2 vL; + varying vec2 vR; + uniform vec2 texelSize; + + void main () { + vUv = aPosition * 0.5 + 0.5; + float offset = 1.33333333; + vL = vUv - texelSize * offset; + vR = vUv + texelSize * offset; + gl_Position = vec4(aPosition, 0.0, 1.0); + } +`); + +const blurShader = compileShader(gl.FRAGMENT_SHADER, ` + precision mediump float; + precision mediump sampler2D; + + varying vec2 vUv; + varying vec2 vL; + varying vec2 vR; + uniform sampler2D uTexture; + + void main () { + vec4 sum = texture2D(uTexture, vUv) * 0.29411764; + sum += texture2D(uTexture, vL) * 0.35294117; + sum += texture2D(uTexture, vR) * 0.35294117; + gl_FragColor = sum; + } +`); + +const copyShader = compileShader(gl.FRAGMENT_SHADER, ` + precision mediump float; + precision mediump sampler2D; + + varying highp vec2 vUv; + uniform sampler2D uTexture; + + void main () { + gl_FragColor = texture2D(uTexture, vUv); + } +`); + +const clearShader = compileShader(gl.FRAGMENT_SHADER, ` + precision mediump float; + precision mediump sampler2D; + + varying highp vec2 vUv; + uniform sampler2D uTexture; + uniform float value; + + void main () { + gl_FragColor = value * texture2D(uTexture, vUv); + } +`); + +const colorShader = compileShader(gl.FRAGMENT_SHADER, ` + precision mediump float; + + uniform vec4 color; + + void main () { + gl_FragColor = color; + } +`); + +const checkerboardShader = compileShader(gl.FRAGMENT_SHADER, ` + precision highp float; + precision highp sampler2D; + + varying vec2 vUv; + uniform sampler2D uTexture; + uniform float aspectRatio; + + #define SCALE 25.0 + + void main () { + vec2 uv = floor(vUv * SCALE * vec2(aspectRatio, 1.0)); + float v = mod(uv.x + uv.y, 2.0); + v = v * 0.1 + 0.8; + gl_FragColor = vec4(vec3(v), 1.0); + } +`); + +const displayShaderSource = ` + precision highp float; + precision highp sampler2D; + + varying vec2 vUv; + varying vec2 vL; + varying vec2 vR; + varying vec2 vT; + varying vec2 vB; + uniform sampler2D uTexture; + uniform sampler2D uBloom; + uniform sampler2D uSunrays; + uniform sampler2D uDithering; + uniform vec2 ditherScale; + uniform vec2 texelSize; + + vec3 linearToGamma (vec3 color) { + color = max(color, vec3(0)); + return max(1.055 * pow(color, vec3(0.416666667)) - 0.055, vec3(0)); + } + + void main () { + vec3 c = texture2D(uTexture, vUv).rgb; + + #ifdef SHADING + vec3 lc = texture2D(uTexture, vL).rgb; + vec3 rc = texture2D(uTexture, vR).rgb; + vec3 tc = texture2D(uTexture, vT).rgb; + vec3 bc = texture2D(uTexture, vB).rgb; + + float dx = length(rc) - length(lc); + float dy = length(tc) - length(bc); + + vec3 n = normalize(vec3(dx, dy, length(texelSize))); + vec3 l = vec3(0.0, 0.0, 1.0); + + float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0); + c *= diffuse; + #endif + + #ifdef BLOOM + vec3 bloom = texture2D(uBloom, vUv).rgb; + #endif + + #ifdef SUNRAYS + float sunrays = texture2D(uSunrays, vUv).r; + c *= sunrays; + #ifdef BLOOM + bloom *= sunrays; + #endif + #endif + + #ifdef BLOOM + float noise = texture2D(uDithering, vUv * ditherScale).r; + noise = noise * 2.0 - 1.0; + bloom += noise / 255.0; + bloom = linearToGamma(bloom); + c += bloom; + #endif + + float a = max(c.r, max(c.g, c.b)); + gl_FragColor = vec4(c, a); + } +`; + +const bloomPrefilterShader = compileShader(gl.FRAGMENT_SHADER, ` + precision mediump float; + precision mediump sampler2D; + + varying vec2 vUv; + uniform sampler2D uTexture; + uniform vec3 curve; + uniform float threshold; + + void main () { + vec3 c = texture2D(uTexture, vUv).rgb; + float br = max(c.r, max(c.g, c.b)); + float rq = clamp(br - curve.x, 0.0, curve.y); + rq = curve.z * rq * rq; + c *= max(rq, br - threshold) / max(br, 0.0001); + gl_FragColor = vec4(c, 0.0); + } +`); + +const bloomBlurShader = compileShader(gl.FRAGMENT_SHADER, ` + precision mediump float; + precision mediump sampler2D; + + varying vec2 vL; + varying vec2 vR; + varying vec2 vT; + varying vec2 vB; + uniform sampler2D uTexture; + + void main () { + vec4 sum = vec4(0.0); + sum += texture2D(uTexture, vL); + sum += texture2D(uTexture, vR); + sum += texture2D(uTexture, vT); + sum += texture2D(uTexture, vB); + sum *= 0.25; + gl_FragColor = sum; + } +`); + +const bloomFinalShader = compileShader(gl.FRAGMENT_SHADER, ` + precision mediump float; + precision mediump sampler2D; + + varying vec2 vL; + varying vec2 vR; + varying vec2 vT; + varying vec2 vB; + uniform sampler2D uTexture; + uniform float intensity; + + void main () { + vec4 sum = vec4(0.0); + sum += texture2D(uTexture, vL); + sum += texture2D(uTexture, vR); + sum += texture2D(uTexture, vT); + sum += texture2D(uTexture, vB); + sum *= 0.25; + gl_FragColor = sum * intensity; + } +`); + +const sunraysMaskShader = compileShader(gl.FRAGMENT_SHADER, ` + precision highp float; + precision highp sampler2D; + + varying vec2 vUv; + uniform sampler2D uTexture; + + void main () { + vec4 c = texture2D(uTexture, vUv); + float br = max(c.r, max(c.g, c.b)); + c.a = 1.0 - min(max(br * 20.0, 0.0), 0.8); + gl_FragColor = c; + } +`); + +const sunraysShader = compileShader(gl.FRAGMENT_SHADER, ` + precision highp float; + precision highp sampler2D; + + varying vec2 vUv; + uniform sampler2D uTexture; + uniform float weight; + + #define ITERATIONS 16 + + void main () { + float Density = 0.3; + float Decay = 0.95; + float Exposure = 0.7; + + vec2 coord = vUv; + vec2 dir = vUv - 0.5; + + dir *= 1.0 / float(ITERATIONS) * Density; + float illuminationDecay = 1.0; + + float color = texture2D(uTexture, vUv).a; + + for (int i = 0; i < ITERATIONS; i++) + { + coord -= dir; + float col = texture2D(uTexture, coord).a; + color += col * illuminationDecay * weight; + illuminationDecay *= Decay; + } + + gl_FragColor = vec4(color * Exposure, 0.0, 0.0, 1.0); + } +`); + +const splatShader = compileShader(gl.FRAGMENT_SHADER, ` + precision highp float; + precision highp sampler2D; + + varying vec2 vUv; + uniform sampler2D uTarget; + uniform float aspectRatio; + uniform vec3 color; + uniform vec2 point; + uniform float radius; + + void main () { + vec2 p = vUv - point.xy; + p.x *= aspectRatio; + vec3 splat = exp(-dot(p, p) / radius) * color; + vec3 base = texture2D(uTarget, vUv).xyz; + gl_FragColor = vec4(base + splat, 1.0); + } +`); + +const advectionShader = compileShader(gl.FRAGMENT_SHADER, ` + precision highp float; + precision highp sampler2D; + + varying vec2 vUv; + uniform sampler2D uVelocity; + uniform sampler2D uSource; + uniform vec2 texelSize; + uniform vec2 dyeTexelSize; + uniform float dt; + uniform float dissipation; + + vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) { + vec2 st = uv / tsize - 0.5; + + vec2 iuv = floor(st); + vec2 fuv = fract(st); + + vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize); + vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize); + vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize); + vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize); + + return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y); + } + + void main () { + #ifdef MANUAL_FILTERING + vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize; + vec4 result = bilerp(uSource, coord, dyeTexelSize); + #else + vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize; + vec4 result = texture2D(uSource, coord); + #endif + float decay = 1.0 + dissipation * dt; + gl_FragColor = result / decay; + }`, + ext.supportLinearFiltering ? null : ['MANUAL_FILTERING'] +); + +const divergenceShader = compileShader(gl.FRAGMENT_SHADER, ` + precision mediump float; + precision mediump sampler2D; + + varying highp vec2 vUv; + varying highp vec2 vL; + varying highp vec2 vR; + varying highp vec2 vT; + varying highp vec2 vB; + uniform sampler2D uVelocity; + + void main () { + float L = texture2D(uVelocity, vL).x; + float R = texture2D(uVelocity, vR).x; + float T = texture2D(uVelocity, vT).y; + float B = texture2D(uVelocity, vB).y; + + vec2 C = texture2D(uVelocity, vUv).xy; + if (vL.x < 0.0) { L = -C.x; } + if (vR.x > 1.0) { R = -C.x; } + if (vT.y > 1.0) { T = -C.y; } + if (vB.y < 0.0) { B = -C.y; } + + float div = 0.5 * (R - L + T - B); + gl_FragColor = vec4(div, 0.0, 0.0, 1.0); + } +`); + +const curlShader = compileShader(gl.FRAGMENT_SHADER, ` + precision mediump float; + precision mediump sampler2D; + + varying highp vec2 vUv; + varying highp vec2 vL; + varying highp vec2 vR; + varying highp vec2 vT; + varying highp vec2 vB; + uniform sampler2D uVelocity; + + void main () { + float L = texture2D(uVelocity, vL).y; + float R = texture2D(uVelocity, vR).y; + float T = texture2D(uVelocity, vT).x; + float B = texture2D(uVelocity, vB).x; + float vorticity = R - L - T + B; + gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0); + } +`); + +const vorticityShader = compileShader(gl.FRAGMENT_SHADER, ` + precision highp float; + precision highp sampler2D; + + varying vec2 vUv; + varying vec2 vL; + varying vec2 vR; + varying vec2 vT; + varying vec2 vB; + uniform sampler2D uVelocity; + uniform sampler2D uCurl; + uniform float curl; + uniform float dt; + + void main () { + float L = texture2D(uCurl, vL).x; + float R = texture2D(uCurl, vR).x; + float T = texture2D(uCurl, vT).x; + float B = texture2D(uCurl, vB).x; + float C = texture2D(uCurl, vUv).x; + + vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L)); + force /= length(force) + 0.0001; + force *= curl * C; + force.y *= -1.0; + + vec2 velocity = texture2D(uVelocity, vUv).xy; + velocity += force * dt; + velocity = min(max(velocity, -1000.0), 1000.0); + gl_FragColor = vec4(velocity, 0.0, 1.0); + } +`); + +const pressureShader = compileShader(gl.FRAGMENT_SHADER, ` + precision mediump float; + precision mediump sampler2D; + + varying highp vec2 vUv; + varying highp vec2 vL; + varying highp vec2 vR; + varying highp vec2 vT; + varying highp vec2 vB; + uniform sampler2D uPressure; + uniform sampler2D uDivergence; + + void main () { + float L = texture2D(uPressure, vL).x; + float R = texture2D(uPressure, vR).x; + float T = texture2D(uPressure, vT).x; + float B = texture2D(uPressure, vB).x; + float C = texture2D(uPressure, vUv).x; + float divergence = texture2D(uDivergence, vUv).x; + float pressure = (L + R + B + T - divergence) * 0.25; + gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0); + } +`); + +const gradientSubtractShader = compileShader(gl.FRAGMENT_SHADER, ` + precision mediump float; + precision mediump sampler2D; + + varying highp vec2 vUv; + varying highp vec2 vL; + varying highp vec2 vR; + varying highp vec2 vT; + varying highp vec2 vB; + uniform sampler2D uPressure; + uniform sampler2D uVelocity; + + void main () { + float L = texture2D(uPressure, vL).x; + float R = texture2D(uPressure, vR).x; + float T = texture2D(uPressure, vT).x; + float B = texture2D(uPressure, vB).x; + vec2 velocity = texture2D(uVelocity, vUv).xy; + velocity.xy -= vec2(R - L, T - B); + gl_FragColor = vec4(velocity, 0.0, 1.0); + } +`); + +const blit = (() => +{ + gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW); + gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(0); + + return (target, clear = false) => + { + if (target == null) + { + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + } + else + { + gl.viewport(0, 0, target.width, target.height); + gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo); + } + if (clear) + { + gl.clearColor(0.0, 0.0, 0.0, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT); + } + // CHECK_FRAMEBUFFER_STATUS(); + gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); + }; +})(); + +function CHECK_FRAMEBUFFER_STATUS() +{ + let status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); + if (status != gl.FRAMEBUFFER_COMPLETE) + console.trace("Framebuffer error: " + status); +} + +let dye; +let velocity; +let divergence; +let curl; +let pressure; +let bloom; +let bloomFramebuffers = []; +let sunrays; +let sunraysTemp; + +let ditheringTexture = createTextureAsync('/LDR_LLL1_0.png'); + +const blurProgram = new Program(blurVertexShader, blurShader); +const copyProgram = new Program(baseVertexShader, copyShader); +const clearProgram = new Program(baseVertexShader, clearShader); +const colorProgram = new Program(baseVertexShader, colorShader); +const checkerboardProgram = new Program(baseVertexShader, checkerboardShader); +const bloomPrefilterProgram = new Program(baseVertexShader, bloomPrefilterShader); +const bloomBlurProgram = new Program(baseVertexShader, bloomBlurShader); +const bloomFinalProgram = new Program(baseVertexShader, bloomFinalShader); +const sunraysMaskProgram = new Program(baseVertexShader, sunraysMaskShader); +const sunraysProgram = new Program(baseVertexShader, sunraysShader); +const splatProgram = new Program(baseVertexShader, splatShader); +const advectionProgram = new Program(baseVertexShader, advectionShader); +const divergenceProgram = new Program(baseVertexShader, divergenceShader); +const curlProgram = new Program(baseVertexShader, curlShader); +const vorticityProgram = new Program(baseVertexShader, vorticityShader); +const pressureProgram = new Program(baseVertexShader, pressureShader); +const gradienSubtractProgram = new Program(baseVertexShader, gradientSubtractShader); + +const displayMaterial = new Material(baseVertexShader, displayShaderSource); + +function initFramebuffers() +{ + let simRes = getResolution(config.SIM_RESOLUTION); + let dyeRes = getResolution(config.DYE_RESOLUTION); + + const texType = ext.halfFloatTexType; + const rgba = ext.formatRGBA; + const rg = ext.formatRG; + const r = ext.formatR; + const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; + + gl.disable(gl.BLEND); + + if (dye == null) + dye = createDoubleFBO(dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering); + else + dye = resizeDoubleFBO(dye, dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering); + + if (velocity == null) + velocity = createDoubleFBO(simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering); + else + velocity = resizeDoubleFBO(velocity, simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering); + + divergence = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); + curl = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); + pressure = createDoubleFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); + + initBloomFramebuffers(); + initSunraysFramebuffers(); +} + +function initBloomFramebuffers() +{ + let res = getResolution(config.BLOOM_RESOLUTION); + + const texType = ext.halfFloatTexType; + const rgba = ext.formatRGBA; + const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; + + bloom = createFBO(res.width, res.height, rgba.internalFormat, rgba.format, texType, filtering); + + bloomFramebuffers.length = 0; + for (let i = 0; i < config.BLOOM_ITERATIONS; i++) + { + let width = res.width >> (i + 1); + let height = res.height >> (i + 1); + + if (width < 2 || height < 2) break; + + let fbo = createFBO(width, height, rgba.internalFormat, rgba.format, texType, filtering); + bloomFramebuffers.push(fbo); + } +} + +function initSunraysFramebuffers() +{ + let res = getResolution(config.SUNRAYS_RESOLUTION); + + const texType = ext.halfFloatTexType; + const r = ext.formatR; + const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; + + sunrays = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering); + sunraysTemp = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering); +} + +function createFBO(w, h, internalFormat, format, type, param) +{ + gl.activeTexture(gl.TEXTURE0); + let texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null); + + let fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + gl.viewport(0, 0, w, h); + gl.clear(gl.COLOR_BUFFER_BIT); + + let texelSizeX = 1.0 / w; + let texelSizeY = 1.0 / h; + + return { + texture, + fbo, + width: w, + height: h, + texelSizeX, + texelSizeY, + attach(id) + { + gl.activeTexture(gl.TEXTURE0 + id); + gl.bindTexture(gl.TEXTURE_2D, texture); + return id; + } + }; +} + +function createDoubleFBO(w, h, internalFormat, format, type, param) +{ + let fbo1 = createFBO(w, h, internalFormat, format, type, param); + let fbo2 = createFBO(w, h, internalFormat, format, type, param); + + return { + width: w, + height: h, + texelSizeX: fbo1.texelSizeX, + texelSizeY: fbo1.texelSizeY, + get read() + { + return fbo1; + }, + set read(value) + { + fbo1 = value; + }, + get write() + { + return fbo2; + }, + set write(value) + { + fbo2 = value; + }, + swap() + { + let temp = fbo1; + fbo1 = fbo2; + fbo2 = temp; + } + }; +} + +function resizeFBO(target, w, h, internalFormat, format, type, param) +{ + let newFBO = createFBO(w, h, internalFormat, format, type, param); + copyProgram.bind(); + gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0)); + blit(newFBO); + return newFBO; +} + +function resizeDoubleFBO(target, w, h, internalFormat, format, type, param) +{ + if (target.width == w && target.height == h) + return target; + target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param); + target.write = createFBO(w, h, internalFormat, format, type, param); + target.width = w; + target.height = h; + target.texelSizeX = 1.0 / w; + target.texelSizeY = 1.0 / h; + return target; +} + +function createTextureAsync(url) +{ + let texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 1, 1, 0, gl.RGB, gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255])); + + let obj = { + texture, + width: 1, + height: 1, + attach(id) + { + gl.activeTexture(gl.TEXTURE0 + id); + gl.bindTexture(gl.TEXTURE_2D, texture); + return id; + } + }; + + let image = new Image(); + image.onload = () => + { + obj.width = image.width; + obj.height = image.height; + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image); + }; + image.src = url; + + return obj; +} + +function updateKeywords() +{ + let displayKeywords = []; + if (config.SHADING) displayKeywords.push("SHADING"); + if (config.BLOOM) displayKeywords.push("BLOOM"); + if (config.SUNRAYS) displayKeywords.push("SUNRAYS"); + displayMaterial.setKeywords(displayKeywords); +} + +updateKeywords(); +initFramebuffers(); +multipleSplats(parseInt(Math.random() * 20) + 5); + +let lastUpdateTime = Date.now(); +let colorUpdateTimer = 0.0; +update(); + +function update() +{ + const dt = calcDeltaTime(); + if (resizeCanvas()) + initFramebuffers(); + updateColors(dt); + applyInputs(); + if (!config.PAUSED) + step(dt); + render(null); + requestAnimationFrame(update); +} + +function calcDeltaTime() +{ + let now = Date.now(); + let dt = (now - lastUpdateTime) / 1000; + dt = Math.min(dt, 0.016666); + lastUpdateTime = now; + return dt; +} + +function resizeCanvas() +{ + let width = scaleByPixelRatio(canvas.clientWidth); + let height = scaleByPixelRatio(canvas.clientHeight); + if (canvas.width != width || canvas.height != height) + { + canvas.width = width; + canvas.height = height; + return true; + } + return false; +} + +function updateColors(dt) +{ + if (!config.COLORFUL) return; + + colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED; + if (colorUpdateTimer >= 1) + { + colorUpdateTimer = wrap(colorUpdateTimer, 0, 1); + pointers.forEach(p => + { + p.color = generateColor(); + }); + } +} + +function applyInputs() +{ + if (splatStack.length > 0) + multipleSplats(splatStack.pop()); + + pointers.forEach(p => + { + if (p.moved) + { + p.moved = false; + splatPointer(p); + } + }); +} + +function step(dt) +{ + gl.disable(gl.BLEND); + + curlProgram.bind(); + gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); + gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0)); + blit(curl); + + vorticityProgram.bind(); + gl.uniform2f(vorticityProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); + gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0)); + gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1)); + gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL); + gl.uniform1f(vorticityProgram.uniforms.dt, dt); + blit(velocity.write); + velocity.swap(); + + divergenceProgram.bind(); + gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); + gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0)); + blit(divergence); + + clearProgram.bind(); + gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0)); + gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE); + blit(pressure.write); + pressure.swap(); + + pressureProgram.bind(); + gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); + gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0)); + for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) + { + gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1)); + blit(pressure.write); + pressure.swap(); + } + + gradienSubtractProgram.bind(); + gl.uniform2f(gradienSubtractProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); + gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.read.attach(0)); + gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1)); + blit(velocity.write); + velocity.swap(); + + advectionProgram.bind(); + gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); + if (!ext.supportLinearFiltering) + gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, velocity.texelSizeX, velocity.texelSizeY); + let velocityId = velocity.read.attach(0); + gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId); + gl.uniform1i(advectionProgram.uniforms.uSource, velocityId); + gl.uniform1f(advectionProgram.uniforms.dt, dt); + gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION); + blit(velocity.write); + velocity.swap(); + + if (!ext.supportLinearFiltering) + gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY); + gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0)); + gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1)); + gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION); + blit(dye.write); + dye.swap(); +} + +function render(target) +{ + if (config.BLOOM) + applyBloom(dye.read, bloom); + if (config.SUNRAYS) + { + applySunrays(dye.read, dye.write, sunrays); + blur(sunrays, sunraysTemp, 1); + } + + if (target == null || !config.TRANSPARENT) + { + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + gl.enable(gl.BLEND); + } + else + { + gl.disable(gl.BLEND); + } + + if (!config.TRANSPARENT) + drawColor(target, normalizeColor(config.BACK_COLOR)); + if (target == null && config.TRANSPARENT) + drawCheckerboard(target); + drawDisplay(target); +} + +function drawColor(target, color) +{ + colorProgram.bind(); + gl.uniform4f(colorProgram.uniforms.color, color.r, color.g, color.b, 1); + blit(target); +} + +function drawCheckerboard(target) +{ + checkerboardProgram.bind(); + gl.uniform1f(checkerboardProgram.uniforms.aspectRatio, canvas.width / canvas.height); + blit(target); +} + +function drawDisplay(target) +{ + let width = target == null ? gl.drawingBufferWidth : target.width; + let height = target == null ? gl.drawingBufferHeight : target.height; + + displayMaterial.bind(); + if (config.SHADING) + gl.uniform2f(displayMaterial.uniforms.texelSize, 1.0 / width, 1.0 / height); + gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0)); + if (config.BLOOM) + { + gl.uniform1i(displayMaterial.uniforms.uBloom, bloom.attach(1)); + gl.uniform1i(displayMaterial.uniforms.uDithering, ditheringTexture.attach(2)); + let scale = getTextureScale(ditheringTexture, width, height); + gl.uniform2f(displayMaterial.uniforms.ditherScale, scale.x, scale.y); + } + if (config.SUNRAYS) + gl.uniform1i(displayMaterial.uniforms.uSunrays, sunrays.attach(3)); + blit(target); +} + +function applyBloom(source, destination) +{ + if (bloomFramebuffers.length < 2) + return; + + let last = destination; + + gl.disable(gl.BLEND); + bloomPrefilterProgram.bind(); + let knee = config.BLOOM_THRESHOLD * config.BLOOM_SOFT_KNEE + 0.0001; + let curve0 = config.BLOOM_THRESHOLD - knee; + let curve1 = knee * 2; + let curve2 = 0.25 / knee; + gl.uniform3f(bloomPrefilterProgram.uniforms.curve, curve0, curve1, curve2); + gl.uniform1f(bloomPrefilterProgram.uniforms.threshold, config.BLOOM_THRESHOLD); + gl.uniform1i(bloomPrefilterProgram.uniforms.uTexture, source.attach(0)); + blit(last); + + bloomBlurProgram.bind(); + for (let i = 0; i < bloomFramebuffers.length; i++) + { + let dest = bloomFramebuffers[i]; + gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); + gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0)); + blit(dest); + last = dest; + } + + gl.blendFunc(gl.ONE, gl.ONE); + gl.enable(gl.BLEND); + + for (let i = bloomFramebuffers.length - 2; i >= 0; i--) + { + let baseTex = bloomFramebuffers[i]; + gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); + gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0)); + gl.viewport(0, 0, baseTex.width, baseTex.height); + blit(baseTex); + last = baseTex; + } + + gl.disable(gl.BLEND); + bloomFinalProgram.bind(); + gl.uniform2f(bloomFinalProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); + gl.uniform1i(bloomFinalProgram.uniforms.uTexture, last.attach(0)); + gl.uniform1f(bloomFinalProgram.uniforms.intensity, config.BLOOM_INTENSITY); + blit(destination); +} + +function applySunrays(source, mask, destination) +{ + gl.disable(gl.BLEND); + sunraysMaskProgram.bind(); + gl.uniform1i(sunraysMaskProgram.uniforms.uTexture, source.attach(0)); + blit(mask); + + sunraysProgram.bind(); + gl.uniform1f(sunraysProgram.uniforms.weight, config.SUNRAYS_WEIGHT); + gl.uniform1i(sunraysProgram.uniforms.uTexture, mask.attach(0)); + blit(destination); +} + +function blur(target, temp, iterations) +{ + blurProgram.bind(); + for (let i = 0; i < iterations; i++) + { + gl.uniform2f(blurProgram.uniforms.texelSize, target.texelSizeX, 0.0); + gl.uniform1i(blurProgram.uniforms.uTexture, target.attach(0)); + blit(temp); + + gl.uniform2f(blurProgram.uniforms.texelSize, 0.0, target.texelSizeY); + gl.uniform1i(blurProgram.uniforms.uTexture, temp.attach(0)); + blit(target); + } +} + +function splatPointer(pointer) +{ + let dx = pointer.deltaX * config.SPLAT_FORCE; + let dy = pointer.deltaY * config.SPLAT_FORCE; + splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color); +} + +function multipleSplats(amount) +{ + for (let i = 0; i < amount; i++) + { + const color = generateColor(); + color.r *= 10.0; + color.g *= 10.0; + color.b *= 10.0; + const x = Math.random(); + const y = Math.random(); + const dx = 1000 * (Math.random() - 0.5); + const dy = 1000 * (Math.random() - 0.5); + splat(x, y, dx, dy, color); + } +} + +function splat(x, y, dx, dy, color) +{ + splatProgram.bind(); + gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0)); + gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height); + gl.uniform2f(splatProgram.uniforms.point, x, y); + gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0); + gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100.0)); + blit(velocity.write); + velocity.swap(); + + gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0)); + gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b); + blit(dye.write); + dye.swap(); +} + +function correctRadius(radius) +{ + let aspectRatio = canvas.width / canvas.height; + if (aspectRatio > 1) + radius *= aspectRatio; + return radius; +} + +canvas.addEventListener('mousedown', e => +{ + let posX = scaleByPixelRatio(e.offsetX); + let posY = scaleByPixelRatio(e.offsetY); + let pointer = pointers.find(p => p.id == -1); + if (pointer == null) + pointer = new pointerPrototype(); + updatePointerDownData(pointer, -1, posX, posY); +}); + +setTimeout(() => +{ + canvas.addEventListener('mousemove', e => + { + let posX = scaleByPixelRatio(e.offsetX); + let posY = scaleByPixelRatio(e.offsetY); + updatePointerMoveData(pointers[0], posX, posY); + }); +}, 500); + +window.addEventListener('mouseup', () => +{ + updatePointerUpData(pointers[0]); +}); + +canvas.addEventListener('touchstart', e => +{ + e.preventDefault(); + const touches = e.targetTouches; + while (touches.length >= pointers.length) + pointers.push(new pointerPrototype()); + for (let i = 0; i < touches.length; i++) + { + let posX = scaleByPixelRatio(touches[i].pageX); + let posY = scaleByPixelRatio(touches[i].pageY); + updatePointerDownData(pointers[i + 1], touches[i].identifier, posX, posY); + } +}); + +canvas.addEventListener('touchmove', e => +{ + e.preventDefault(); + const touches = e.targetTouches; + for (let i = 0; i < touches.length; i++) + { + let pointer = pointers[i + 1]; + if (!pointer.down) continue; + let posX = scaleByPixelRatio(touches[i].pageX); + let posY = scaleByPixelRatio(touches[i].pageY); + updatePointerMoveData(pointer, posX, posY); + } +}, false); + +window.addEventListener('touchend', e => +{ + const touches = e.changedTouches; + for (let i = 0; i < touches.length; i++) + { + let pointer = pointers.find(p => p.id == touches[i].identifier); + if (pointer == null) continue; + updatePointerUpData(pointer); + } +}); + +window.addEventListener('keydown', e => +{ + if (e.code === 'KeyP') + config.PAUSED = !config.PAUSED; + if (e.key === ' ') + splatStack.push(parseInt(Math.random() * 20) + 5); +}); + +function updatePointerDownData(pointer, id, posX, posY) +{ + pointer.id = id; + pointer.down = true; + pointer.moved = false; + pointer.texcoordX = posX / canvas.width; + pointer.texcoordY = 1.0 - posY / canvas.height; + pointer.prevTexcoordX = pointer.texcoordX; + pointer.prevTexcoordY = pointer.texcoordY; + pointer.deltaX = 0; + pointer.deltaY = 0; + pointer.color = generateColor(); +} + +function updatePointerMoveData(pointer, posX, posY) +{ + pointer.prevTexcoordX = pointer.texcoordX; + pointer.prevTexcoordY = pointer.texcoordY; + pointer.texcoordX = posX / canvas.width; + pointer.texcoordY = 1.0 - posY / canvas.height; + pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX); + pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY); + pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0; +} + +function updatePointerUpData(pointer) +{ + pointer.down = false; +} + +function correctDeltaX(delta) +{ + let aspectRatio = canvas.width / canvas.height; + if (aspectRatio < 1) delta *= aspectRatio; + return delta; +} + +function correctDeltaY(delta) +{ + let aspectRatio = canvas.width / canvas.height; + if (aspectRatio > 1) delta /= aspectRatio; + return delta; +} + +function generateColor() +{ + let c = HSVtoRGB(Math.random(), 1.0, 1.0); + c.r *= 0.15; + c.g *= 0.15; + c.b *= 0.15; + return c; +} + +function HSVtoRGB(h, s, v) +{ + let r, g, b, i, f, p, q, t; + i = Math.floor(h * 6); + f = h * 6 - i; + p = v * (1 - s); + q = v * (1 - f * s); + t = v * (1 - (1 - f) * s); + + switch (i % 6) + { + case 0: r = v, g = t, b = p; break; + case 1: r = q, g = v, b = p; break; + case 2: r = p, g = v, b = t; break; + case 3: r = p, g = q, b = v; break; + case 4: r = t, g = p, b = v; break; + case 5: r = v, g = p, b = q; break; + } + + return { + r, + g, + b + }; +} + +function normalizeColor(input) +{ + let output = { + r: input.r / 255, + g: input.g / 255, + b: input.b / 255 + }; + return output; +} + +function wrap(value, min, max) +{ + let range = max - min; + if (range == 0) return min; + return (value - min) % range + min; +} + +function getResolution(resolution) +{ + let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight; + if (aspectRatio < 1) + aspectRatio = 1.0 / aspectRatio; + + let min = Math.round(resolution); + let max = Math.round(resolution * aspectRatio); + + if (gl.drawingBufferWidth > gl.drawingBufferHeight) + return { width: max, height: min }; + else + return { width: min, height: max }; +} + +function getTextureScale(texture, width, height) +{ + return { + x: width / texture.width, + y: height / texture.height + }; +} + +function scaleByPixelRatio(input) +{ + let pixelRatio = window.devicePixelRatio || 1; + return Math.floor(input * pixelRatio); +} + +function hashCode(s) +{ + if (s.length == 0) return 0; + let hash = 0; + for (let i = 0; i < s.length; i++) + { + hash = (hash << 5) - hash + s.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } + return hash; +}; \ No newline at end of file diff --git a/public/ogimage.jpg b/public/ogimage.jpg new file mode 100644 index 0000000..70a0360 Binary files /dev/null and b/public/ogimage.jpg differ diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..8f90132 --- /dev/null +++ b/src/app.js @@ -0,0 +1,112 @@ +import { event_listener_array, calc_delta_time } from './utils'; + +export function perspective_3d(class_name) +{ + const elements = document.body.getElementsByClassName(class_name); + + let velocity_x = 0; + let velocity_y = 0; + let rotation_x = 0; + let rotation_y = 0; + let target_rotation_x = 0; + let target_rotation_y = 0; + + (function update() + { + let dt = calc_delta_time(); + const ss = .08; + + velocity_x = spring(rotation_x, target_rotation_x, velocity_x, 10, dt); + velocity_y = spring(rotation_y, target_rotation_y, velocity_y, 10, dt); + rotation_x += velocity_x * dt * ss; + rotation_y += velocity_y * dt * ss; + + const style = `perspective(700px) rotateX(${rotation_y}rad) rotateY(${rotation_x}rad)`; + + for (const el of elements) + { + el.style.transform = style; + } + + requestAnimationFrame(update); + })(); + + event_listener_array(window, ["mousemove", "touchmove"], (e) => + { + if (e.changedTouches && e.changedTouches[0]) + { + e = e.changedTouches[0]; + } + + target_rotation_x = (e.clientX / window.innerWidth) * 2 - 1; + target_rotation_y = -(e.clientY / window.innerHeight) * 2 + 1; + + target_rotation_x = clamp(target_rotation_x, -0.5, 0.5); + target_rotation_y = clamp(target_rotation_y, -0.5, 0.5); + }, false); + + function spring(position, target, velocity, omega, dt) + { + let n1 = velocity - (position - target) * (Math.pow(omega, 2) * dt); + let n2 = 1 + omega * dt; + return n1 / Math.pow(n2, 2); + } + function clamp(value, min, max) + { + return Math.min(Math.max(value, min), max); + } +} + +(function pick_greeting() +{ + const hours = new Date().getHours(); + const greeing_el = document.querySelector(".greeting"); + + if (hours < 6) + { + const data = "Good night"; + greeing_el.textContent = data; + greeing_el.innerText = data; + } else if (hours >= 6 && hours < 12) + { + const data = "Good morning"; + greeing_el.textContent = data; + greeing_el.innerText = data; + } else if (hours >= 12 && hours < 16) + { + const data = "Good afternoon"; + greeing_el.textContent = data; + greeing_el.innerText = data; + } else if (hours >= 16 && hours <= 23) + { + const data = "Good evening"; + greeing_el.textContent = data; + greeing_el.innerText = data; + } else + { + const data = "Hello"; + greeing_el.textContent = data; + greeing_el.innerText = data; + } + + setTimeout(pick_greeting, 6e4); +})(); + +(function render_time() +{ + const time = new Date(); + let h = time.getHours(); + + h = h.toString().padStart(2, 0); + let m = time.getMinutes().toString().padStart(2, 0); + let s = time.getSeconds().toString().padStart(2, 0); + + const clock_el = document.querySelector('.clock'); + if (!clock_el) return; + + const formatted_date = `${h}:${m}:${s}`; + clock_el.textContent = formatted_date; + clock_el.innerText = formatted_date; + + setTimeout(render_time, 1e3); +})(); \ No newline at end of file diff --git a/src/css/animation.css b/src/css/animation.css new file mode 100644 index 0000000..f356513 --- /dev/null +++ b/src/css/animation.css @@ -0,0 +1,9 @@ +@keyframes fadein { + from { + opacity: 0; + } + + to { + opacity: 1.0; + } +} \ No newline at end of file diff --git a/src/css/button.css b/src/css/button.css new file mode 100644 index 0000000..c2345f8 --- /dev/null +++ b/src/css/button.css @@ -0,0 +1,117 @@ +.button-container { + margin-top: 2em; +} + +.button { + cursor: pointer; + background: #e7e7e7; + border: 0; + border-radius: 2px; + padding: 1.5rem 3rem; + font-family: inherit; + font-variation-settings: "wght" 700; + letter-spacing: -0.02em; + font-size: 1em; + position: relative; + + padding: 1rem 1.5rem; + overflow: hidden; + color: #fff; + opacity: 0.5; + transition: opacity 300ms; +} + +.button:hover { + opacity: 1.0; +} + +.button-inner { + display: grid; + grid-auto-flow: column; + align-items: center; + grid-gap: 0.7em; +} + +.button::before, +.button::after { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.button--hyperion > span { + overflow: hidden; +} + +.button--hyperion > span > span { + overflow: hidden; + mix-blend-mode: difference; +} + +.button--hyperion:hover > span > span { + animation: MoveUpInitial 0.2s forwards, MoveUpEnd 0.2s forwards 0.2s; +} + +.button--hyperion::before { + content: ''; + background: rgba(0, 0, 0, 1); + transition: transform 0.3s cubic-bezier(0.7, 0, 0.2, 1); + transform-origin: 100% 50%; +} + +.button--hyperion:hover::before { + transform: scale3d(0,1,1); + transform-origin: 0% 50%; +} + +@keyframes MoveUpInitial { + to { + transform: translate3d(0,-105%,0); + } +} + +@keyframes MoveUpEnd { + from { + transform: translate3d(0,100%,0); + } + to { + transform: translate3d(0,0,0); + } +} + +.spotify-icon { + position: relative; + fill: currentColor; + mix-blend-mode: difference; +} + +.links-container { + margin-top: 1.5em; + display: grid; + grid-auto-flow: column; + grid-gap: 1em; + justify-content: center; + align-items: center; +} + +.link { + color: white; + text-decoration: none; + letter-spacing: -0.03em; + font-variation-settings: "wght" 500; + font-size: 1.1em; + mix-blend-mode: overlay; + opacity: 0.5; + padding-bottom: 0.2em; + padding-left: 0.5em; + padding-right: 0.5em; + border-bottom: solid 2px #FFFFFF00; + transition: opacity 300ms, border-bottom 300ms; +} + +.link:hover { + opacity: 1.0; + border-bottom: solid 2px #FFFFFF50; +} \ No newline at end of file diff --git a/src/css/font.css b/src/css/font.css new file mode 100644 index 0000000..15b62ae --- /dev/null +++ b/src/css/font.css @@ -0,0 +1,19 @@ +@font-face { + font-family: "Inter Var"; + font-weight: 400 900; + font-display: swap; + font-style: normal; + src: url("/src/font/Inter.woff2") format("woff2"); +} + +html { + font-family: "Inter Var", sans-serif; + font-size: 17px; + box-sizing: border-box; +} + +.greeting { + font-variation-settings: "wght" 400; + letter-spacing: 0rem; + line-height: 1.7rem; +} \ No newline at end of file diff --git a/src/css/style.css b/src/css/style.css new file mode 100644 index 0000000..d5992ab --- /dev/null +++ b/src/css/style.css @@ -0,0 +1,75 @@ +* { + user-select: none; + -webkit-user-select: none +} + +html { + box-sizing: border-box; +} + +body { + margin: 0; + display: grid; + user-select: none; + background: rgb(0, 0, 0); + color: #FFF; + touch-action: none; + -webkit-overflow-scrolling: auto; + overflow: hidden; + overscroll-behavior: none; +} + +canvas { + width: 100%; + height: 100%; +} + +.greeting { + margin-bottom: 0.5em; +} + +.clock { + margin-top: 0 +} + +.container { + display: grid; + place-items: center; + width: 100%; + height: 100vh; + font-size: 17px; +} + +canvas, +.container { + grid-column: 1 / -1; + grid-row: 1 / -1 +} + +.content { + text-align: center; + letter-spacing: 0.5em; + animation: fadein 2s both cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +.content>* { + opacity: 0.5; +} + +.content-info { + transition: opacity 300ms; +} + +.content-info:hover { + opacity: 1.0; +} + +.button-container { + opacity: 1.0; +} + +@media (max-width: 420px) { + body { + height: 100%; + } +} \ No newline at end of file diff --git a/src/font/Inter.woff2 b/src/font/Inter.woff2 new file mode 100644 index 0000000..365eedc Binary files /dev/null and b/src/font/Inter.woff2 differ diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..ffedd32 --- /dev/null +++ b/src/main.js @@ -0,0 +1,19 @@ +// CSS Import +import './css/style.css'; +import './css/font.css'; +import './css/animation.css'; +import './css/button.css'; + +// JS Import +import './app'; + +import { perspective_3d } from './app'; +import { import_js_as_module } from './utils'; + +window.onload = start; + +async function start() +{ + await import_js_as_module('/fluid.js'); + perspective_3d('perspective'); +} \ No newline at end of file diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..4e02d9c --- /dev/null +++ b/src/utils.js @@ -0,0 +1,38 @@ +export let last_update = Date.now(); + +export function calc_delta_time() +{ + let now = Date.now(); + let dt = (now - last_update) / 1e3; + dt = Math.min(dt, 0.016); + last_update = now; + return dt; +} + +export function event_listener_array(whos, event_names, callback, options = null) +{ + if (!Array.isArray(whos)) + { + whos = [whos]; + } + for (const name of event_names) + { + for (const who of whos) + { + who.addEventListener(name, callback, options); + } + } +} + +export async function import_js_as_module(url) +{ + return new Promise((resole, reject) => + { + const body = document.getElementsByTagName('body')[0]; + const script = document.createElement('script'); + script.type = 'module'; + script.src = url; + script.onload = resole; + body.appendChild(script); + }); +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..a9b4b04 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,88 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +colorette@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + +esbuild@^0.12.8: + version "0.12.9" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.9.tgz#bed4e7087c286cd81d975631f77d47feb1660070" + integrity sha512-MWRhAbMOJ9RJygCrt778rz/qNYgA4ZVj6aXnNPxFjs7PmIpb0fuB9Gmg5uWrr6n++XKwwm/RmSz6RR5JL2Ocsw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +is-core-module@^2.2.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" + integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A== + dependencies: + has "^1.0.3" + +nanoid@^3.1.23: + version "3.1.23" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" + integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== + +path-parse@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +postcss@^8.3.4: + version "8.3.5" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.5.tgz#982216b113412bc20a86289e91eb994952a5b709" + integrity sha512-NxTuJocUhYGsMiMFHDUkmjSKT3EdH4/WbGF6GCi1NDGk+vbcUTun4fpbOqaPtD8IIsztA2ilZm2DhYCuyN58gA== + dependencies: + colorette "^1.2.2" + nanoid "^3.1.23" + source-map-js "^0.6.2" + +resolve@^1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + +rollup@^2.38.5: + version "2.52.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.52.2.tgz#a7e90d10ddae3e8472c2857bd9f44b09ef34a47a" + integrity sha512-4RlFC3k2BIHlUsJ9mGd8OO+9Lm2eDF5P7+6DNQOp5sx+7N/1tFM01kELfbxlMX3MxT6owvLB1ln4S3QvvQlbUA== + optionalDependencies: + fsevents "~2.3.2" + +source-map-js@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" + integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== + +vite@^2.3.7: + version "2.3.8" + resolved "https://registry.yarnpkg.com/vite/-/vite-2.3.8.tgz#42e3e03953859fd410e4e6ab3d1cca0aab2adc3c" + integrity sha512-QiEx+iqNnJntSgSF2fWRQvRey9pORIrtNJzNyBJXwc+BdzWs83FQolX84cTBo393cfhObrtWa6180dAa4NLDiQ== + dependencies: + esbuild "^0.12.8" + postcss "^8.3.4" + resolve "^1.20.0" + rollup "^2.38.5" + optionalDependencies: + fsevents "~2.3.2"