class Portal { constructor(resourceDir = '/', animate = true, randomize = false) { // Vertex shader. const vglsl = `#version 300 es layout (location = 0) in vec3 Position; uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; uniform vec2 canvasResolution; vec4 projection_from_position(vec4 position) { vec4 projection = position * 0.5; projection.xy = vec2(projection.x + projection.w, projection.y + projection.w); projection.zw = position.zw; return projection; } out vec4 texProj0; void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(Position, 1.0); texProj0 = projection_from_position(gl_Position); texProj0 = vec4(texProj0.xy * canvasResolution / max(canvasResolution.x, canvasResolution.y), texProj0.zw); }` // Fragment shader. const fglsl = `#version 300 es precision highp float; uniform sampler2D sky; uniform sampler2D particles; uniform float dt; in vec4 texProj0; const int LAYERS = 15; const vec3 COLORS[] = vec3[]( vec3(0.022087, 0.098399, 0.110818), vec3(0.011892, 0.095924, 0.089485), vec3(0.027636, 0.101689, 0.100326), vec3(0.046564, 0.109883, 0.114838), vec3(0.064901, 0.117696, 0.097189), vec3(0.063761, 0.086895, 0.123646), vec3(0.084817, 0.111994, 0.166380), vec3(0.097489, 0.154120, 0.091064), vec3(0.106152, 0.131144, 0.195191), vec3(0.097721, 0.110188, 0.187229), vec3(0.133516, 0.138278, 0.148582), vec3(0.070006, 0.243332, 0.235792), vec3(0.196766, 0.142899, 0.214696), vec3(0.047281, 0.315338, 0.321970), vec3(0.204675, 0.390010, 0.302066), vec3(0.080955, 0.314821, 0.661491) ); const mat4 SCALE_TRANSLATE = mat4( 0.5, 0.0, 0.0, 0.25, 0.0, 0.5, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ); mat2 mat2_rotate_z(float radians) { return mat2( cos(radians), -sin(radians), sin(radians), cos(radians) ); } mat4 portal_layer(float layer) { mat4 translate = mat4( 1.0, 0.0, 0.0, 17.0 / layer, 0.0, 1.0, 0.0, (2.0 + layer / 1.5) * (dt * 1.5), 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ); mat2 rotate = mat2_rotate_z(radians((layer * layer * 4321.0 + layer * 9.0) * 2.0)); mat2 scale = mat2((4.5 - layer / 4.0) * 2.0); return mat4(scale * rotate) * translate * SCALE_TRANSLATE; } out vec4 fragColor; void main() { vec3 color = textureProj(sky, texProj0).rgb * COLORS[0]; for (int i = 0; i < LAYERS; i++) { color += textureProj(particles, texProj0 * portal_layer(float(i + 1))).rgb * COLORS[i]; } fragColor = vec4(color, 1.0); }` const images = [ `${resourceDir !== '/' ? resourceDir : ''}/sky.png`, `${resourceDir !== '/' ? resourceDir : ''}/portal.png`, ]; // Version & notice. this.version = '1.3'; this.notice(); // Set properties. this.animate = animate; this.randomize = randomize; // Create a canvas and acquire its context. this.canvas = this.createCanvas(); this.gl = this.canvas.getContext('webgl2'); // If WebGL isn't part of available features, fail. if (!this.gl) { alert("Unable to initialize WebGL 2.\nThe website will lack parallax animation."); console.error("Unable to initialize WebGL 2. Your browser or machine may not support it."); return this.destroyCanvas(); } // Set default speed. this.speed = 1; this.initialSpeed = this.speed; // It's integral for the animation to tie it to the current time, else it will be tied to the framerate. this.currentTime = Date.now(); this.pauseTime = null; // Get current tick and slightly randomize it if animation is enabled. this.tick = (this.animate && randomize) * this.randomRange(0, 10); // Build shaders. this.prog = this.build(vglsl, fglsl); if (!this.prog) { alert("Failed to compile WebGL 2 shaders.\nOpen the developer console for debug output."); return this.destroyCanvas(); } // Bind class context to the resize handler, pass it to the event bus. window.addEventListener('resize', this.resize.bind(this)); // Set the clickTime to null for the onClick event. this.clickTime = null; // Create image loading promises. this.promises = images.map(image => this.loadImage(image)); // Initialize the scene. this.initialize(); // Once all promises have been fulfilled, build the scene. Promise.all(this.promises).then(resources => { // Load resources. resources.forEach((image, index) => this.loadResource(image, index)); // Bind program this.gl.useProgram(this.prog); // Begin render. this.render(); }); } createCanvas() { let canvas = document.createElement('canvas'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; canvas.style.display = 'block'; canvas.style.position = 'fixed'; canvas.style.bottom = '0'; canvas.style.left = '0'; canvas.style.zIndex = '-1'; canvas.innerHTML = 'Your browser does not support the canvas element, which is required for animation.'; // Add a class for subclassing support. canvas.classList.add('ecmaportal'); canvas.id = 'ecmaportal'; if (this.animate) { canvas.onclick = () => this.clickTime = Date.now(); } document.body.appendChild(canvas); return canvas; } destroyCanvas() { document.body.removeChild(this.canvas); } notice() { console.log( `%c \u2587%c\u2587%c\u2587 %c\u2587%c\u2587%c\u2587 %c // ECMAPortal v${this.version} by Endermanch & WiPet\n\n\thttps://enderman.ch\n\thttps://go.enderman.ch/wipet`, 'color: #E58EFF', 'color: #D52DFF', 'color: #E58EFF', 'color: #E58EFF', 'color: #D52DFF', 'color: #E58EFF', 'color: #008000', ); console.log( 'If any errors occur below, please send a screenshot of them my way!\n\t%ccontact@enderman.ch', 'color: #87CEFA' ); } randomRange(min, max) { return Math.random() * (max - min) + min; } build(vshSource, fshSource) { let vsh = this.gl.createShader(this.gl.VERTEX_SHADER); this.gl.shaderSource(vsh, vshSource); this.gl.compileShader(vsh); if (!this.gl.getShaderParameter(vsh, this.gl.COMPILE_STATUS)) { console.error(this.gl.getShaderInfoLog(vsh)); return false; } let fsh = this.gl.createShader(this.gl.FRAGMENT_SHADER); this.gl.shaderSource(fsh, fshSource); this.gl.compileShader(fsh); if (!this.gl.getShaderParameter(fsh, this.gl.COMPILE_STATUS)) { console.error(this.gl.getShaderInfoLog(fsh)); return false; } let program = this.gl.createProgram(); this.gl.attachShader(program, vsh); this.gl.attachShader(program, fsh); this.gl.linkProgram(program); if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) { console.error(this.gl.getProgramInfoLog(program)); return false; } return program; } loadImage(url) { return new Promise((resolve, reject) => { let img = new Image(); img.onload = () => resolve(img); img.onerror = reject; img.src = url; }); } loadResource(image, index) { const samplerParameters = [ this.gl.CLAMP_TO_EDGE, this.gl.MIRRORED_REPEAT, ]; let sampler = this.gl.createSampler(); this.gl.samplerParameteri(sampler, this.gl.TEXTURE_WRAP_S, samplerParameters[index]); this.gl.samplerParameteri(sampler, this.gl.TEXTURE_WRAP_T, samplerParameters[index]); this.gl.samplerParameteri(sampler, this.gl.TEXTURE_WRAP_R, samplerParameters[index]); this.gl.samplerParameteri(sampler, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST_MIPMAP_LINEAR); this.gl.samplerParameteri(sampler, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST); this.gl.bindSampler(index, sampler); let texture = this.gl.createTexture(); this.gl.activeTexture(this.gl.TEXTURE0 + index); this.gl.bindTexture(this.gl.TEXTURE_2D, texture); this.gl.texImage2D( this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, image ); this.gl.generateMipmap(this.gl.TEXTURE_2D); } initialize() { this.uniforms = { modelViewMatrix: this.gl.getUniformLocation(this.prog, "modelViewMatrix"), projectionMatrix: this.gl.getUniformLocation(this.prog, "projectionMatrix"), dt: this.gl.getUniformLocation(this.prog, "dt"), resolution: this.gl.getUniformLocation(this.prog, "canvasResolution"), sky: this.gl.getUniformLocation(this.prog, "sky"), particles: this.gl.getUniformLocation(this.prog, "particles"), } let vertexPosBuffer = this.gl.createBuffer(); let positions = new Float32Array([ -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0 ]); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexPosBuffer); this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null); let vertexTexBuffer = this.gl.createBuffer(); let texCoords = new Float32Array([ 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ]); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexTexBuffer); this.gl.bufferData(this.gl.ARRAY_BUFFER, texCoords, this.gl.STATIC_DRAW); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null); let vertexPosLocation = 0; this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexPosBuffer); this.gl.vertexAttribPointer(vertexPosLocation, 2, this.gl.FLOAT, false, 0, 0); this.gl.enableVertexAttribArray(vertexPosLocation); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null); let vertexTexLocation = 4; this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexTexBuffer); this.gl.vertexAttribPointer(vertexTexLocation, 2, this.gl.FLOAT, false, 0, 0); this.gl.enableVertexAttribArray(vertexTexLocation); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null); } scene() { let identityMatrix = new Float32Array([ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ]); if (this.animate) { // Speed up the animation if the user has clicked. if (this.clickTime !== null) { this.speed = 20 - Math.min((Date.now() - this.clickTime) / 100, 20 - this.initialSpeed); if (this.speed === this.initialSpeed) this.clickTime = null; } // R(t) = t + dt (mod 20) this.tick += this.dt; this.tick %= 20; } this.gl.uniformMatrix4fv(this.uniforms.modelViewMatrix, false, identityMatrix); this.gl.uniformMatrix4fv(this.uniforms.projectionMatrix, false, identityMatrix); this.gl.uniform2f(this.uniforms.resolution, this.canvas.clientWidth, this.canvas.clientHeight); this.gl.uniform1f(this.uniforms.dt, this.tick); this.gl.uniform1i(this.uniforms.sky, 0); this.gl.uniform1i(this.uniforms.particles, 1); this.gl.drawArraysInstanced(this.gl.TRIANGLES, 0, 6, 1); } render() { // Make sure the viewport matches the size of the canvas' drawingBuffer. this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight); // Calculate the delta time. this.dt = (Date.now() - this.currentTime) * this.speed / 1000000; this.currentTime = Date.now(); // Run the scene. this.scene(); // Request the next animation frame if animation is enabled. if (this.animate) requestAnimationFrame(this.render.bind(this)); } resize() { let width = window.innerWidth; let height = window.innerHeight; if ( this.canvas.width !== width || this.canvas.height !== height ) { this.canvas.width = width; this.canvas.height = height; } // If we have animation disabled, we still have to re-render the scene once on resize. if (!this.animate) requestAnimationFrame(this.render.bind(this)); } continue() { if (this.pauseTime === null) return; // Add the time that has passed since the pause to the current time. this.currentTime += Date.now() - this.pauseTime; this.pauseTime = null; this.animate = true; this.render(); } pause() { if (this.pauseTime !== null) return; // Save the pause time to calculate the delta time. this.pauseTime = Date.now(); this.animate = false; } destruct() { // Doesn't need to be called usually, the garbage collector should do its job. // The method isn't implemented, but the pseudocode is as follows: // this.gl.deleteBuffer(vertexPosBuffer); // this.gl.deleteBuffer(vertexTexBuffer); // this.gl.deleteSampler(sampler); // this.gl.deleteTexture(texture); this.gl.deleteProgram(this.prog); } } const portal = new Portal('/images/portal', true, true);