;(() => { const elemParent = document.querySelector('div.layout-background') const elemSprite = document.querySelector('link[rel="texture"]') if (!elemParent || !elemSprite) throw 'Invalid Document' const FRAME_INTERVAL = 1000 const FRAME_TIME_IDLE = 12 const FRAME_TIME_ACTIVE = 60 const CAM_ACCEL = 2 const CAM_FRICTION = 0.85 const PARTICLE_COUNT = 160 const COLOR_PARTICLE = 0x484848 const COLOR_FOREGROUND = 0x363636 const COLOR_BACKGROUND = 0x000000 let keysHeld = new Set() let frameTime = FRAME_INTERVAL / FRAME_TIME_IDLE let lastTime = 0 let lastFrame = 0 let time = 0 let cameraMVP = mat4() let cameraRotX = 0 let cameraRotY = 0 let camVelRotX = 0 let camVelRotY = 0 let spriteTexture: WebGLTexture let spriteModel: Float32Array let particlePos: Float32Array let particleColor: Float32Array let particleData: { x: number y: number z: number vx: number vy: number vz: number life: number maxLife: number }[] = [] let planeW: number let planeH: number let planeSegX: number let planeSegY: number let planeVerts: Float32Array let planeOrig: Float32Array let planeIdx const canvas = document.createElement('canvas') const gl = canvas.getContext('webgl') if (!gl) throw 'Failed to allocate WebGL Context' // --- Camera Controls --- document.addEventListener('keydown', (e) => { if (!e.shiftKey && !e.ctrlKey) return if (e.target instanceof HTMLTextAreaElement) return if (e.target instanceof HTMLInputElement) return if ((e as any).isContentEditable) return keysHeld.add(e.key) }) document.addEventListener('keyup', (e) => { keysHeld.delete(e.key) }) // --- Math Functions --- function mat4(): Float32Array { return new Float32Array(16) } function mat4Identity(m: Float32Array): Float32Array { m[0] = 1 m[1] = 0 m[2] = 0 m[3] = 0 m[4] = 0 m[5] = 1 m[6] = 0 m[7] = 0 m[8] = 0 m[9] = 0 m[10] = 1 m[11] = 0 m[12] = 0 m[13] = 0 m[14] = 0 m[15] = 1 return m } function mat4Multiply(out: Float32Array, a: Float32Array, b: Float32Array): Float32Array { for (let i = 0; i < 4; i++) for (let j = 0; j < 4; j++) { out[j * 4 + i] = 0 for (let k = 0; k < 4; k++) out[j * 4 + i] += a[k * 4 + i] * b[j * 4 + k] } return out } function mat4Perspective(m: Float32Array, fovY: number, aspect: number, near: number, far: number): Float32Array { const f = 1.0 / Math.tan(fovY / 2) m[0] = f / aspect m[1] = 0 m[2] = 0 m[3] = 0 m[4] = 0 m[5] = f m[6] = 0 m[7] = 0 m[8] = 0 m[9] = 0 m[10] = (far + near) / (near - far) m[11] = -1 m[12] = 0 m[13] = 0 m[14] = (2 * far * near) / (near - far) m[15] = 0 return m } function mat4RotateX(m: Float32Array, angle: number): Float32Array { const c = Math.cos(angle) const s = Math.sin(angle) const t = mat4Identity(mat4()) t[5] = c t[6] = s t[9] = -s t[10] = c return mat4Multiply(mat4(), t, m) } function mat4RotateY(m: Float32Array, angle: number): Float32Array { const c = Math.cos(angle) const s = Math.sin(angle) const t = mat4Identity(mat4()) t[0] = c t[2] = -s t[8] = s t[10] = c return mat4Multiply(mat4(), t, m) } function mat4Translate(m: Float32Array, x: number, y: number, z: number): Float32Array { const t = mat4Identity(mat4()) t[12] = x t[13] = y t[14] = z return mat4Multiply(mat4(), t, m) } function randFloat(lo: number, hi: number): number { return lo + Math.random() * (hi - lo) } function randFloatSpread(range: number): number { return randFloat(-range / 2, range / 2) } function degToRad(d: number): number { return (d * Math.PI) / 180 } function intToRGB(i: number): [number, number, number] { return [((i >> 16) & 0xff) / 255, ((i >> 8) & 0xff) / 255, ((i >> 0) & 0xff) / 255] } // --- Prepare Shaders --- function createShader(type: number, src: string): WebGLShader { if (!gl) throw 'Missing Global GL Context' const s = gl.createShader(type) if (!s) throw 'Shader Compilation Failed' gl.shaderSource(s, src) gl.compileShader(s) return s } function createProgram(vert: string, frag: string): WebGLProgram { if (!gl) throw 'Missing Global GL Context' const p = gl.createProgram() gl.attachShader(p, createShader(gl.VERTEX_SHADER, vert)) gl.attachShader(p, createShader(gl.FRAGMENT_SHADER, frag)) gl.linkProgram(p) return p } const planeProg = createProgram( `attribute vec3 aPosition; uniform mat4 uMVP; uniform float uTime; varying float vDist; void main() { float dist = sqrt(aPosition.x * aPosition.x + aPosition.z * aPosition.z); float wave = sin(dist * 0.5 - uTime); float cave = -exp(-dist * 0.1) * 3.5; vec3 pos = vec3(aPosition.x, aPosition.y + wave + cave, aPosition.z); vDist = length((uMVP * vec4(pos, 1.0)).xyz); gl_Position = uMVP * vec4(pos, 1.0); }`, `precision mediump float; uniform vec3 uFogColor; uniform float uFogNear; uniform float uFogFar; varying float vDist; void main() { float fog = clamp((vDist - uFogNear) / (uFogFar - uFogNear), 0.0, 1.0); vec3 color = mix(vec3(${intToRGB(COLOR_FOREGROUND).join(',')}), uFogColor, fog); gl_FragColor = vec4(color, 1.0); }`, ) const particleProg = createProgram( `attribute vec3 aPosition; attribute vec4 aColor; uniform mat4 uMVP; varying vec4 vColor; varying float vDist; void main() { vec4 pos = uMVP * vec4(aPosition, 1.0); vDist = length(pos.xyz); vColor = aColor; gl_PointSize = 3.0; gl_Position = pos; }`, `precision mediump float; uniform vec3 uFogColor; uniform float uFogNear; uniform float uFogFar; varying vec4 vColor; varying float vDist; void main() { float fog = clamp((vDist - uFogNear) / (uFogFar - uFogNear), 0.0, 1.0); float alpha = vColor.a * (1.0 - fog); gl_FragColor = vec4(mix(vColor.rgb, uFogColor, fog), alpha); }`, ) const spriteProg = createProgram( `attribute vec2 aPosition; attribute vec2 aUV; uniform mat4 uMVP; varying vec2 vUV; void main() { vUV = aUV; gl_Position = uMVP * vec4(aPosition.x , 0, aPosition.y, 1); }`, `precision mediump float; uniform sampler2D uTex; varying vec2 vUV; void main() { gl_FragColor = texture2D(uTex, vUV); }`, ) const planeBuf = gl.createBuffer() const planeOrigBuf = gl.createBuffer() const planeIdxBuf = gl.createBuffer() const uTimePlane = gl.getUniformLocation(planeProg, 'uTime') const uMVPPlane = gl.getUniformLocation(planeProg, 'uMVP') const uFogColorPlane = gl.getUniformLocation(planeProg, 'uFogColor') const uFogNearPlane = gl.getUniformLocation(planeProg, 'uFogNear') const uFogFarPlane = gl.getUniformLocation(planeProg, 'uFogFar') const particlePosBuf = gl.createBuffer() const particleColorBuf = gl.createBuffer() const uMVPParticle = gl.getUniformLocation(particleProg, 'uMVP') const uFogColorParticle = gl.getUniformLocation(particleProg, 'uFogColor') const uFogNearParticle = gl.getUniformLocation(particleProg, 'uFogNear') const uFogFarParticle = gl.getUniformLocation(particleProg, 'uFogFar') const spriteImage = new Image() spriteImage.src = elemSprite.href const spriteBuf = gl.createBuffer() const spriteIdxBuf = gl.createBuffer() const uMVPSprite = gl.getUniformLocation(spriteProg, 'uMVP') const uTexSprite = gl.getUniformLocation(spriteProg, 'uTex') function spawnParticle(i: number) { // Put them in the center because its out of frame anyways save some resources const x = randFloat(-planeW / 2, planeW / 2) const z = randFloat(-planeH / 2, planeH / 2) const l = randFloat(3.0, 6.0) particleData[i] = { x, y: 0, z, vx: randFloatSpread(0.05), vy: randFloat(0.02, 0.05), vz: randFloatSpread(0.05), life: l, maxLife: l, } const p = i * 3 particlePos[p + 0] = x particlePos[p + 1] = 0 particlePos[p + 2] = z const c = i * 4 const [r, g, b] = intToRGB(COLOR_PARTICLE) particleColor[c + 0] = r particleColor[c + 1] = g particleColor[c + 2] = b particleColor[c + 3] = 0 } function startup() { if (!gl) throw 'Missing Global GL Context' // Render Resolution canvas.width = Math.floor(window.innerWidth * 0.3) canvas.height = Math.floor(window.innerHeight * 0.3) canvas.style.width = window.innerWidth + 'px' canvas.style.height = window.innerHeight + 'px' canvas.style.imageRendering = 'pixelated' // Build Sprite spriteImage.onload = () => { gl.bindBuffer(gl.ARRAY_BUFFER, spriteBuf) gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, 1, 1, -1, 0, 1, 1, 1, 0, 0, -1, 1, 1, 0]), gl.STATIC_DRAW, ) gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, spriteIdxBuf) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW) spriteModel = mat4Identity(mat4()) spriteModel = mat4RotateY(spriteModel, degToRad(180)) spriteModel = mat4Translate(spriteModel, 0, 4, 16) // Upload Texture spriteTexture = gl.createTexture() gl.bindTexture(gl.TEXTURE_2D, spriteTexture) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, spriteImage) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) 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.useProgram(spriteProg) gl.uniform1i(uTexSprite, 0) gl.activeTexture(gl.TEXTURE0) gl.bindTexture(gl.TEXTURE_2D, spriteTexture) gl.bindBuffer(gl.ARRAY_BUFFER, spriteBuf) } // Build Plane gl.viewport(0, 0, canvas.width, canvas.height) gl.clearColor(...intToRGB(COLOR_BACKGROUND), 1) gl.enable(gl.BLEND) gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) const ratio = canvas.width / canvas.height const scale = 24 planeSegX = Math.round(12 * ratio) planeSegY = 12 planeW = scale * ratio planeH = scale const nx = planeSegX + 1 const ny = planeSegY + 1 planeVerts = new Float32Array(nx * ny * 3) planeOrig = new Float32Array(nx * ny * 3) let vi = 0 for (let iy = 0; iy < ny; iy++) { for (let ix = 0; ix < nx; ix++) { const x = (ix / planeSegX - 0.5) * planeW const z = (iy / planeSegY - 0.5) * planeH planeVerts[vi] = x planeVerts[vi + 1] = 0 planeVerts[vi + 2] = z planeOrig[vi] = x planeOrig[vi + 1] = 0 planeOrig[vi + 2] = z vi += 3 } } const lines = [] for (let iy = 0; iy < ny; iy++) { for (let ix = 0; ix < nx; ix++) { const idx = iy * nx + ix // wireframe indices two triangles per quad if (ix < planeSegX) { lines.push(idx, idx + 1) } if (iy < planeSegY) { lines.push(idx, idx + nx) } if (ix < planeSegX && iy < planeSegY) { lines.push(idx, idx + nx + 1) } } } planeIdx = new Uint16Array(lines) // Upload plane buffers gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf) gl.bufferData(gl.ARRAY_BUFFER, planeVerts, gl.DYNAMIC_DRAW) gl.bindBuffer(gl.ARRAY_BUFFER, planeOrigBuf) gl.bufferData(gl.ARRAY_BUFFER, planeOrig, gl.STATIC_DRAW) gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, planeIdxBuf) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, planeIdx, gl.STATIC_DRAW) // Initialize particles if first time if (particleData.length === 0) { particlePos = new Float32Array(PARTICLE_COUNT * 3) particleColor = new Float32Array(PARTICLE_COUNT * 4) for (let i = 0; i < PARTICLE_COUNT; i++) { spawnParticle(i) } } gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf) gl.bufferData(gl.ARRAY_BUFFER, particlePos, gl.DYNAMIC_DRAW) gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf) gl.bufferData(gl.ARRAY_BUFFER, particleColor, gl.DYNAMIC_DRAW) // Initialize Fog gl.useProgram(planeProg) gl.uniform3f(uFogColorPlane, ...intToRGB(COLOR_BACKGROUND)) gl.uniform1f(uFogNearPlane, 2) gl.uniform1f(uFogFarPlane, 22) gl.useProgram(particleProg) gl.uniformMatrix4fv(uMVPParticle, false, cameraMVP) gl.uniform3f(uFogColorParticle, ...intToRGB(COLOR_BACKGROUND)) gl.uniform1f(uFogNearParticle, 6) gl.uniform1f(uFogFarParticle, 24) // Force dirty camera reset camVelRotY = 0.005 } function animate(now: number) { requestAnimationFrame(animate) // Sleep (warning this sucks) if (now - lastFrame < frameTime) return const delta = (now - lastTime) * 0.00008 || 0 lastFrame = now lastTime = now time += delta if (!gl) throw 'Missing Global GL Context' gl.clear(gl.COLOR_BUFFER_BIT) // Camera Movement if (keysHeld.has('ArrowLeft')) camVelRotY -= CAM_ACCEL if (keysHeld.has('ArrowRight')) camVelRotY += CAM_ACCEL if (keysHeld.has('ArrowUp')) camVelRotX -= CAM_ACCEL if (keysHeld.has('ArrowDown')) camVelRotX += CAM_ACCEL + 8 camVelRotX *= CAM_FRICTION camVelRotY *= CAM_FRICTION const dirty = Math.abs(camVelRotX) + Math.abs(camVelRotY) > 0.001 if (dirty) { cameraRotX += camVelRotX * delta * 1000 cameraRotY += camVelRotY * delta * 1000 // Update Camera const ratio = canvas.width / canvas.height const proj = mat4Perspective(mat4(), degToRad(70), ratio, 0.1, 100) let view = mat4Identity(mat4()) view = mat4Translate(view, 0, -7.5, -15) view = mat4RotateX(view, degToRad(camVelRotX + 33.75)) view = mat4RotateY(view, degToRad(camVelRotY)) cameraMVP = mat4Multiply(mat4(), proj, view) gl.useProgram(planeProg) gl.uniformMatrix4fv(uMVPPlane, false, cameraMVP) gl.useProgram(particleProg) gl.uniformMatrix4fv(uMVPParticle, false, cameraMVP) frameTime = FRAME_INTERVAL / FRAME_TIME_ACTIVE } else { frameTime = FRAME_INTERVAL / FRAME_TIME_IDLE } // Update Sprite if (spriteTexture) { const spriteMVP = mat4Multiply(mat4(), cameraMVP, spriteModel) gl.useProgram(spriteProg) gl.uniformMatrix4fv(uMVPSprite, false, spriteMVP) gl.uniform1i(uTexSprite, 0) gl.activeTexture(gl.TEXTURE0) gl.bindTexture(gl.TEXTURE_2D, spriteTexture) gl.bindBuffer(gl.ARRAY_BUFFER, spriteBuf) const aPos = gl.getAttribLocation(spriteProg, 'aPosition') const aUV = gl.getAttribLocation(spriteProg, 'aUV') gl.enableVertexAttribArray(aPos) gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 16, 0) gl.enableVertexAttribArray(aUV) gl.vertexAttribPointer(aUV, 2, gl.FLOAT, false, 16, 8) gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, spriteIdxBuf) gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0) } { // Update Particles for (let i = 0; i < PARTICLE_COUNT; i++) { const p = particleData[i] // fade out p.life -= delta if (p.life <= 0) { spawnParticle(i) continue } particleColor[i * 4 + 3] = Math.min((p.life / p.maxLife) * 1.2, 1) // drift away p.x += p.vx * delta * 40 p.y += p.vy * delta * 20 p.z += p.vz * delta * 40 const pi = i * 3 particlePos[pi + 0] = p.x particlePos[pi + 1] = p.y particlePos[pi + 2] = p.z } gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf) gl.bufferSubData(gl.ARRAY_BUFFER, 0, particlePos) gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf) gl.bufferSubData(gl.ARRAY_BUFFER, 0, particleColor) // Draw Particles gl.useProgram(particleProg) const aPos = gl.getAttribLocation(particleProg, 'aPosition') const aCol = gl.getAttribLocation(particleProg, 'aColor') gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf) gl.enableVertexAttribArray(aPos) gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0) gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf) gl.enableVertexAttribArray(aCol) gl.vertexAttribPointer(aCol, 4, gl.FLOAT, false, 0, 0) gl.drawArrays(gl.POINTS, 0, PARTICLE_COUNT) } { // Update Plane for (let i = 0; i < planeVerts.length; i += 3) { const x = planeOrig[i] const z = planeOrig[i + 2] const dist = Math.sqrt(x * x + z * z) planeVerts[i] = x planeVerts[i + 1] = Math.sin(dist * 0.5 - time) * 0.5 + -Math.exp(-dist * 0.1) * 3.5 planeVerts[i + 2] = z } gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf) gl.bufferSubData(gl.ARRAY_BUFFER, 0, planeVerts) // Draw Plane gl.useProgram(planeProg) gl.uniform1f(uTimePlane, time) const aPos = gl.getAttribLocation(planeProg, 'aPosition') gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf) gl.enableVertexAttribArray(aPos) gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0) gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, planeIdxBuf) gl.drawElements(gl.LINES, planeIdx.length, gl.UNSIGNED_SHORT, 0) } } // --- Prepare Canvas --- window.addEventListener('resize', startup) startup() animate(0) elemParent.append(canvas) })()