├── .gitignore ├── .gitattributes ├── docs ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── images │ ├── noise-dark.png │ ├── noise-light.png │ ├── gear.svg │ └── resize.svg ├── readme │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── images │ │ ├── noise-dark.png │ │ └── noise-light.png │ ├── css │ │ └── page.css │ └── index.html ├── shaders │ ├── update-2D.vert │ ├── display-2D.vert │ ├── display-2D.frag │ └── update-2D.frag ├── index.html ├── script │ ├── page.min.js │ └── main.min.js └── css │ └── page.css ├── src ├── resources │ └── README │ │ ├── update.png │ │ ├── illustration.png │ │ └── cells_packing.png ├── shaders │ ├── update-2D.vert │ ├── display-2D.vert │ ├── display-2D.frag │ ├── _packing.frag │ └── update-2D.frag ├── ts │ ├── gl-utils │ │ ├── gl-resource.ts │ │ ├── utils.ts │ │ ├── viewport.ts │ │ ├── vbo.ts │ │ ├── gl-canvas.ts │ │ ├── fbo.ts │ │ ├── shader-sources.ts │ │ ├── matrix │ │ │ ├── vec3.ts │ │ │ └── mat4.ts │ │ ├── shader-manager.ts │ │ └── shader.ts │ ├── main.ts │ ├── parameters.ts │ └── automaton-2D.ts ├── config │ ├── tslint.json │ ├── webpack.config.js │ └── tsconfig.json ├── build-shaders.js └── generate-page-template.ts ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *generated.* 3 | debug.log 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /docs/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piellardj/game-of-life-webgl/HEAD/docs/favicon-16x16.png -------------------------------------------------------------------------------- /docs/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piellardj/game-of-life-webgl/HEAD/docs/favicon-32x32.png -------------------------------------------------------------------------------- /docs/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piellardj/game-of-life-webgl/HEAD/docs/favicon-96x96.png -------------------------------------------------------------------------------- /docs/images/noise-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piellardj/game-of-life-webgl/HEAD/docs/images/noise-dark.png -------------------------------------------------------------------------------- /docs/images/noise-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piellardj/game-of-life-webgl/HEAD/docs/images/noise-light.png -------------------------------------------------------------------------------- /docs/readme/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piellardj/game-of-life-webgl/HEAD/docs/readme/favicon-16x16.png -------------------------------------------------------------------------------- /docs/readme/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piellardj/game-of-life-webgl/HEAD/docs/readme/favicon-32x32.png -------------------------------------------------------------------------------- /docs/readme/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piellardj/game-of-life-webgl/HEAD/docs/readme/favicon-96x96.png -------------------------------------------------------------------------------- /src/resources/README/update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piellardj/game-of-life-webgl/HEAD/src/resources/README/update.png -------------------------------------------------------------------------------- /docs/readme/images/noise-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piellardj/game-of-life-webgl/HEAD/docs/readme/images/noise-dark.png -------------------------------------------------------------------------------- /docs/readme/images/noise-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piellardj/game-of-life-webgl/HEAD/docs/readme/images/noise-light.png -------------------------------------------------------------------------------- /src/resources/README/illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piellardj/game-of-life-webgl/HEAD/src/resources/README/illustration.png -------------------------------------------------------------------------------- /src/resources/README/cells_packing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piellardj/game-of-life-webgl/HEAD/src/resources/README/cells_packing.png -------------------------------------------------------------------------------- /docs/shaders/update-2D.vert: -------------------------------------------------------------------------------- 1 | attribute vec2 aCorner; // {-1,1}x{-1,1} 2 | 3 | varying vec2 coords; // {0,1}x{0,1} 4 | 5 | void main(void) 6 | { 7 | coords = .5 * aCorner + .5; 8 | gl_Position = vec4(aCorner, 0, 1); 9 | } 10 | -------------------------------------------------------------------------------- /src/shaders/update-2D.vert: -------------------------------------------------------------------------------- 1 | attribute vec2 aCorner; // {-1,1}x{-1,1} 2 | 3 | varying vec2 coords; // {0,1}x{0,1} 4 | 5 | void main(void) 6 | { 7 | coords = .5 * aCorner + .5; 8 | gl_Position = vec4(aCorner, 0, 1); 9 | } 10 | -------------------------------------------------------------------------------- /docs/shaders/display-2D.vert: -------------------------------------------------------------------------------- 1 | attribute vec2 aCorner; // {-1,1}x{-1,1} 2 | 3 | uniform vec4 uSubTexture; // in [0,1]^^4: x, y, width, height 4 | 5 | varying vec2 coords; // {0,1}x{0,1} 6 | 7 | void main(void) 8 | { 9 | coords = (vec2(.5,-.5) * aCorner + .5) * uSubTexture.zw + uSubTexture.xy; 10 | gl_Position = vec4(aCorner, 0, 1); 11 | } 12 | -------------------------------------------------------------------------------- /src/shaders/display-2D.vert: -------------------------------------------------------------------------------- 1 | attribute vec2 aCorner; // {-1,1}x{-1,1} 2 | 3 | uniform vec4 uSubTexture; // in [0,1]^^4: x, y, width, height 4 | 5 | varying vec2 coords; // {0,1}x{0,1} 6 | 7 | void main(void) 8 | { 9 | coords = (vec2(.5,-.5) * aCorner + .5) * uSubTexture.zw + uSubTexture.xy; 10 | gl_Position = vec4(aCorner, 0, 1); 11 | } 12 | -------------------------------------------------------------------------------- /src/ts/gl-utils/gl-resource.ts: -------------------------------------------------------------------------------- 1 | abstract class GLResource { 2 | private _gl: WebGLRenderingContext; 3 | 4 | constructor(gl: WebGLRenderingContext) { 5 | this._gl = gl; 6 | } 7 | 8 | public gl(): WebGLRenderingContext { 9 | return this._gl; 10 | } 11 | 12 | public abstract freeGLResources(): void; 13 | } 14 | 15 | export default GLResource; 16 | -------------------------------------------------------------------------------- /src/ts/gl-utils/utils.ts: -------------------------------------------------------------------------------- 1 | function resizeCanvas(gl: WebGLRenderingContext, hidpi: boolean = false): void { 2 | const cssPixel: number = (hidpi) ? window.devicePixelRatio : 1; 3 | 4 | const width: number = Math.floor(gl.canvas.clientWidth * cssPixel); 5 | const height: number = Math.floor(gl.canvas.clientHeight * cssPixel); 6 | if (gl.canvas.width != width || gl.canvas.height != height) { 7 | gl.canvas.width = width; 8 | gl.canvas.height = height; 9 | } 10 | } 11 | 12 | export { resizeCanvas }; -------------------------------------------------------------------------------- /src/config/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "indent": [ 9 | true, 10 | "spaces", 11 | 4 12 | ], 13 | "no-console": false, 14 | "object-literal-sort-keys": false, 15 | "variable-name": [ 16 | true, 17 | "check-format", 18 | "allow-leading-underscore" 19 | ] 20 | }, 21 | "rulesDirectory": [] 22 | } -------------------------------------------------------------------------------- /src/ts/gl-utils/viewport.ts: -------------------------------------------------------------------------------- 1 | class Viewport { 2 | public static setFullCanvas(gl: WebGLRenderingContext): void { 3 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 4 | } 5 | 6 | public lower: number; 7 | public left: number; 8 | public width: number; 9 | public height: number; 10 | 11 | constructor(left: number, lower: number, width: number, height: number) { 12 | this.left = left; 13 | this.lower = lower; 14 | this.width = width; 15 | this.height = height; 16 | } 17 | 18 | public set(gl: WebGLRenderingContext): void { 19 | gl.viewport(this.lower, this.left, this.width, this.height); 20 | } 21 | } 22 | 23 | export default Viewport; 24 | -------------------------------------------------------------------------------- /docs/images/gear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/config/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const PROJECT_DIR = path.resolve(__dirname, "..", ".."); 4 | 5 | module.exports = { 6 | devtool: "source-map", 7 | mode: "production", 8 | entry: path.join(PROJECT_DIR, "src", "ts", "main.ts"), 9 | output: { 10 | path: path.join(PROJECT_DIR, "docs", "script"), 11 | filename: "[name].min.js" 12 | }, 13 | target: ["web", "es5"], 14 | resolve: { 15 | extensions: [".ts"] 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.ts?$/, 21 | exclude: /node_modules/, 22 | use: [ 23 | { 24 | loader: "ts-loader", 25 | options: { 26 | // transpileOnly: true, 27 | compilerOptions: { 28 | rootDir: path.join(PROJECT_DIR, "src", "ts") 29 | }, 30 | configFile: path.join(PROJECT_DIR, "src", "config", 'tsconfig.json') 31 | } 32 | } 33 | ], 34 | } 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/shaders/display-2D.frag: -------------------------------------------------------------------------------- 1 | #ifdef GL_FRAGMENT_PRECISION_HIGH 2 | precision highp float; 3 | #else 4 | precision mediump float; 5 | #endif 6 | 7 | uniform float uClearFactor; // 1 - persistence 8 | uniform vec2 uGridSize; 9 | uniform sampler2D uTexture; 10 | 11 | varying vec2 coords; // {0,1}x{0,1} 12 | 13 | #include "_packing.frag" 14 | 15 | /* 0 < a < 1 => values.x 16 | * 1 < a < 2 => values.y 17 | * 2 < a < 3 => values.z 18 | * 3 < a < 4 => values.w 19 | */ 20 | float multiStep(vec4 values, float a) 21 | { 22 | return mix( 23 | mix(values.x, values.y, step(1.0, a)), 24 | mix(values.z, values.w, step(3.0, a)), 25 | step(2.0, a) 26 | ); 27 | } 28 | 29 | float getState(sampler2D texture, vec2 pos) 30 | { 31 | vec4 texel = texture2D(texture, pos); 32 | mat4 virtualBlock = unpackTexel(texel); 33 | 34 | vec2 a = mod(uGridSize * pos, 4.0); // in [0, 4]^2 35 | 36 | vec4 row = vec4( 37 | multiStep(virtualBlock[0], a.y), 38 | multiStep(virtualBlock[1], a.y), 39 | multiStep(virtualBlock[2], a.y), 40 | multiStep(virtualBlock[3], a.y) 41 | ); 42 | 43 | return multiStep(row, a.x); 44 | } 45 | 46 | void main(void) 47 | { 48 | float living = getState(uTexture, coords); 49 | gl_FragColor = vec4(living, living, living, max(uClearFactor, living)); 50 | } -------------------------------------------------------------------------------- /docs/images/resize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "game-of-life-webgl", 3 | "homepage": "https://piellardj.github.io/game-of-life-webgl", 4 | "description": "Game of Life running on GPU", 5 | "author": "Jérémie PIELLARD (https://github.com/piellardj)", 6 | "repository": "github:piellardj/game-of-life-webgl", 7 | "private": true, 8 | "license": "ISC", 9 | "scripts": { 10 | "pre-commit": "npm run rebuild && npm run lint", 11 | "build-demo-js": "npm run webpack-production", 12 | "build-page-template": "ts-node-script src/generate-page-template.ts", 13 | "build-shaders": "node src/build-shaders.js", 14 | "build": "npm run build-page-template && npm run build-shaders && npm run build-demo-js", 15 | "rebuild": "npm run clean && npm run build", 16 | "clean": "shx rm -rf docs/* **/*generated.*", 17 | "lint": "tslint -c src/config/tslint.json -p src/config/tsconfig.json", 18 | "webpack-production": "webpack --config src/config/webpack.config.js" 19 | }, 20 | "engines": { 21 | "node": ">=18.16.0" 22 | }, 23 | "devDependencies": { 24 | "@types/fs-extra": "^11.0.1", 25 | "@types/node": "^20.3.0", 26 | "fs-extra": "^11.1.1", 27 | "shx": "^0.3.4", 28 | "ts-loader": "^9.4.3", 29 | "ts-node": "^10.9.1", 30 | "tslint": "^6.1.3", 31 | "typescript": "^5.1.3", 32 | "webpack": "^5.86.0", 33 | "webpack-cli": "^5.1.4", 34 | "webpage-templates": "github:piellardj/webpage-templates" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ts/gl-utils/vbo.ts: -------------------------------------------------------------------------------- 1 | import GLResource from "./gl-resource"; 2 | 3 | class VBO extends GLResource { 4 | public static createQuad(gl: WebGLRenderingContext, minX: number, minY: number, maxX: number, maxY: number): VBO { 5 | const vert = [ 6 | minX, minY, 7 | maxX, minY, 8 | minX, maxY, 9 | maxX, maxY, 10 | ]; 11 | 12 | return new VBO(gl, new Float32Array(vert), 2, gl.FLOAT); 13 | } 14 | 15 | private id: WebGLBuffer; 16 | private size: number; 17 | private type: GLenum; 18 | private normalize: GLboolean; 19 | private stride: GLsizei; 20 | private offset: GLintptr; 21 | 22 | constructor(gl: WebGLRenderingContext, array: any, size: number, type: GLenum) { 23 | super(gl); 24 | 25 | this.id = gl.createBuffer(); 26 | gl.bindBuffer(gl.ARRAY_BUFFER, this.id); 27 | gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW); 28 | gl.bindBuffer(gl.ARRAY_BUFFER, null); 29 | 30 | this.size = size; 31 | this.type = type; 32 | this.normalize = false; 33 | this.stride = 0; 34 | this.offset = 0; 35 | } 36 | 37 | public freeGLResources(): void { 38 | this.gl().deleteBuffer(this.id); 39 | this.id = null; 40 | } 41 | 42 | public bind(location: GLuint): void { 43 | const gl = super.gl(); 44 | 45 | gl.enableVertexAttribArray(location); 46 | gl.bindBuffer(gl.ARRAY_BUFFER, this.id); 47 | gl.vertexAttribPointer(location, this.size, this.type, this.normalize, this.stride, this.offset); 48 | } 49 | } 50 | 51 | export default VBO; 52 | -------------------------------------------------------------------------------- /src/ts/gl-utils/gl-canvas.ts: -------------------------------------------------------------------------------- 1 | import "../page-interface-generated"; 2 | 3 | let gl: WebGLRenderingContext = null; 4 | 5 | /** Initializes a WebGL context */ 6 | function initGL(flags: any): boolean { 7 | function setError(message: string) { 8 | Page.Demopage.setErrorMessage("webgl-support", message); 9 | } 10 | 11 | const canvas = Page.Canvas.getCanvas(); 12 | 13 | gl = canvas.getContext("webgl", flags) as WebGLRenderingContext; 14 | if (gl == null) { 15 | gl = canvas.getContext("experimental-webgl", flags) as WebGLRenderingContext; 16 | if (gl == null) { 17 | setError("Your browser or device does not seem to support WebGL."); 18 | return false; 19 | } 20 | 21 | setError("Your browser or device only supports experimental WebGL.\n" + 22 | "The simulation may not run as expected."); 23 | } 24 | 25 | gl.disable(gl.CULL_FACE); 26 | gl.disable(gl.DEPTH_TEST); 27 | gl.disable(gl.BLEND); 28 | gl.clearColor(0.0, 0.0, 0.0, 1.0); 29 | 30 | return true; 31 | } 32 | 33 | /* Adjusts the GL canvas size to the actual canvas element size on the page */ 34 | function adjustSize(hidpi: boolean = false): void { 35 | const cssPixel: number = (hidpi) ? window.devicePixelRatio : 1; 36 | const canvas = gl.canvas as HTMLCanvasElement; 37 | 38 | const width: number = Math.floor(canvas.clientWidth * cssPixel); 39 | const height: number = Math.floor(canvas.clientHeight * cssPixel); 40 | if (canvas.width !== width || canvas.height !== height) { 41 | canvas.width = width; 42 | canvas.height = height; 43 | } 44 | } 45 | 46 | export { 47 | adjustSize, 48 | initGL, 49 | gl, 50 | }; 51 | -------------------------------------------------------------------------------- /src/shaders/_packing.frag: -------------------------------------------------------------------------------- 1 | /* Packs 4 floats in {0,1} into one float in [0, 1]*/ 2 | float pack(vec4 values) 3 | { 4 | return 0.03125 + dot(vec4(0.0625, 0.125, 0.25, 0.5), values); 5 | } 6 | 7 | /* Unpacks 4 floats in {0,1} from one float in [0, 1]*/ 8 | vec4 unpack(float v) 9 | { 10 | return vec4( 11 | step(0.0625, mod(v, 0.125)), 12 | step(0.125, mod(v, 0.25)), 13 | step(0.25, mod(v, 0.5)), 14 | step(0.5, v) 15 | ); 16 | } 17 | 18 | mat4 unpackTexel(vec4 rgba) 19 | { 20 | return mat4(unpack(rgba.r), unpack(rgba.g), unpack(rgba.b), unpack(rgba.a)); 21 | } 22 | 23 | vec4 packTexel(mat4 mat) 24 | { 25 | return vec4(pack(mat[0]), pack(mat[1]), pack(mat[2]), pack(mat[3])); 26 | } 27 | 28 | float unpackX(float v) 29 | { 30 | return step(0.0625, mod(v, 0.125)); 31 | } 32 | 33 | float unpackW(float v) 34 | { 35 | return step(0.5, v); 36 | } 37 | 38 | vec4 unpackTopRow(vec4 texel) 39 | { 40 | return vec4(unpackX(texel.r), unpackX(texel.g), unpackX(texel.b), unpackX(texel.a)); 41 | } 42 | 43 | vec4 unpackBottomRow(vec4 texel) 44 | { 45 | return vec4(unpackW(texel.r), unpackW(texel.g), unpackW(texel.b), unpackW(texel.a)); 46 | } 47 | 48 | vec4 unpackLeftColumn(vec4 texel) 49 | { 50 | return unpack(texel.r); 51 | } 52 | 53 | vec4 unpackRightColumn(vec4 texel) 54 | { 55 | return unpack(texel.a); 56 | } 57 | 58 | float unpackTopLeft(vec4 texel) 59 | { 60 | return unpackX(texel.r); 61 | } 62 | 63 | float unpackBottomLeft(vec4 texel) 64 | { 65 | return unpackW(texel.r); 66 | } 67 | 68 | float unpackTopRight(vec4 texel) 69 | { 70 | return unpackX(texel.a); 71 | } 72 | 73 | float unpackBottomRight(vec4 texel) 74 | { 75 | return unpackW(texel.a); 76 | } 77 | -------------------------------------------------------------------------------- /src/build-shaders.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const fse = require("fs-extra"); 3 | const path = require("path"); 4 | 5 | const SRC_DIR = path.resolve(__dirname, "shaders"); 6 | const DST_DIR = path.resolve(__dirname, "..", "docs", "shaders"); 7 | 8 | const MAX_INCLUDE_DEPTH = 10; 9 | 10 | /** 11 | * Resolves every #include "XXX" directive in shader source files. 12 | * Every path is resolved from the SRC_DIR directory. 13 | * Cyclic includes are not supported. 14 | * @param {string} filepath 15 | * @return {string} 16 | */ 17 | function resolveIncludes(filepath) { 18 | let includeDepth = 0; 19 | 20 | let processedStr = fs.readFileSync(filepath).toString(); 21 | let includesLeft; 22 | do { 23 | includesLeft = 0; 24 | 25 | processedStr = processedStr.replace(/^\s*#include\s*\"(.*)\"\s*$/mg, 26 | (match, p1) => { 27 | includesLeft++; 28 | const fullpath = path.join(SRC_DIR, p1); 29 | return fs.readFileSync(fullpath).toString(); 30 | }); 31 | 32 | includeDepth++; 33 | if (includeDepth === MAX_INCLUDE_DEPTH) { 34 | console.error("Error while preprocessing '" + filepath + "': too much #include depth."); 35 | } 36 | } while (includesLeft > 0 && includeDepth < MAX_INCLUDE_DEPTH); 37 | 38 | return processedStr; 39 | } 40 | 41 | 42 | fse.ensureDirSync(DST_DIR); 43 | 44 | fs.readdirSync(SRC_DIR).forEach(file => { 45 | if (!file.startsWith("_")) { 46 | const srcFilepath = path.join(SRC_DIR, file); 47 | const dstFilepath = path.join(DST_DIR, file); 48 | 49 | const resolvedStr = resolveIncludes(srcFilepath); 50 | fs.writeFileSync(dstFilepath, resolvedStr); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /src/ts/gl-utils/fbo.ts: -------------------------------------------------------------------------------- 1 | import GLResource from "./gl-resource"; 2 | import Viewport from "./viewport"; 3 | 4 | class FBO extends GLResource { 5 | public static bindDefault(gl: WebGLRenderingContext, viewport: Viewport = null): void { 6 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 7 | 8 | if (viewport === null) { 9 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 10 | } else { 11 | gl.viewport(viewport.left, viewport.lower, viewport.width, viewport.height); 12 | } 13 | 14 | } 15 | 16 | public width: number; 17 | public height: number; 18 | private id: WebGLFramebuffer; 19 | 20 | constructor(gl: WebGLRenderingContext, width: number, height: number) { 21 | super(gl); 22 | 23 | this.id = gl.createFramebuffer(); 24 | this.width = width; 25 | this.height = height; 26 | } 27 | 28 | public bind(colorBuffers: WebGLTexture[], depthBuffer: WebGLRenderbuffer = null): void { 29 | const gl = super.gl(); 30 | 31 | gl.bindFramebuffer(gl.FRAMEBUFFER, this.id); 32 | gl.viewport(0, 0, this.width, this.height); 33 | 34 | for (let i = 0; i < colorBuffers.length; ++i) { 35 | gl.framebufferTexture2D( 36 | gl.FRAMEBUFFER, gl["COLOR_ATTACHMENT" + i], gl.TEXTURE_2D, colorBuffers[i], 0); 37 | } 38 | 39 | if (depthBuffer) { 40 | gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); 41 | gl.framebufferRenderbuffer( 42 | gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer); 43 | } 44 | } 45 | 46 | public freeGLResources(): void { 47 | super.gl().deleteFramebuffer(this.id); 48 | this.id = null; 49 | } 50 | } 51 | 52 | export default FBO; 53 | -------------------------------------------------------------------------------- /src/ts/gl-utils/shader-sources.ts: -------------------------------------------------------------------------------- 1 | type LoadCallback = (success: boolean) => void; 2 | 3 | interface ICachedSource { 4 | text: string; 5 | pending: boolean; 6 | failed: boolean; 7 | callbacks: LoadCallback[]; 8 | } 9 | 10 | const cachedSources: { [id: string]: ICachedSource } = {}; 11 | 12 | /* Fetches asynchronously the shader source from server and stores it in cache. */ 13 | function loadSource(filename: string, callback: LoadCallback) { 14 | function callAndClearCallbacks(cached: ICachedSource) { 15 | for (const cachedCallback of cached.callbacks) { 16 | cachedCallback(!cached.failed); 17 | } 18 | 19 | cached.callbacks = []; 20 | } 21 | 22 | if (typeof cachedSources[filename] === "undefined") { 23 | cachedSources[filename] = { 24 | callbacks: [callback], 25 | failed: false, 26 | pending: true, 27 | text: null, 28 | }; 29 | const cached = cachedSources[filename]; 30 | 31 | const xhr = new XMLHttpRequest(); 32 | xhr.open("GET", "./shaders/" + filename, true); 33 | xhr.onload = () => { 34 | if (xhr.readyState === 4) { 35 | cached.pending = false; 36 | 37 | if (xhr.status === 200) { 38 | cached.text = xhr.responseText; 39 | cached.failed = false; 40 | } else { 41 | console.error("Cannot load '" + filename + "' shader source: " + xhr.statusText); 42 | cached.failed = true; 43 | } 44 | 45 | callAndClearCallbacks(cached); 46 | } 47 | }; 48 | xhr.onerror = () => { 49 | console.error("Cannot load '" + filename + "' shader source: " + xhr.statusText); 50 | cached.pending = false; 51 | cached.failed = true; 52 | callAndClearCallbacks(cached); 53 | }; 54 | 55 | xhr.send(null); 56 | } else { 57 | const cached = cachedSources[filename]; 58 | 59 | if (cached.pending === true) { 60 | cached.callbacks.push(callback); 61 | } else { 62 | cached.callbacks = [callback]; 63 | callAndClearCallbacks(cached); 64 | } 65 | } 66 | } 67 | 68 | function getSource(filename: string): string { 69 | return cachedSources[filename].text; 70 | } 71 | 72 | export { 73 | getSource, 74 | loadSource, 75 | }; 76 | -------------------------------------------------------------------------------- /src/ts/main.ts: -------------------------------------------------------------------------------- 1 | import FBO from "./gl-utils/fbo"; 2 | import * as GlCanvas from "./gl-utils/gl-canvas"; 3 | import { gl } from "./gl-utils/gl-canvas"; 4 | import Viewport from "./gl-utils/viewport"; 5 | 6 | import Automaton2D from "./automaton-2D"; 7 | import Parameters from "./parameters"; 8 | 9 | import "./page-interface-generated"; 10 | 11 | function main() { 12 | const glParams = { 13 | alpha: false, 14 | preserveDrawingBuffer: true, 15 | }; 16 | if (!GlCanvas.initGL(glParams)) { 17 | return; 18 | } 19 | 20 | gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); 21 | 22 | Page.Canvas.showLoader(true); 23 | 24 | let needToAdjustSize = true; 25 | Page.Canvas.Observers.canvasResize.push(() => needToAdjustSize = true); 26 | 27 | Parameters.autorun = true; 28 | Parameters.persistence = 0; 29 | 30 | const automaton = new Automaton2D(); 31 | 32 | let lastIteration = automaton.iteration; 33 | function updateIterationPerSecIndicator() { 34 | Page.Canvas.setIndicatorText("iterations-per-sec", "" + (automaton.iteration - lastIteration)); 35 | lastIteration = automaton.iteration; 36 | } 37 | window.setInterval(updateIterationPerSecIndicator, 1000); 38 | 39 | function updateIterationIndicator() { 40 | Page.Canvas.setIndicatorText("iteration", "" + automaton.iteration); 41 | } 42 | window.setInterval(updateIterationIndicator, 50); 43 | 44 | let forceUpdate = false; 45 | Parameters.nextStepObservers.push(() => forceUpdate = true); 46 | 47 | let firstDraw = true; 48 | let lastUpdate = 0; 49 | function mainLoop(time: number) { 50 | const update = automaton.needToUpdate || forceUpdate || 51 | (Parameters.autorun && (time - lastUpdate > Parameters.updateWaitTime)); 52 | if (update) { 53 | lastUpdate = time; 54 | automaton.update(); 55 | forceUpdate = false; 56 | } 57 | 58 | if (update || automaton.needToRedraw) { 59 | FBO.bindDefault(gl); 60 | 61 | if (needToAdjustSize) { 62 | GlCanvas.adjustSize(); 63 | needToAdjustSize = false; 64 | } 65 | 66 | Viewport.setFullCanvas(gl); 67 | 68 | automaton.draw(); 69 | 70 | if (firstDraw) { 71 | firstDraw = false; 72 | 73 | Page.Canvas.showLoader(false); 74 | } 75 | } 76 | 77 | requestAnimationFrame(mainLoop); 78 | } 79 | 80 | requestAnimationFrame(mainLoop); 81 | } 82 | 83 | main(); 84 | -------------------------------------------------------------------------------- /src/ts/gl-utils/matrix/vec3.ts: -------------------------------------------------------------------------------- 1 | const EPSILON = 0.0001; 2 | 3 | class Vec3 { 4 | public static dotProduct(v1: Vec3, v2: Vec3): number { 5 | return v1._val[0] * v2._val[0] + 6 | v1._val[1] * v2._val[1] + 7 | v1._val[2] * v2._val[2]; 8 | } 9 | 10 | public static crossProduct(v1: Vec3, v2: Vec3): Vec3 { 11 | return new Vec3( 12 | v1._val[1] * v2._val[2] - v1._val[2] * v2._val[1], 13 | v1._val[2] * v2._val[0] - v1._val[0] * v2._val[2], 14 | v1._val[0] * v2._val[1] - v1._val[1] * v2._val[0]); 15 | } 16 | 17 | public static substract(v1: Vec3, v2: Vec3): Vec3 { 18 | return new Vec3( 19 | v1._val[0] - v2._val[0], 20 | v1._val[1] - v2._val[1], 21 | v1._val[2] - v2._val[2]); 22 | } 23 | 24 | private _val: Float32Array; 25 | 26 | constructor(x: number, y: number, z: number) { 27 | this._val = new Float32Array(3); 28 | this._val[0] = x; 29 | this._val[1] = y; 30 | this._val[2] = z; 31 | } 32 | 33 | public get x(): number { 34 | return this._val[0]; 35 | } 36 | 37 | public get y(): number { 38 | return this._val[1]; 39 | } 40 | 41 | public get z(): number { 42 | return this._val[2]; 43 | } 44 | 45 | public equals(other: Vec3): boolean { 46 | return Math.abs(this._val[0] - other._val[0]) < EPSILON && 47 | Math.abs(this._val[1] - other._val[1]) < EPSILON && 48 | Math.abs(this._val[2] - other._val[2]) < EPSILON; 49 | } 50 | 51 | public divideByScalar(scalar: number): void { 52 | this._val[0] /= scalar; 53 | this._val[1] /= scalar; 54 | this._val[2] /= scalar; 55 | } 56 | 57 | public substract(other: Vec3): void { 58 | this._val[0] -= other._val[0]; 59 | this._val[1] -= other._val[1]; 60 | this._val[2] -= other._val[2]; 61 | } 62 | 63 | public get length(): number { 64 | const norm = this._val[0] * this._val[0] + 65 | this._val[1] * this._val[1] + 66 | this._val[2] * this._val[2]; 67 | return Math.sqrt(norm); 68 | } 69 | 70 | /* Return false if vector cannot be normalized because it's null. */ 71 | public normalize(): boolean { 72 | if (Math.abs(this._val[0]) < EPSILON && 73 | Math.abs(this._val[1]) < EPSILON && 74 | Math.abs(this._val[2]) < EPSILON) { 75 | this._val[0] = 0; 76 | this._val[1] = 0; 77 | this._val[2] = 0; 78 | return false; 79 | } 80 | 81 | this.divideByScalar(this.length); 82 | 83 | return true; 84 | } 85 | } 86 | 87 | export { Vec3 }; 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # game-of-life-webgl 2 | 3 | ## Description 4 | This project is a simple simulation of Conway's Game of Life, running on GPU. 5 | 6 | The rules can be changed to see how the world evolves. You can use the mouse to zoom in and explore the world. 7 | 8 | See it live [here](https://piellardj.github.io/game-of-life-webgl/?page%3Acanvas%3Afullscreen=true&page%3Acanvas%3Asidepane=true). 9 | 10 | [![Donate](https://raw.githubusercontent.com/piellardj/piellardj.github.io/master/images/readme/donate-paypal.svg)](https://www.paypal.com/donate/?hosted_button_id=AF7H7GEJTL95E) 11 | 12 | ![Illustration](src/resources/README/illustration.png) 13 | 14 | ## Details 15 | 16 | ### Texel packing 17 | 18 | In order for the GPU to compute the world's evolution, the grid data must be stored in a texture so that it can be accessed from the fragment shader. 19 | 20 | A first approach would be to store each cell in a pixel of the texture. However the cell state is binary and it would be a waste to use a whole 32 bits texel to store it. 21 | 22 | Unfortunately, compact texture formats are only available in WebGL 2, so I chose to use a RGBA texture and to pack a 4x4 binary grid in it: each 8 bit channel stores the state of 4 vertical cells. The packing/unpacking is handled in the fragment shaders and uses floats since WebGL 1 does not allow integer textures or bitwise operators. 23 | 24 | ![Each texel contains a 4x4 grid of cells](src/resources/README/cells_packing.png) 25 | 26 | Packing a 4x4 cells grid in each texel allows for faster processing. The texture stored in VRAM is 16 times smaller than the visible grid. Updating a texel still takes 9 texture fetches (8 neighbours + previous version of itself) but it updates 16 grid cells at once, so we only need 0.56 texture fetches per grid cell (instead of 9 for a non-compressed storage). Of course by using more appropriate formats, we could achieve a much better ratio but it was enough for this project. 27 | 28 | Packing / unpacking the cells adds a bit more computing complexity to the shaders, however it negligible compared to the texture fetches it avoids. Moreover, I try to unpack as few cells as needed: in order to udpate a texel, I only unpack one mat4, four vec4 and four floats. 29 | 30 | ![Each texel contains a 4x4 grid of cells](src/resources/README/update.png) 31 | 32 | ### Rules 33 | 34 | The update shader is partially rewritten and recompiled every time the rules change. The rules are written using exclusively the step function. The standard Game of Life rules require four step calls to update a cell (and the compiler will mutualize two of them so only three calls are actually used). 35 | 36 | ### Persistence 37 | 38 | Persistence is a visual effect making a dead cell look like it is slowly turning off. It helps see motion (especially gliders). 39 | 40 | Since grid data is compressed into the texture, there is no room to store for how long a cell has been dead. So to add persistence to the rendering, I simply avoid clearing the drawing buffer each frame (using the 'preserveDrawingBuffer' option) and I use blending to slowly dim dead cells. Unfortunately because of this technique, when zooming or moving in the world, the canvas must be cleared to avoid artifacts, and the persistence is lost. -------------------------------------------------------------------------------- /docs/shaders/display-2D.frag: -------------------------------------------------------------------------------- 1 | #ifdef GL_FRAGMENT_PRECISION_HIGH 2 | precision highp float; 3 | #else 4 | precision mediump float; 5 | #endif 6 | 7 | uniform float uClearFactor; // 1 - persistence 8 | uniform vec2 uGridSize; 9 | uniform sampler2D uTexture; 10 | 11 | varying vec2 coords; // {0,1}x{0,1} /* Packs 4 floats in {0,1} into one float in [0, 1]*/ 12 | float pack(vec4 values) 13 | { 14 | return 0.03125 + dot(vec4(0.0625, 0.125, 0.25, 0.5), values); 15 | } 16 | 17 | /* Unpacks 4 floats in {0,1} from one float in [0, 1]*/ 18 | vec4 unpack(float v) 19 | { 20 | return vec4( 21 | step(0.0625, mod(v, 0.125)), 22 | step(0.125, mod(v, 0.25)), 23 | step(0.25, mod(v, 0.5)), 24 | step(0.5, v) 25 | ); 26 | } 27 | 28 | mat4 unpackTexel(vec4 rgba) 29 | { 30 | return mat4(unpack(rgba.r), unpack(rgba.g), unpack(rgba.b), unpack(rgba.a)); 31 | } 32 | 33 | vec4 packTexel(mat4 mat) 34 | { 35 | return vec4(pack(mat[0]), pack(mat[1]), pack(mat[2]), pack(mat[3])); 36 | } 37 | 38 | float unpackX(float v) 39 | { 40 | return step(0.0625, mod(v, 0.125)); 41 | } 42 | 43 | float unpackW(float v) 44 | { 45 | return step(0.5, v); 46 | } 47 | 48 | vec4 unpackTopRow(vec4 texel) 49 | { 50 | return vec4(unpackX(texel.r), unpackX(texel.g), unpackX(texel.b), unpackX(texel.a)); 51 | } 52 | 53 | vec4 unpackBottomRow(vec4 texel) 54 | { 55 | return vec4(unpackW(texel.r), unpackW(texel.g), unpackW(texel.b), unpackW(texel.a)); 56 | } 57 | 58 | vec4 unpackLeftColumn(vec4 texel) 59 | { 60 | return unpack(texel.r); 61 | } 62 | 63 | vec4 unpackRightColumn(vec4 texel) 64 | { 65 | return unpack(texel.a); 66 | } 67 | 68 | float unpackTopLeft(vec4 texel) 69 | { 70 | return unpackX(texel.r); 71 | } 72 | 73 | float unpackBottomLeft(vec4 texel) 74 | { 75 | return unpackW(texel.r); 76 | } 77 | 78 | float unpackTopRight(vec4 texel) 79 | { 80 | return unpackX(texel.a); 81 | } 82 | 83 | float unpackBottomRight(vec4 texel) 84 | { 85 | return unpackW(texel.a); 86 | } 87 | 88 | /* 0 < a < 1 => values.x 89 | * 1 < a < 2 => values.y 90 | * 2 < a < 3 => values.z 91 | * 3 < a < 4 => values.w 92 | */ 93 | float multiStep(vec4 values, float a) 94 | { 95 | return mix( 96 | mix(values.x, values.y, step(1.0, a)), 97 | mix(values.z, values.w, step(3.0, a)), 98 | step(2.0, a) 99 | ); 100 | } 101 | 102 | float getState(sampler2D texture, vec2 pos) 103 | { 104 | vec4 texel = texture2D(texture, pos); 105 | mat4 virtualBlock = unpackTexel(texel); 106 | 107 | vec2 a = mod(uGridSize * pos, 4.0); // in [0, 4]^2 108 | 109 | vec4 row = vec4( 110 | multiStep(virtualBlock[0], a.y), 111 | multiStep(virtualBlock[1], a.y), 112 | multiStep(virtualBlock[2], a.y), 113 | multiStep(virtualBlock[3], a.y) 114 | ); 115 | 116 | return multiStep(row, a.x); 117 | } 118 | 119 | void main(void) 120 | { 121 | float living = getState(uTexture, coords); 122 | gl_FragColor = vec4(living, living, living, max(uClearFactor, living)); 123 | } -------------------------------------------------------------------------------- /src/ts/gl-utils/shader-manager.ts: -------------------------------------------------------------------------------- 1 | import { gl } from "./gl-canvas"; 2 | import Shader from "./shader"; 3 | import * as ShaderSources from "./shader-sources"; 4 | 5 | type RegisterCallback = (success: boolean, shader: Shader | null) => void; 6 | 7 | interface IShaderInfos { 8 | fragmentFilename: string; 9 | vertexFilename: string; 10 | injected: { [id: string]: string }; 11 | } 12 | 13 | interface ICachedShader { 14 | shader: Shader; 15 | infos: IShaderInfos; 16 | pending: boolean; 17 | failed: boolean; 18 | callbacks: RegisterCallback[]; 19 | } 20 | 21 | const cachedShaders: { [id: string]: ICachedShader} = {}; 22 | 23 | function getShader(name: string): Shader | null { 24 | return cachedShaders[name].shader; 25 | } 26 | 27 | type BuildCallback = (builtShader: Shader | null) => void; 28 | 29 | function buildShader(infos: IShaderInfos, callback: BuildCallback) { 30 | let sourcesPending = 2; 31 | let sourcesFailed = 0; 32 | 33 | function loadedSource(success: boolean) { 34 | function processSource(source: string): string { 35 | return source.replace(/#INJECT\((.*)\)/mg, (match, name) => { 36 | if (infos.injected[name]) { 37 | return infos.injected[name]; 38 | } 39 | return match; 40 | }); 41 | } 42 | 43 | sourcesPending--; 44 | if (!success) { 45 | sourcesFailed++; 46 | } 47 | 48 | if (sourcesPending === 0) { 49 | let shader = null; 50 | 51 | if (sourcesFailed === 0) { 52 | const vert = ShaderSources.getSource(infos.vertexFilename); 53 | const frag = ShaderSources.getSource(infos.fragmentFilename); 54 | 55 | const processedVert = processSource(vert); 56 | const processedFrag = processSource(frag); 57 | 58 | shader = new Shader(gl, processedVert, processedFrag); 59 | } 60 | 61 | callback(shader); 62 | } 63 | } 64 | 65 | ShaderSources.loadSource(infos.vertexFilename, loadedSource); 66 | ShaderSources.loadSource(infos.fragmentFilename, loadedSource); 67 | } 68 | 69 | function registerShader(name: string, infos: IShaderInfos, callback: RegisterCallback): void { 70 | function callAndClearCallbacks(cached: ICachedShader) { 71 | for (const cachedCallback of cached.callbacks) { 72 | cachedCallback(!cached.failed, cached.shader); 73 | } 74 | 75 | cached.callbacks = []; 76 | } 77 | 78 | if (typeof cachedShaders[name] === "undefined") { 79 | cachedShaders[name] = { 80 | callbacks: [callback], 81 | failed: false, 82 | infos, 83 | pending: true, 84 | shader: null, 85 | }; 86 | const cached = cachedShaders[name]; 87 | 88 | buildShader(infos, (builtShader) => { 89 | cached.pending = false; 90 | cached.failed = builtShader === null; 91 | cached.shader = builtShader; 92 | 93 | callAndClearCallbacks(cached); 94 | }); 95 | } else { 96 | const cached = cachedShaders[name]; 97 | 98 | if (cached.pending === true) { 99 | cached.callbacks.push(callback); 100 | } else { 101 | callAndClearCallbacks(cached); 102 | } 103 | } 104 | } 105 | 106 | function deleteShader(name: string) { 107 | if (cachedShaders[name]) { 108 | if (cachedShaders[name].shader) { 109 | cachedShaders[name].shader.freeGLResources(); 110 | } 111 | delete cachedShaders[name]; 112 | } 113 | } 114 | 115 | export { 116 | buildShader, 117 | getShader, 118 | IShaderInfos, 119 | registerShader, 120 | deleteShader, 121 | }; 122 | -------------------------------------------------------------------------------- /src/shaders/update-2D.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform sampler2D uPrevious; 4 | uniform vec2 uPhysicalCellSize; 5 | 6 | varying vec2 coords; // {0,1}x{0,1} 7 | 8 | #include "_packing.frag" 9 | 10 | float evolveCell(float currentState, float N) 11 | { 12 | #INJECT(rules) 13 | return clamp(currentState, 0.0, 1.0); 14 | } 15 | 16 | vec4 evolveColumn(vec4 current, vec4 neighbours) 17 | { 18 | return vec4( 19 | evolveCell(current.x, neighbours.x), 20 | evolveCell(current.y, neighbours.y), 21 | evolveCell(current.z, neighbours.z), 22 | evolveCell(current.w, neighbours.w) 23 | ); 24 | } 25 | 26 | mat4 evolveBlock(mat4 current, mat4 neighbours) 27 | { 28 | return mat4( 29 | evolveColumn(current[0], neighbours[0]), 30 | evolveColumn(current[1], neighbours[1]), 31 | evolveColumn(current[2], neighbours[2]), 32 | evolveColumn(current[3], neighbours[3]) 33 | ); 34 | } 35 | 36 | void main(void) 37 | { 38 | vec4 topLeftTexel = texture2D(uPrevious, coords + vec2(-1,-1) * uPhysicalCellSize); 39 | vec4 leftTexel = texture2D(uPrevious, coords + vec2(-1,+0) * uPhysicalCellSize); 40 | vec4 bottomLeftTexel = texture2D(uPrevious, coords + vec2(-1,+1) * uPhysicalCellSize); 41 | vec4 topTexel = texture2D(uPrevious, coords + vec2(+0,-1) * uPhysicalCellSize); 42 | vec4 middleTexel = texture2D(uPrevious, coords); 43 | vec4 bottomTexel = texture2D(uPrevious, coords + vec2(+0,+1) * uPhysicalCellSize); 44 | vec4 topRightTexel = texture2D(uPrevious, coords + vec2(+1,-1) * uPhysicalCellSize); 45 | vec4 rightTexel = texture2D(uPrevious, coords + vec2(+1,+0) * uPhysicalCellSize); 46 | vec4 bottomRightTexel = texture2D(uPrevious, coords + vec2(+1,+1) * uPhysicalCellSize); 47 | 48 | vec4 top = unpackBottomRow(topTexel); 49 | vec4 bottom = unpackTopRow(bottomTexel); 50 | vec4 left = unpackRightColumn(leftTexel); 51 | vec4 right = unpackLeftColumn(rightTexel); 52 | float topLeft = unpackBottomRight(topLeftTexel); 53 | float topRight = unpackBottomLeft(topRightTexel); 54 | float bottomLeft = unpackTopRight(bottomLeftTexel); 55 | float bottomRight = unpackTopLeft(bottomRightTexel); 56 | 57 | mat4 current = unpackTexel(middleTexel); 58 | 59 | mat4 neighbours = mat4( 60 | /* First column */ 61 | topLeft + top.x + top.y + left.x + current[1][0] + left.y + current[0][1] + current[1][1], 62 | left.x + current[0][0] + current[1][0] + left.y + current[1][1] + left.z + current[0][2] + current[1][2], 63 | left.y + current[0][1] + current[1][1] + left.z + current[1][2] + left.w + current[0][3] + current[1][3], 64 | left.z + current[0][2] + current[1][2] + left.w + current[1][3] + bottomLeft + bottom.x + bottom.y, 65 | 66 | /* Second column */ 67 | top.x + top.y + top.z + current[0][0] + current[2][0] + current[0][1] + current[1][1] + current[2][1], 68 | current[0][0] + current[1][0] + current[2][0] + current[0][1] + current[2][1] + current[0][2] + current[1][2] + current[2][2], 69 | current[0][1] + current[1][1] + current[2][1] + current[0][2] + current[2][2] + current[0][3] + current[1][3] + current[2][3], 70 | current[0][2] + current[1][2] + current[2][2] + current[0][3] + current[2][3] + bottom.x + bottom.y + bottom.z, 71 | 72 | /* Third column */ 73 | top.y + top.z + top.w + current[1][0] + current[3][0] + current[1][1] + current[2][1] + current[3][1], 74 | current[1][0] + current[2][0] + current[3][0] + current[1][1] + current[3][1] + current[1][2] + current[2][2] + current[3][2], 75 | current[1][1] + current[2][1] + current[3][1] + current[1][2] + current[3][2] + current[1][3] + current[2][3] + current[3][3], 76 | current[1][2] + current[2][2] + current[3][2] + current[1][3] + current[3][3] + bottom.y + bottom.z + bottom.w, 77 | 78 | /* Fourth column */ 79 | top.z + top.w + topRight + current[2][0] + right.x + current[2][1] + current[3][1] + right.y, 80 | current[2][0] + current[3][0] + right.x + current[2][1] + right.y + current[2][2] + current[3][2] + right.z, 81 | current[2][1] + current[3][1] + right.y + current[2][2] + right.z + current[2][3] + current[3][3] + right.w, 82 | current[2][2] + current[3][2] + right.z + current[2][3] + right.w + bottom.z + bottom.w + bottomRight 83 | ); 84 | 85 | gl_FragColor = packTexel(evolveBlock(current, neighbours)); 86 | } -------------------------------------------------------------------------------- /docs/readme/css/page.css: -------------------------------------------------------------------------------- 1 | a{color:#009688;color:var(--var-color-control-accent, #009688);font-weight:bold;text-decoration:none;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;border-width:0 0 2px;border-style:solid;border-color:rgba(0,0,0,0)}a:focus,a:hover{border-color:#009688;border-color:var(--var-color-control-accent, #009688)}:root{--color-code: #e0e0e0}@media(prefers-color-scheme: dark){:root{--color-code: #343434}}body{max-width:100%}.contents{line-height:1.5em;max-width:900px;margin:auto;padding:16px 32px;border-radius:8px;border:1px solid #c9c9c9;border:var(--var-color-block-border, 1px solid #c9c9c9);background:#eeeeee;background:var(--var-color-block-background, #eeeeee)}h1{text-align:center;margin-bottom:1em}pre{overflow-x:auto;background:var(--color-code);padding:4px 16px;border-radius:8px;line-height:1.45}pre::-webkit-scrollbar{width:16px}pre::-webkit-scrollbar-track{background-color:rgba(0,0,0,0)}pre::-webkit-scrollbar-thumb{border-width:6px;border-style:solid;border-radius:8px;border-color:var(--color-code);background-color:#a5a5a5;background-color:var(--var-color-scrollbar, #a5a5a5)}pre::-webkit-scrollbar-thumb:focus,pre::-webkit-scrollbar-thumb:hover{background-color:#b2b2b2;background-color:var(--var-color-scrollbar-hover, #b2b2b2)}pre::-webkit-scrollbar-thumb:active{background-color:#959595;background-color:var(--var-color-scrollbar-active, #959595)}pre:hover::-webkit-scrollbar-thumb{border-width:5px}pre code{padding:0}code{background:var(--color-code);padding:2px 4px;border-radius:3px;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;line-height:1.5em}video,img{max-width:100%;border-radius:8px} 2 | .logo{display:block;position:relative;width:64px;height:64px;margin:8px auto 16px;border-radius:50%;user-select:none;box-sizing:border-box}.logo,.logo:hover,.logo:focus,.logo:active{border-width:1px;border-style:solid;border-color:#009688;border-color:var(--var-color-control-accent, #009688)}.logo::before,.logo svg.logo-icon,.logo::after{position:absolute;top:-1px;left:-1px;width:64px;height:64px;border-radius:50%;pointer-events:none}.logo svg.logo-icon{stroke:#009688;stroke:var(--var-color-control-accent, #009688);fill:#009688;fill:var(--var-color-control-accent, #009688)}.logo::before{content:"";transform:scale(0);-webkit-transform:scale(0);-ms-transform:scale(0);transition:.1s ease;-webkit-transition:.1s ease}.logo.logo-animate-fill .logo::before{content:"";transform:scale(0);-webkit-transform:scale(0);-ms-transform:scale(0);transition:.1s ease;-webkit-transition:.1s ease}.logo:hover::before{transform:scale(1);-webkit-transform:scale(1);-ms-transform:scale(1)}.logo.logo-animate-fill{background:#eeeeee;background:var(--var-color-block-background, #eeeeee)}.logo.logo-animate-fill::before{background:#009688;background:var(--var-color-control-accent, #009688)}.logo.logo-animate-fill:hover svg.logo-icon{fill:#fff;stroke:#fff}.logo.logo-animate-empty{background:#009688;background:var(--var-color-control-accent, #009688)}.logo.logo-animate-empty::before{top:0;left:0;width:62px;height:62px;background:#eeeeee;background:var(--var-color-block-background, #eeeeee)} 3 | :root{--var-color-theme:white;--var-color-page-background:#ededed;--var-page-background-image:url("../images/noise-light.png");--var-color-block-background:#eeeeee;--var-color-block-border:1px solid #c9c9c9;--var-color-title:#535353;--var-color-text:#676767;--var-color-block-actionitem:#5e5e5e;--var-color-block-actionitem-hover:#7e7e7e;--var-color-block-actionitem-active:#535353;--var-color-scrollbar:#a5a5a5;--var-color-scrollbar-hover:#b2b2b2;--var-color-scrollbar-active:#959595;--var-color-control-neutral:#c9c9c9;--var-color-control-accent:#009688;--var-color-control-accent-hover:#26a69a;--var-color-control-accent-active:#00897b}@media(prefers-color-scheme: dark){:root{--var-color-theme:black;--var-color-page-background:#232323;--var-page-background-image:url("../images/noise-dark.png");--var-color-block-background:#202020;--var-color-block-border:1px solid #535353;--var-color-title:#eeeeee;--var-color-text:#dbdbdb;--var-color-block-actionitem:#dbdbdb;--var-color-block-actionitem-hover:#eeeeee;--var-color-block-actionitem-active:#c9c9c9;--var-color-scrollbar:#7e7e7e;--var-color-scrollbar-hover:#959595;--var-color-scrollbar-active:#676767;--var-color-control-neutral:#5e5e5e;--var-color-control-accent:#26a69a;--var-color-control-accent-hover:#4db6ac;--var-color-control-accent-active:#009688}}:root{color-scheme:light dark}html{display:flex;min-height:100%;font-family:Arial,Helvetica,sans-serif}body{display:flex;flex:1;flex-direction:column;min-height:100vh;margin:0px;background-attachment:fixed;background:#ededed;background:var(--var-color-page-background, #ededed);background-image:url("../images/noise-light.png");background-image:var(--var-page-background-image, url("../images/noise-light.png"));color:#676767;color:var(--var-color-text, #676767)}main{display:block;flex-grow:1;padding-bottom:32px}h1,h2,h3{color:#535353;color:var(--var-color-title, #535353)} 4 | .badge{width:32px;height:32px;margin:8px 12px;border:none}.badge>svg{width:32px;height:32px}.badge,.badge:hover,.badge:focus,.badge:active{border:none}.badge svg{fill:#5e5e5e;fill:var(--var-color-block-actionitem, #5e5e5e)}.badge svg:focus,.badge svg:hover{fill:#7e7e7e;fill:var(--var-color-block-actionitem-hover, #7e7e7e)}.badge svg:active{fill:#535353;fill:var(--var-color-block-actionitem-active, #535353)}.badge-shelf{display:flex;flex-flow:row;justify-content:center}footer{align-items:center;padding:8px;text-align:center;border-top:1px solid #c9c9c9;border-top:var(--var-color-block-border, 1px solid #c9c9c9);background:#eeeeee;background:var(--var-color-block-background, #eeeeee)} 5 | -------------------------------------------------------------------------------- /src/config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "../ts/main.ts" 4 | ], 5 | "compilerOptions": { 6 | /* Basic Options */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "../../tmp/script/skybox-editor.js", /* Concatenate and emit output to single file. */ 17 | "outDir": "../../tmp/script", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | "strictNullChecks": false, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | /* Additional Checks */ 35 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 37 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | "typeRoots": [ 45 | "../@types", 46 | "../../node_modules/@types" 47 | ], /* List of folders to include type definitions from. */ 48 | // "types": [], /* Type declaration files to be included in compilation. */ 49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 50 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | /* Source Map Options */ 53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 57 | /* Experimental Options */ 58 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 59 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 60 | } 61 | } -------------------------------------------------------------------------------- /docs/shaders/update-2D.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform sampler2D uPrevious; 4 | uniform vec2 uPhysicalCellSize; 5 | 6 | varying vec2 coords; // {0,1}x{0,1} /* Packs 4 floats in {0,1} into one float in [0, 1]*/ 7 | float pack(vec4 values) 8 | { 9 | return 0.03125 + dot(vec4(0.0625, 0.125, 0.25, 0.5), values); 10 | } 11 | 12 | /* Unpacks 4 floats in {0,1} from one float in [0, 1]*/ 13 | vec4 unpack(float v) 14 | { 15 | return vec4( 16 | step(0.0625, mod(v, 0.125)), 17 | step(0.125, mod(v, 0.25)), 18 | step(0.25, mod(v, 0.5)), 19 | step(0.5, v) 20 | ); 21 | } 22 | 23 | mat4 unpackTexel(vec4 rgba) 24 | { 25 | return mat4(unpack(rgba.r), unpack(rgba.g), unpack(rgba.b), unpack(rgba.a)); 26 | } 27 | 28 | vec4 packTexel(mat4 mat) 29 | { 30 | return vec4(pack(mat[0]), pack(mat[1]), pack(mat[2]), pack(mat[3])); 31 | } 32 | 33 | float unpackX(float v) 34 | { 35 | return step(0.0625, mod(v, 0.125)); 36 | } 37 | 38 | float unpackW(float v) 39 | { 40 | return step(0.5, v); 41 | } 42 | 43 | vec4 unpackTopRow(vec4 texel) 44 | { 45 | return vec4(unpackX(texel.r), unpackX(texel.g), unpackX(texel.b), unpackX(texel.a)); 46 | } 47 | 48 | vec4 unpackBottomRow(vec4 texel) 49 | { 50 | return vec4(unpackW(texel.r), unpackW(texel.g), unpackW(texel.b), unpackW(texel.a)); 51 | } 52 | 53 | vec4 unpackLeftColumn(vec4 texel) 54 | { 55 | return unpack(texel.r); 56 | } 57 | 58 | vec4 unpackRightColumn(vec4 texel) 59 | { 60 | return unpack(texel.a); 61 | } 62 | 63 | float unpackTopLeft(vec4 texel) 64 | { 65 | return unpackX(texel.r); 66 | } 67 | 68 | float unpackBottomLeft(vec4 texel) 69 | { 70 | return unpackW(texel.r); 71 | } 72 | 73 | float unpackTopRight(vec4 texel) 74 | { 75 | return unpackX(texel.a); 76 | } 77 | 78 | float unpackBottomRight(vec4 texel) 79 | { 80 | return unpackW(texel.a); 81 | } 82 | 83 | float evolveCell(float currentState, float N) 84 | { 85 | #INJECT(rules) 86 | return clamp(currentState, 0.0, 1.0); 87 | } 88 | 89 | vec4 evolveColumn(vec4 current, vec4 neighbours) 90 | { 91 | return vec4( 92 | evolveCell(current.x, neighbours.x), 93 | evolveCell(current.y, neighbours.y), 94 | evolveCell(current.z, neighbours.z), 95 | evolveCell(current.w, neighbours.w) 96 | ); 97 | } 98 | 99 | mat4 evolveBlock(mat4 current, mat4 neighbours) 100 | { 101 | return mat4( 102 | evolveColumn(current[0], neighbours[0]), 103 | evolveColumn(current[1], neighbours[1]), 104 | evolveColumn(current[2], neighbours[2]), 105 | evolveColumn(current[3], neighbours[3]) 106 | ); 107 | } 108 | 109 | void main(void) 110 | { 111 | vec4 topLeftTexel = texture2D(uPrevious, coords + vec2(-1,-1) * uPhysicalCellSize); 112 | vec4 leftTexel = texture2D(uPrevious, coords + vec2(-1,+0) * uPhysicalCellSize); 113 | vec4 bottomLeftTexel = texture2D(uPrevious, coords + vec2(-1,+1) * uPhysicalCellSize); 114 | vec4 topTexel = texture2D(uPrevious, coords + vec2(+0,-1) * uPhysicalCellSize); 115 | vec4 middleTexel = texture2D(uPrevious, coords); 116 | vec4 bottomTexel = texture2D(uPrevious, coords + vec2(+0,+1) * uPhysicalCellSize); 117 | vec4 topRightTexel = texture2D(uPrevious, coords + vec2(+1,-1) * uPhysicalCellSize); 118 | vec4 rightTexel = texture2D(uPrevious, coords + vec2(+1,+0) * uPhysicalCellSize); 119 | vec4 bottomRightTexel = texture2D(uPrevious, coords + vec2(+1,+1) * uPhysicalCellSize); 120 | 121 | vec4 top = unpackBottomRow(topTexel); 122 | vec4 bottom = unpackTopRow(bottomTexel); 123 | vec4 left = unpackRightColumn(leftTexel); 124 | vec4 right = unpackLeftColumn(rightTexel); 125 | float topLeft = unpackBottomRight(topLeftTexel); 126 | float topRight = unpackBottomLeft(topRightTexel); 127 | float bottomLeft = unpackTopRight(bottomLeftTexel); 128 | float bottomRight = unpackTopLeft(bottomRightTexel); 129 | 130 | mat4 current = unpackTexel(middleTexel); 131 | 132 | mat4 neighbours = mat4( 133 | /* First column */ 134 | topLeft + top.x + top.y + left.x + current[1][0] + left.y + current[0][1] + current[1][1], 135 | left.x + current[0][0] + current[1][0] + left.y + current[1][1] + left.z + current[0][2] + current[1][2], 136 | left.y + current[0][1] + current[1][1] + left.z + current[1][2] + left.w + current[0][3] + current[1][3], 137 | left.z + current[0][2] + current[1][2] + left.w + current[1][3] + bottomLeft + bottom.x + bottom.y, 138 | 139 | /* Second column */ 140 | top.x + top.y + top.z + current[0][0] + current[2][0] + current[0][1] + current[1][1] + current[2][1], 141 | current[0][0] + current[1][0] + current[2][0] + current[0][1] + current[2][1] + current[0][2] + current[1][2] + current[2][2], 142 | current[0][1] + current[1][1] + current[2][1] + current[0][2] + current[2][2] + current[0][3] + current[1][3] + current[2][3], 143 | current[0][2] + current[1][2] + current[2][2] + current[0][3] + current[2][3] + bottom.x + bottom.y + bottom.z, 144 | 145 | /* Third column */ 146 | top.y + top.z + top.w + current[1][0] + current[3][0] + current[1][1] + current[2][1] + current[3][1], 147 | current[1][0] + current[2][0] + current[3][0] + current[1][1] + current[3][1] + current[1][2] + current[2][2] + current[3][2], 148 | current[1][1] + current[2][1] + current[3][1] + current[1][2] + current[3][2] + current[1][3] + current[2][3] + current[3][3], 149 | current[1][2] + current[2][2] + current[3][2] + current[1][3] + current[3][3] + bottom.y + bottom.z + bottom.w, 150 | 151 | /* Fourth column */ 152 | top.z + top.w + topRight + current[2][0] + right.x + current[2][1] + current[3][1] + right.y, 153 | current[2][0] + current[3][0] + right.x + current[2][1] + right.y + current[2][2] + current[3][2] + right.z, 154 | current[2][1] + current[3][1] + right.y + current[2][2] + right.z + current[2][3] + current[3][3] + right.w, 155 | current[2][2] + current[3][2] + right.z + current[2][3] + right.w + bottom.z + bottom.w + bottomRight 156 | ); 157 | 158 | gl_FragColor = packTexel(evolveBlock(current, neighbours)); 159 | } -------------------------------------------------------------------------------- /docs/readme/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Game of Life - Explanations 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 25 |
26 |
27 |
28 |

game-of-life-webgl

29 |

Description

30 |

This project is a simple simulation of Conway's Game of Life, running on GPU.

31 |

The rules can be changed to see how the world evolves. You can use the mouse to zoom in and explore the world.

32 |

See it live here.

33 |

Donate

34 |

Illustration

35 |

Details

36 |

Texel packing

37 |

In order for the GPU to compute the world's evolution, the grid data must be stored in a texture so that it can be accessed from the fragment shader.

38 |

A first approach would be to store each cell in a pixel of the texture. However the cell state is binary and it would be a waste to use a whole 32 bits texel to store it.

39 |

Unfortunately, compact texture formats are only available in WebGL 2, so I chose to use a RGBA texture and to pack a 4x4 binary grid in it: each 8 bit channel stores the state of 4 vertical cells. The packing/unpacking is handled in the fragment shaders and uses floats since WebGL 1 does not allow integer textures or bitwise operators.

40 |

Each texel contains a 4x4 grid of cells

41 |

Packing a 4x4 cells grid in each texel allows for faster processing. The texture stored in VRAM is 16 times smaller than the visible grid. Updating a texel still takes 9 texture fetches (8 neighbours + previous version of itself) but it updates 16 grid cells at once, so we only need 0.56 texture fetches per grid cell (instead of 9 for a non-compressed storage). Of course by using more appropriate formats, we could achieve a much better ratio but it was enough for this project.

42 |

Packing / unpacking the cells adds a bit more computing complexity to the shaders, however it negligible compared to the texture fetches it avoids. Moreover, I try to unpack as few cells as needed: in order to udpate a texel, I only unpack one mat4, four vec4 and four floats.

43 |

Each texel contains a 4x4 grid of cells

44 |

Rules

45 |

The update shader is partially rewritten and recompiled every time the rules change. The rules are written using exclusively the step function. The standard Game of Life rules require four step calls to update a cell (and the compiler will mutualize two of them so only three calls are actually used).

46 |

Persistence

47 |

Persistence is a visual effect making a dead cell look like it is slowly turning off. It helps see motion (especially gliders).

48 |

Since grid data is compressed into the texture, there is no room to store for how long a cell has been dead. So to add persistence to the rendering, I simply avoid clearing the drawing buffer each frame (using the 'preserveDrawingBuffer' option) and I use blending to slowly dim dead cells. Unfortunately because of this technique, when zooming or moving in the world, the canvas must be cleared to avoid artifacts, and the persistence is lost.

49 |
50 |
51 | 52 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/ts/gl-utils/matrix/mat4.ts: -------------------------------------------------------------------------------- 1 | import { Vec3 } from "./vec3"; 2 | 3 | /* Column-first */ 4 | class Mat4 { 5 | private static _tmpMatrix = null; // helps for internal computing 6 | 7 | private static get tmpMatrix(): Mat4 { 8 | if (Mat4._tmpMatrix === null) { 9 | Mat4._tmpMatrix = new Mat4(); 10 | } 11 | 12 | return Mat4._tmpMatrix; 13 | } 14 | 15 | private _val: Float32Array; 16 | 17 | constructor() { 18 | this._val = new Float32Array(16); 19 | this.identity(); 20 | } 21 | 22 | public get val(): Float32Array { 23 | return this._val; 24 | } 25 | 26 | public identity(): void { 27 | for (let i = 0; i < 16; ++i) { 28 | this._val[i] = 0; 29 | } 30 | 31 | this._val[0] = 1; 32 | this._val[5] = 1; 33 | this._val[10] = 1; 34 | this._val[15] = 1; 35 | } 36 | 37 | public lookAt(eye: Vec3, center: Vec3, up: Vec3): void { 38 | if (eye.equals(center)) { 39 | this.identity(); 40 | return; 41 | } 42 | 43 | const z = Vec3.substract(eye, center); 44 | z.normalize(); 45 | 46 | const x = Vec3.crossProduct(up, z); 47 | x.normalize(); 48 | 49 | const y = Vec3.crossProduct(z, x); 50 | y.normalize(); 51 | 52 | this._val[0] = x.x; 53 | this._val[1] = y.x; 54 | this._val[2] = z.x; 55 | this._val[3] = 0; 56 | this._val[4] = x.y; 57 | this._val[5] = y.y; 58 | this._val[6] = z.y; 59 | this._val[7] = 0; 60 | this._val[8] = x.z; 61 | this._val[9] = y.z; 62 | this._val[10] = z.z; 63 | this._val[11] = 0; 64 | this._val[12] = -Vec3.dotProduct(x, eye); 65 | this._val[13] = -Vec3.dotProduct(y, eye); 66 | this._val[14] = -Vec3.dotProduct(z, eye); 67 | this._val[15] = 1; 68 | } 69 | 70 | public multiplyRight(m2: Mat4) { 71 | const tmp = Mat4.tmpMatrix._val; 72 | const myself = this._val; 73 | const other = m2._val; 74 | 75 | for (let iCol = 0; iCol < 4; ++iCol) { 76 | for (let iRow = 0; iRow < 4; ++iRow) { 77 | tmp[4 * iCol + iRow] = 0; 78 | 79 | for (let i = 0; i < 4; ++i) { 80 | tmp[4 * iCol + iRow] += myself[4 * i + iRow] * other[4 * iCol + i]; 81 | } 82 | } 83 | } 84 | 85 | this.swapWithTmpMatrix(); 86 | } 87 | 88 | /* Returns false is the matrix cannot be inverted. */ 89 | public invert(): boolean { 90 | // shortcuts 91 | const m = this._val; 92 | /* tslint:disable:one-variable-per-declaration */ 93 | const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3]; 94 | const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7]; 95 | const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11]; 96 | const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15]; 97 | /* tslint:enable:one-variable-per-declaration */ 98 | 99 | const b00 = m00 * m11 - m01 * m10; 100 | const b01 = m00 * m12 - m02 * m10; 101 | const b02 = m00 * m13 - m03 * m10; 102 | const b03 = m01 * m12 - m02 * m11; 103 | const b04 = m01 * m13 - m03 * m11; 104 | const b05 = m02 * m13 - m03 * m12; 105 | const b06 = m20 * m31 - m21 * m30; 106 | const b07 = m20 * m32 - m22 * m30; 107 | const b08 = m20 * m33 - m23 * m30; 108 | const b09 = m21 * m32 - m22 * m31; 109 | const b10 = m21 * m33 - m23 * m31; 110 | const b11 = m22 * m33 - m23 * m32; 111 | 112 | // Compute the determinant 113 | let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 114 | 115 | if (!det) { 116 | return false; 117 | } 118 | det = 1.0 / det; 119 | 120 | /* Compute the invert and store it on Mat4.tmp matrix */ 121 | const tmpVals = Mat4.tmpMatrix._val; 122 | tmpVals[0] = (m11 * b11 - m12 * b10 + m13 * b09) * det; 123 | tmpVals[1] = (m02 * b10 - m01 * b11 - m03 * b09) * det; 124 | tmpVals[2] = (m31 * b05 - m32 * b04 + m33 * b03) * det; 125 | tmpVals[3] = (m22 * b04 - m21 * b05 - m23 * b03) * det; 126 | tmpVals[4] = (m12 * b08 - m10 * b11 - m13 * b07) * det; 127 | tmpVals[5] = (m00 * b11 - m02 * b08 + m03 * b07) * det; 128 | tmpVals[6] = (m32 * b02 - m30 * b05 - m33 * b01) * det; 129 | tmpVals[7] = (m20 * b05 - m22 * b02 + m23 * b01) * det; 130 | tmpVals[8] = (m10 * b10 - m11 * b08 + m13 * b06) * det; 131 | tmpVals[9] = (m01 * b08 - m00 * b10 - m03 * b06) * det; 132 | tmpVals[10] = (m30 * b04 - m31 * b02 + m33 * b00) * det; 133 | tmpVals[11] = (m21 * b02 - m20 * b04 - m23 * b00) * det; 134 | tmpVals[12] = (m11 * b07 - m10 * b09 - m12 * b06) * det; 135 | tmpVals[13] = (m00 * b09 - m01 * b07 + m02 * b06) * det; 136 | tmpVals[14] = (m31 * b01 - m30 * b03 - m32 * b00) * det; 137 | tmpVals[15] = (m20 * b03 - m21 * b01 + m22 * b00) * det; 138 | 139 | this.swapWithTmpMatrix(); 140 | 141 | return true; 142 | } 143 | 144 | public perspective(fovy: number, aspectRatio: number, nearPlane: number, farPlane: number): void { 145 | const f = 1 / Math.tan(fovy / 2); 146 | 147 | this._val[0] = f / aspectRatio; 148 | this._val[1] = 0; 149 | this._val[2] = 0; 150 | this._val[3] = 0; 151 | this._val[4] = 0; 152 | this._val[5] = f; 153 | this._val[6] = 0; 154 | this._val[7] = 0; 155 | this._val[8] = 0; 156 | this._val[9] = 0; 157 | this._val[11] = -1; 158 | this._val[12] = 0; 159 | this._val[13] = 0; 160 | this._val[15] = 0; 161 | 162 | if (farPlane === Infinity) { 163 | this._val[10] = -1; 164 | this._val[14] = -2 * nearPlane; 165 | } else { 166 | const tmp = 1 / (nearPlane - farPlane); 167 | this._val[10] = (farPlane + nearPlane) * tmp; 168 | this._val[14] = (2 * farPlane * nearPlane) * tmp; 169 | } 170 | } 171 | 172 | public perspectiveInverse(fovy: number, aspectRatio: number, nearPlane: number, farPlane: number): void { 173 | const f = Math.tan(fovy / 2); 174 | 175 | this._val[0] = aspectRatio * f; 176 | this._val[1] = 0; 177 | this._val[2] = 0; 178 | this._val[3] = 0; 179 | this._val[4] = 0; 180 | this._val[5] = f; 181 | this._val[6] = 0; 182 | this._val[7] = 0; 183 | this._val[8] = 0; 184 | this._val[9] = 0; 185 | this._val[10] = 0; 186 | this._val[12] = 0; 187 | this._val[13] = 0; 188 | this._val[14] = -1; 189 | 190 | if (farPlane === Infinity) { 191 | this._val[11] = -0.5; 192 | this._val[15] = 0.5 / nearPlane; 193 | } else { 194 | const tmp = 0.5 / (nearPlane * farPlane); 195 | this._val[11] = (nearPlane - farPlane) * tmp; 196 | this._val[15] = (nearPlane + farPlane) * tmp; 197 | } 198 | } 199 | 200 | private swapWithTmpMatrix(): void { 201 | const tmp = Mat4.tmpMatrix._val; 202 | Mat4.tmpMatrix._val = this._val; 203 | this._val = tmp; 204 | } 205 | } 206 | 207 | export { Mat4 }; 208 | -------------------------------------------------------------------------------- /src/ts/parameters.ts: -------------------------------------------------------------------------------- 1 | import "./page-interface-generated"; 2 | 3 | enum Rule { 4 | DEATH = "death", 5 | ALIVE = "alive", 6 | BIRTH = "birth", 7 | } 8 | 9 | type RulesSet = [Rule, Rule, Rule, Rule, Rule, Rule, Rule, Rule, Rule]; 10 | 11 | const DEFAULT_RULES: RulesSet = [ 12 | Rule.DEATH, 13 | Rule.DEATH, 14 | Rule.ALIVE, 15 | Rule.BIRTH, 16 | Rule.DEATH, 17 | Rule.DEATH, 18 | Rule.DEATH, 19 | Rule.DEATH, 20 | Rule.DEATH, 21 | ]; 22 | 23 | const rules = DEFAULT_RULES.slice() as RulesSet; 24 | 25 | function updateRuleControl(id: number) { 26 | const controlId = `neighbours-tabs-${id}`; 27 | if (rules[id] === Rule.DEATH) { 28 | Page.Tabs.setValues(controlId, ["death"]); 29 | } else if (rules[id] === Rule.ALIVE) { 30 | Page.Tabs.setValues(controlId, ["alive"]); 31 | } else if (rules[id] === Rule.BIRTH) { 32 | Page.Tabs.setValues(controlId, ["alive", "birth"]); 33 | } 34 | Page.Tabs.storeState(controlId); 35 | } 36 | 37 | type RuleObserver = () => void; 38 | const rulesObservers: RuleObserver[] = []; 39 | function callRulesObservers(): void { 40 | for (const observer of rulesObservers) { 41 | observer(); 42 | } 43 | } 44 | 45 | const CUSTOM_RULE_FLAG = "customrules"; 46 | function addCustomRulesUrlFlag(): void { 47 | if (typeof URLSearchParams !== "undefined") { 48 | const searchParamsObject = new URLSearchParams(window.location.search); 49 | searchParamsObject.set(CUSTOM_RULE_FLAG, "true"); 50 | const searchParams = searchParamsObject.toString(); 51 | 52 | const newUrl = window.location.origin + window.location.pathname + (searchParams ? `?${searchParams}` : "") 53 | window.history.replaceState("", "", newUrl); 54 | } 55 | 56 | for (let i = 0; i < 9; i++) { 57 | const controlId = `neighbours-tabs-${i}`; 58 | Page.Tabs.storeState(controlId); 59 | } 60 | } 61 | 62 | function removeCustomRulesUrlFlag(): void { 63 | if (typeof URLSearchParams !== "undefined") { 64 | const searchParamsObject = new URLSearchParams(window.location.search); 65 | searchParamsObject.delete(CUSTOM_RULE_FLAG); 66 | const searchParams = searchParamsObject.toString(); 67 | 68 | const newUrl = window.location.origin + window.location.pathname + (searchParams ? `?${searchParams}` : "") 69 | window.history.replaceState("", "", newUrl); 70 | } 71 | } 72 | 73 | function isCustomRulesUrlFlagPresent(): boolean { 74 | if (typeof URLSearchParams !== "undefined") { 75 | const searchParamsObject = new URLSearchParams(window.location.search); 76 | return searchParamsObject.has(CUSTOM_RULE_FLAG); 77 | } 78 | return false; 79 | } 80 | 81 | window.addEventListener("load", () => { 82 | const customRules = isCustomRulesUrlFlagPresent(); 83 | 84 | for (let i = 0; i < 9; ++i) { 85 | const controlId = `neighbours-tabs-${i}`; 86 | 87 | Page.Tabs.addObserver(controlId, (values) => { 88 | const previous = rules[i]; 89 | if (rules[i] !== Rule.DEATH && values.indexOf(Rule.DEATH) >= 0) { 90 | rules[i] = Rule.DEATH; 91 | } else if (rules[i] !== Rule.ALIVE && values.indexOf(Rule.ALIVE) >= 0) { 92 | rules[i] = Rule.ALIVE; 93 | } else if (rules[i] !== Rule.BIRTH && values.indexOf(Rule.BIRTH) >= 0) { 94 | rules[i] = Rule.BIRTH; 95 | } 96 | 97 | updateRuleControl(i); 98 | addCustomRulesUrlFlag(); 99 | 100 | if (previous !== rules[i]) { 101 | callRulesObservers(); 102 | } 103 | }); 104 | 105 | if (customRules) { 106 | const values = Page.Tabs.getValues(controlId); 107 | if (values.indexOf(Rule.BIRTH) >= 0) { 108 | rules[i] = Rule.BIRTH; 109 | } else if (values.indexOf(Rule.ALIVE) >= 0) { 110 | rules[i] = Rule.ALIVE; 111 | } else { 112 | rules[i] = Rule.DEATH; 113 | } 114 | } 115 | 116 | updateRuleControl(i); 117 | } 118 | 119 | callRulesObservers(); 120 | }); 121 | 122 | Page.Button.addObserver("reset-rules-button-id", () => { 123 | let somethingChanged = false; 124 | for (let i = 0; i < 9; i++) { 125 | somethingChanged = somethingChanged || (rules[i] !== DEFAULT_RULES[i]); 126 | rules[i] = DEFAULT_RULES[i]; 127 | updateRuleControl(i); 128 | Page.Tabs.clearStoredState(`neighbours-tabs-${i}`); 129 | } 130 | 131 | removeCustomRulesUrlFlag(); 132 | 133 | if (somethingChanged) { 134 | callRulesObservers(); 135 | } 136 | }); 137 | 138 | type ButtonObserver = () => void; 139 | type RangeObserver = (newValue: number) => void; 140 | 141 | let autorun: boolean; 142 | const AUTORUN_CONTROL_ID = "autorun-checkbox-id"; 143 | Page.Checkbox.addObserver(AUTORUN_CONTROL_ID, (checked: boolean) => { 144 | autorun = checked; 145 | }); 146 | autorun = Page.Checkbox.isChecked(AUTORUN_CONTROL_ID); 147 | 148 | let speed: number; 149 | const updateWaitTime = [1000 / 1, 1000 / 2, 1000 / 5, 1000 / 11, 1000 / 31, 0]; // iterations per second 150 | const SPEED_CONTROL_ID = "speed-range-id"; 151 | Page.Range.addObserver(SPEED_CONTROL_ID, (newValue: number) => { 152 | speed = newValue; 153 | }); 154 | speed = Page.Range.getValue(SPEED_CONTROL_ID); 155 | 156 | const NEXT_STEP_CONTROL_ID = "next-button-id"; 157 | const nextStepObservers: ButtonObserver[] = []; 158 | Page.Button.addObserver(NEXT_STEP_CONTROL_ID, () => { 159 | for (const observer of nextStepObservers) { 160 | observer(); 161 | } 162 | }); 163 | 164 | const RESET_CONTROL_ID = "reset-button-id"; 165 | const resetObservers: ButtonObserver[] = []; 166 | Page.Button.addObserver(RESET_CONTROL_ID, () => { 167 | for (const observer of resetObservers) { 168 | observer(); 169 | } 170 | }); 171 | 172 | let persistence: number; 173 | const persistenceObservers: RangeObserver[] = []; 174 | const persistenceScale = [0, .6, .7, .8, .9]; 175 | const PERSISTENCE_CONTROL_ID = "persistence-range-id"; 176 | Page.Range.addObserver(PERSISTENCE_CONTROL_ID, (newValue: number) => { 177 | persistence = newValue; 178 | 179 | for (const observer of persistenceObservers) { 180 | observer(persistence); 181 | } 182 | }); 183 | persistence = Page.Range.getValue(PERSISTENCE_CONTROL_ID); 184 | 185 | let scale: number; // integer 186 | let exactScale: number; 187 | const MIN_SCALE = 1; 188 | const MAX_SCALE = 10; 189 | type ScaleObserver = (newScale: number, zoomCenter: number[]) => void; 190 | const scaleObservers: ScaleObserver[] = []; 191 | Page.Canvas.Observers.mouseWheel.push((delta: number, zoomCenter: number[]) => { 192 | exactScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, exactScale - delta)); 193 | 194 | const newScale = Math.round(exactScale); 195 | if (newScale !== scale) { 196 | scale = newScale; 197 | 198 | if (!zoomCenter) { 199 | zoomCenter = Page.Canvas.getMousePosition(); 200 | } 201 | 202 | for (const observer of scaleObservers) { 203 | observer(scale, zoomCenter); 204 | } 205 | } 206 | }); 207 | scale = MIN_SCALE; 208 | exactScale = scale; 209 | 210 | const INDICATORS_CONTROL_ID = "indicators-checkbox-id"; 211 | Page.Checkbox.addObserver(INDICATORS_CONTROL_ID, (checked: boolean) => { 212 | Page.Canvas.setIndicatorsVisibility(checked); 213 | }); 214 | 215 | class Parameters { 216 | public static get autorun(): boolean { 217 | return autorun; 218 | } 219 | public static set autorun(ar: boolean) { 220 | autorun = ar; 221 | Page.Checkbox.setChecked(AUTORUN_CONTROL_ID, ar); 222 | } 223 | 224 | public static get updateWaitTime(): number { 225 | return updateWaitTime[speed - 1]; 226 | } 227 | 228 | public static get nextStepObservers(): ButtonObserver[] { 229 | return nextStepObservers; 230 | } 231 | 232 | public static get resetObservers(): ButtonObserver[] { 233 | return resetObservers; 234 | } 235 | 236 | public static get scale(): number { 237 | return scale; 238 | } 239 | public static get scaleObservers(): ScaleObserver[] { 240 | return scaleObservers; 241 | } 242 | 243 | public static get persistence(): number { 244 | return persistenceScale[persistence]; 245 | } 246 | public static set persistence(newValue: number) { 247 | Page.Range.setValue(PERSISTENCE_CONTROL_ID, newValue); 248 | persistence = Page.Range.getValue(PERSISTENCE_CONTROL_ID); 249 | } 250 | public static get persistenceObservers(): RangeObserver[] { 251 | return persistenceObservers; 252 | } 253 | 254 | public static get rules(): RulesSet { 255 | return rules; 256 | } 257 | public static get rulesObservers(): RuleObserver[] { 258 | return rulesObservers; 259 | } 260 | 261 | private constructor() { } 262 | } 263 | 264 | export default Parameters; 265 | -------------------------------------------------------------------------------- /src/ts/gl-utils/shader.ts: -------------------------------------------------------------------------------- 1 | import GLResource from "./gl-resource"; 2 | import VBO from "./vbo"; 3 | 4 | function notImplemented(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: any): void { 5 | alert("NOT IMPLEMENTED YET"); 6 | } 7 | 8 | function bindUniformFloat(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number | number[]): void; 9 | function bindUniformFloat(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: any): void { 10 | if (Array.isArray(value)) { 11 | gl.uniform1fv(location, value); 12 | } else { 13 | gl.uniform1f(location, value); 14 | } 15 | } 16 | 17 | function bindUniformFloat2v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void { 18 | gl.uniform2fv(location, value); 19 | } 20 | 21 | function bindUniformFloat3v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void { 22 | gl.uniform3fv(location, value); 23 | } 24 | 25 | function bindUniformFloat4v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void { 26 | gl.uniform4fv(location, value); 27 | } 28 | 29 | function bindUniformInt(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number | number[]): void; 30 | function bindUniformInt(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: any): void { 31 | if (Array.isArray(value)) { 32 | gl.uniform1iv(location, value); 33 | } else { 34 | gl.uniform1iv(location, value); 35 | } 36 | } 37 | 38 | function bindUniformInt2v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void { 39 | gl.uniform2iv(location, value); 40 | } 41 | 42 | function bindUniformInt3v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void { 43 | gl.uniform3iv(location, value); 44 | } 45 | 46 | function bindUniformInt4v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void { 47 | gl.uniform4iv(location, value); 48 | } 49 | 50 | function bindUniformBool(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: boolean | number): void { 51 | gl.uniform1i(location, +value); 52 | } 53 | 54 | function bindUniformBool2v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value): void { 55 | gl.uniform2iv(location, value); 56 | } 57 | 58 | function bindUniformBool3v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value): void { 59 | gl.uniform3iv(location, value); 60 | } 61 | 62 | function bindUniformBool4v(gl: WebGLRenderingContext, location: WebGLUniformLocation, value): void { 63 | gl.uniform4iv(location, value); 64 | } 65 | 66 | function bindUniformFloatMat2(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void { 67 | gl.uniformMatrix2fv(location, false, value); 68 | } 69 | 70 | function bindUniformFloatMat3(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void { 71 | gl.uniformMatrix3fv(location, false, value); 72 | } 73 | 74 | function bindUniformFloatMat4(gl: WebGLRenderingContext, location: WebGLUniformLocation, value: number[]): void { 75 | gl.uniformMatrix4fv(location, false, value); 76 | } 77 | 78 | function bindSampler2D(gl: WebGLRenderingContext, location: WebGLUniformLocation, unitNb: number, 79 | value: WebGLTexture): void { 80 | gl.uniform1i(location, unitNb); 81 | gl.activeTexture(gl["TEXTURE" + unitNb]); 82 | gl.bindTexture(gl.TEXTURE_2D, value); 83 | } 84 | 85 | function bindSamplerCube(gl: WebGLRenderingContext, location: WebGLUniformLocation, unitNb: number, 86 | value: WebGLTexture): void { 87 | gl.uniform1i(location, unitNb); 88 | gl.activeTexture(gl["TEXTURE" + unitNb]); 89 | gl.bindTexture(gl.TEXTURE_CUBE_MAP, value); 90 | } 91 | 92 | /* From WebGL spec: 93 | * http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14 */ 94 | const types = { 95 | 0x8B50: { str: "FLOAT_VEC2", binder: bindUniformFloat2v }, 96 | 0x8B51: { str: "FLOAT_VEC3", binder: bindUniformFloat3v }, 97 | 0x8B52: { str: "FLOAT_VEC4", binder: bindUniformFloat4v }, 98 | 0x8B53: { str: "INT_VEC2", binder: bindUniformInt2v }, 99 | 0x8B54: { str: "INT_VEC3", binder: bindUniformInt3v }, 100 | 0x8B55: { str: "INT_VEC4", binder: bindUniformInt4v }, 101 | 0x8B56: { str: "BOOL", binder: bindUniformBool }, 102 | 0x8B57: { str: "BOOL_VEC2", binder: bindUniformBool2v }, 103 | 0x8B58: { str: "BOOL_VEC3", binder: bindUniformBool3v }, 104 | 0x8B59: { str: "BOOL_VEC4", binder: bindUniformBool4v }, 105 | 0x8B5A: { str: "FLOAT_MAT2", binder: bindUniformFloatMat2 }, 106 | 0x8B5B: { str: "FLOAT_MAT3", binder: bindUniformFloatMat3 }, 107 | 0x8B5C: { str: "FLOAT_MAT4", binder: bindUniformFloatMat4 }, 108 | 0x8B5E: { str: "SAMPLER_2D", binder: bindSampler2D }, 109 | 0x8B60: { str: "SAMPLER_CUBE", binder: bindSamplerCube }, 110 | 0x1400: { str: "BYTE", binder: notImplemented }, 111 | 0x1401: { str: "UNSIGNED_BYTE", binder: notImplemented }, 112 | 0x1402: { str: "SHORT", binder: notImplemented }, 113 | 0x1403: { str: "UNSIGNED_SHORT", binder: notImplemented }, 114 | 0x1404: { str: "INT", binder: bindUniformInt }, 115 | 0x1405: { str: "UNSIGNED_INT", binder: notImplemented }, 116 | 0x1406: { str: "FLOAT", binder: bindUniformFloat }, 117 | }; 118 | 119 | interface IShaderUniform { 120 | value: boolean | boolean[] | number | number[] | WebGLTexture | WebGLTexture[]; 121 | loc: WebGLUniformLocation; 122 | size: number; 123 | type: number; 124 | } 125 | 126 | interface IShaderAttribute { 127 | VBO: VBO; 128 | loc: GLint; 129 | size: number; 130 | type: number; 131 | } 132 | 133 | class ShaderProgram extends GLResource { 134 | public u: IShaderUniform[]; 135 | public a: IShaderAttribute[]; 136 | 137 | private id: WebGLProgram; 138 | private uCount: number; 139 | private aCount: number; 140 | 141 | constructor(gl: WebGLRenderingContext, vertexSource: string, fragmentSource: string) { 142 | function createShader(type: GLenum, source: string): WebGLShader { 143 | const shader = gl.createShader(type); 144 | gl.shaderSource(shader, source); 145 | gl.compileShader(shader); 146 | 147 | const compileSuccess = gl.getShaderParameter(shader, gl.COMPILE_STATUS); 148 | if (!compileSuccess) { 149 | console.error(gl.getShaderInfoLog(shader)); 150 | gl.deleteShader(shader); 151 | return null; 152 | } 153 | 154 | return shader; 155 | } 156 | 157 | super(gl); 158 | 159 | this.id = null; 160 | this.uCount = 0; 161 | this.aCount = 0; 162 | 163 | const vertexShader = createShader(gl.VERTEX_SHADER, vertexSource); 164 | const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentSource); 165 | 166 | const id = gl.createProgram(); 167 | gl.attachShader(id, vertexShader); 168 | gl.attachShader(id, fragmentShader); 169 | gl.linkProgram(id); 170 | 171 | const linkSuccess = gl.getProgramParameter(id, gl.LINK_STATUS); 172 | if (!linkSuccess) { 173 | console.error(gl.getProgramInfoLog(id)); 174 | gl.deleteProgram(id); 175 | } else { 176 | this.id = id; 177 | 178 | this.introspection(); 179 | } 180 | } 181 | 182 | public freeGLResources(): void { 183 | super.gl().deleteProgram(this.id); 184 | this.id = null; 185 | } 186 | 187 | public use(): void { 188 | super.gl().useProgram(this.id); 189 | } 190 | 191 | public bindUniforms(): void { 192 | const gl: WebGLRenderingContext = super.gl(); 193 | let currTextureUnitNb: number = 0; 194 | 195 | Object.keys(this.u).forEach((uName) => { 196 | const uniform = this.u[uName]; 197 | if (uniform.value !== null) { 198 | if (uniform.type === 0x8B5E || uniform.type === 0x8B60) { 199 | const unitNb: number = currTextureUnitNb; 200 | types[uniform.type].binder(gl, uniform.loc, unitNb, uniform.value); 201 | currTextureUnitNb++; 202 | } else { 203 | types[uniform.type].binder(gl, uniform.loc, uniform.value); 204 | } 205 | } 206 | }); 207 | } 208 | 209 | public bindAttributes(): void { 210 | Object.keys(this.a).forEach((aName) => { 211 | const attribute = this.a[aName]; 212 | if (attribute.VBO !== null) { 213 | attribute.VBO.bind(attribute.loc); 214 | } 215 | }); 216 | } 217 | 218 | public bindUniformsAndAttributes(): void { 219 | this.bindUniforms(); 220 | this.bindAttributes(); 221 | } 222 | 223 | private introspection(): void { 224 | const gl = super.gl(); 225 | 226 | this.uCount = gl.getProgramParameter(this.id, gl.ACTIVE_UNIFORMS); 227 | this.u = []; 228 | for (let i = 0; i < this.uCount; ++i) { 229 | const uniform = gl.getActiveUniform(this.id, i); 230 | const name = uniform.name; 231 | 232 | this.u[name] = { 233 | loc: gl.getUniformLocation(this.id, name), 234 | size: uniform.size, 235 | type: uniform.type, 236 | value: null, 237 | }; 238 | } 239 | 240 | this.aCount = gl.getProgramParameter(this.id, gl.ACTIVE_ATTRIBUTES); 241 | this.a = []; 242 | for (let i = 0; i < this.aCount; ++i) { 243 | const attribute = gl.getActiveAttrib(this.id, i); 244 | const name = attribute.name; 245 | 246 | this.a[name] = { 247 | VBO: null, 248 | loc: gl.getAttribLocation(this.id, name), 249 | size: attribute.size, 250 | type: attribute.type, 251 | }; 252 | } 253 | } 254 | } 255 | 256 | export default ShaderProgram; 257 | -------------------------------------------------------------------------------- /src/generate-page-template.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { Demopage } from "webpage-templates"; 4 | 5 | const data = { 6 | title: "Game of Life", 7 | description: "Variations of Conway's Game of Life, running on GPU.", 8 | introduction: [ 9 | "This project is a simple simulation of Conway's Game of Life, running on GPU.", 10 | "The rules can be changed to see how the world evolves. You can use the mouse to zoom in and explore the world." 11 | ], 12 | githubProjectName: "game-of-life-webgl", 13 | readme: { 14 | filepath: path.join(__dirname, "..", "README.md"), 15 | branchName: "master" 16 | }, 17 | additionalLinks: [], 18 | scriptFiles: [ 19 | "script/main.min.js" 20 | ], 21 | indicators: [ 22 | { 23 | id: "iterations-per-sec", 24 | label: "Iterations per second" 25 | }, 26 | { 27 | id: "iteration", 28 | label: "Iteration" 29 | }, 30 | { 31 | id: "grid-size", 32 | label: "Grid size" 33 | } 34 | ], 35 | canvas: { 36 | width: 512, 37 | height: 512, 38 | enableFullscreen: true 39 | }, 40 | controlsSections: [ 41 | { 42 | title: "Simulation", 43 | controls: [ 44 | { 45 | type: Demopage.supportedControls.Checkbox, 46 | title: "Autorun", 47 | id: "autorun-checkbox-id", 48 | checked: true 49 | }, 50 | { 51 | type: Demopage.supportedControls.Range, 52 | title: "Speed", 53 | id: "speed-range-id", 54 | min: 1, 55 | max: 6, 56 | value: 6, 57 | step: 1 58 | }, 59 | { 60 | type: Demopage.supportedControls.Button, 61 | id: "next-button-id", 62 | label: "Next step", 63 | flat: true 64 | }, 65 | { 66 | type: Demopage.supportedControls.Button, 67 | id: "reset-button-id", 68 | label: "Reset", 69 | flat: true 70 | } 71 | ] 72 | }, 73 | { 74 | title: "Rendering", 75 | controls: [ 76 | { 77 | type: Demopage.supportedControls.Range, 78 | title: "Persistence", 79 | id: "persistence-range-id", 80 | min: 0, 81 | max: 4, 82 | value: 0, 83 | step: 1 84 | }, 85 | { 86 | type: Demopage.supportedControls.Checkbox, 87 | title: "Show indicators", 88 | id: "indicators-checkbox-id", 89 | checked: true 90 | } 91 | ] 92 | }, 93 | { 94 | title: "Rules", 95 | controls: [ 96 | { 97 | type: Demopage.supportedControls.Tabs, 98 | title: "0 neighbour", 99 | id: "neighbours-tabs-0", 100 | options: [ 101 | { 102 | value: "death", 103 | label: "Death", 104 | checked: true 105 | }, 106 | { 107 | value: "alive", 108 | label: "Alive" 109 | }, 110 | { 111 | value: "birth", 112 | label: "Birth" 113 | } 114 | ] 115 | }, 116 | { 117 | type: Demopage.supportedControls.Tabs, 118 | title: "1 neighbour", 119 | id: "neighbours-tabs-1", 120 | options: [ 121 | { 122 | value: "death", 123 | label: "Death", 124 | checked: true 125 | }, 126 | { 127 | value: "alive", 128 | label: "Alive" 129 | }, 130 | { 131 | value: "birth", 132 | label: "Birth" 133 | } 134 | ] 135 | }, 136 | { 137 | type: Demopage.supportedControls.Tabs, 138 | title: "2 neighbours", 139 | id: "neighbours-tabs-2", 140 | options: [ 141 | { 142 | value: "death", 143 | label: "Death" 144 | }, 145 | { 146 | value: "alive", 147 | label: "Alive", 148 | checked: true 149 | }, 150 | { 151 | value: "birth", 152 | label: "Birth" 153 | } 154 | ] 155 | }, 156 | { 157 | type: Demopage.supportedControls.Tabs, 158 | title: "3 neighbours", 159 | id: "neighbours-tabs-3", 160 | options: [ 161 | { 162 | value: "death", 163 | label: "Death" 164 | }, 165 | { 166 | value: "alive", 167 | label: "Alive", 168 | checked: true 169 | }, 170 | { 171 | value: "birth", 172 | label: "Birth", 173 | checked: true 174 | } 175 | ] 176 | }, 177 | { 178 | type: Demopage.supportedControls.Tabs, 179 | title: "4 neighbours", 180 | id: "neighbours-tabs-4", 181 | options: [ 182 | { 183 | value: "death", 184 | label: "Death", 185 | checked: true 186 | }, 187 | { 188 | value: "alive", 189 | label: "Alive" 190 | }, 191 | { 192 | value: "birth", 193 | label: "Birth" 194 | } 195 | ] 196 | }, 197 | { 198 | type: Demopage.supportedControls.Tabs, 199 | title: "5 neighbours", 200 | id: "neighbours-tabs-5", 201 | options: [ 202 | { 203 | value: "death", 204 | label: "Death", 205 | checked: true 206 | }, 207 | { 208 | value: "alive", 209 | label: "Alive" 210 | }, 211 | { 212 | value: "birth", 213 | label: "Birth" 214 | } 215 | ] 216 | }, 217 | { 218 | type: Demopage.supportedControls.Tabs, 219 | title: "6 neighbours", 220 | id: "neighbours-tabs-6", 221 | options: [ 222 | { 223 | value: "death", 224 | label: "Death", 225 | checked: true 226 | }, 227 | { 228 | value: "alive", 229 | label: "Alive" 230 | }, 231 | { 232 | value: "birth", 233 | label: "Birth" 234 | } 235 | ] 236 | }, 237 | { 238 | type: Demopage.supportedControls.Tabs, 239 | title: "7 neighbours", 240 | id: "neighbours-tabs-7", 241 | options: [ 242 | { 243 | value: "death", 244 | label: "Death", 245 | checked: true 246 | }, 247 | { 248 | value: "alive", 249 | label: "Alive" 250 | }, 251 | { 252 | value: "birth", 253 | label: "Birth" 254 | } 255 | ] 256 | }, 257 | { 258 | type: Demopage.supportedControls.Tabs, 259 | title: "8 neighbours", 260 | id: "neighbours-tabs-8", 261 | options: [ 262 | { 263 | value: "death", 264 | label: "Death", 265 | checked: true 266 | }, 267 | { 268 | value: "alive", 269 | label: "Alive" 270 | }, 271 | { 272 | value: "birth", 273 | label: "Birth" 274 | } 275 | ] 276 | }, 277 | { 278 | type: Demopage.supportedControls.Button, 279 | id: "reset-rules-button-id", 280 | label: "Reset rules", 281 | flat: true 282 | } 283 | ] 284 | } 285 | ] 286 | }; 287 | 288 | const DEST_DIR = path.resolve(__dirname, "..", "docs"); 289 | const minified = true; 290 | 291 | const buildResult = Demopage.build(data, DEST_DIR, { 292 | debug: !minified, 293 | }); 294 | 295 | // disable linting on this file because it is generated 296 | buildResult.pageScriptDeclaration = "/* tslint:disable */\n" + buildResult.pageScriptDeclaration; 297 | 298 | const SCRIPT_DECLARATION_FILEPATH = path.resolve(__dirname, ".", "ts", "page-interface-generated.ts"); 299 | fs.writeFileSync(SCRIPT_DECLARATION_FILEPATH, buildResult.pageScriptDeclaration); -------------------------------------------------------------------------------- /src/ts/automaton-2D.ts: -------------------------------------------------------------------------------- 1 | import FBO from "./gl-utils/fbo"; 2 | import { gl } from "./gl-utils/gl-canvas"; 3 | import GLResource from "./gl-utils/gl-resource"; 4 | import Shader from "./gl-utils/shader"; 5 | import * as ShaderManager from "./gl-utils/shader-manager"; 6 | import VBO from "./gl-utils/vbo"; 7 | 8 | import Parameters from "./parameters"; 9 | 10 | import "./page-interface-generated"; 11 | 12 | class Automaton2D extends GLResource { 13 | private _displayShader: Shader; 14 | private _updateShader: Shader; 15 | 16 | private _FBO: FBO; 17 | private _vbo: VBO; 18 | 19 | private _gridSize: number[]; 20 | private _textureSize: number[]; 21 | private _textures: WebGLTexture[]; 22 | private _currentIndex: number; 23 | private _visibleSubTexture: number[]; 24 | 25 | private _iteration: number; 26 | 27 | private _needToRecomputeShader: boolean; 28 | private _needToRedraw: boolean; 29 | private _mustClear: boolean; 30 | 31 | constructor() { 32 | super(gl); 33 | 34 | this._FBO = new FBO(gl, 512, 512); 35 | this._vbo = VBO.createQuad(gl, -1, -1, 1, 1); 36 | 37 | const initializeTexturesForCanvas = () => { 38 | const canvasSize = Page.Canvas.getSize(); 39 | this.initializeTextures(canvasSize[0], canvasSize[1]); 40 | this.recomputeVisibleSubTexture(); 41 | }; 42 | 43 | this._needToRecomputeShader = true; 44 | this._mustClear = true; 45 | 46 | this._textures = [null, null]; 47 | this._visibleSubTexture = [0, 0, 1, 1]; 48 | initializeTexturesForCanvas(); 49 | 50 | Page.Canvas.Observers.mouseDrag.push((dX: number, dY: number) => { 51 | this._visibleSubTexture[0] -= dX * this._visibleSubTexture[2]; 52 | this._visibleSubTexture[1] -= dY * this._visibleSubTexture[3]; 53 | this.recomputeVisibleSubTexture(); 54 | this._needToRedraw = true; 55 | this._mustClear = true; 56 | }); 57 | 58 | Page.Canvas.Observers.canvasResize.push(initializeTexturesForCanvas); 59 | Parameters.resetObservers.push(initializeTexturesForCanvas); 60 | Parameters.rulesObservers.push(() => this._needToRecomputeShader = true ); 61 | 62 | let previousScale = Parameters.scale; 63 | Parameters.scaleObservers.push((newScale: number, zoomCenter: number[]) => { 64 | this._needToRedraw = true; 65 | this._mustClear = true; 66 | 67 | this._visibleSubTexture[0] += zoomCenter[0] * (1 - previousScale / newScale) * this._visibleSubTexture[2]; 68 | this._visibleSubTexture[1] += zoomCenter[1] * (1 - previousScale / newScale) * this._visibleSubTexture[3]; 69 | 70 | previousScale = newScale; 71 | this.recomputeVisibleSubTexture(); 72 | }); 73 | 74 | ShaderManager.buildShader( 75 | { 76 | fragmentFilename: "display-2D.frag", 77 | vertexFilename: "display-2D.vert", 78 | injected: {}, 79 | }, 80 | (shader) => { 81 | if (shader !== null) { 82 | /* tslint:disable:no-string-literal */ 83 | this._displayShader = shader; 84 | this._displayShader.a["aCorner"].VBO = this._vbo; 85 | this._displayShader.u["uSubTexture"].value = this._visibleSubTexture; 86 | /* tslint:enable:no-string-literal */ 87 | } 88 | }, 89 | ); 90 | } 91 | 92 | public freeGLResources(): void { 93 | if (this._FBO) { 94 | this._FBO.freeGLResources(); 95 | } 96 | 97 | if (this._vbo) { 98 | this._vbo.freeGLResources(); 99 | this._vbo = null; 100 | } 101 | 102 | if (this._displayShader) { 103 | this._displayShader.freeGLResources(); 104 | this._displayShader = null; 105 | } 106 | 107 | if (this._updateShader) { 108 | this._updateShader.freeGLResources(); 109 | this._updateShader = null; 110 | } 111 | 112 | this.freeTextures(); 113 | } 114 | 115 | public update(): void { 116 | if (this._needToRecomputeShader) { 117 | this.recomputeUpdateShader(); 118 | } 119 | 120 | const shader = this._updateShader; 121 | 122 | if (shader) { 123 | const current = this._textures[this._currentIndex]; 124 | const next = this._textures[(this._currentIndex + 1) % 2]; 125 | 126 | this._FBO.bind([next]); 127 | 128 | /* tslint:disable:no-string-literal */ 129 | shader.u["uPrevious"].value = current; 130 | shader.u["uPhysicalCellSize"].value = [1 / this._textureSize[0], 1 / this._textureSize[1]]; 131 | /* tslint:enable:no-string-literal */ 132 | 133 | shader.use(); 134 | shader.bindUniformsAndAttributes(); 135 | 136 | gl.disable(gl.BLEND); 137 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 138 | 139 | this._currentIndex = (this._currentIndex + 1) % 2; 140 | this._iteration++; 141 | } 142 | } 143 | 144 | public draw(): void { 145 | const shader = this._displayShader; 146 | 147 | if (shader) { 148 | /* tslint:disable:no-string-literal */ 149 | shader.u["uClearFactor"].value = (this._mustClear) ? 1 : 1 - Parameters.persistence; 150 | shader.u["uTexture"].value = this._textures[this._currentIndex]; 151 | shader.u["uGridSize"].value = this._gridSize; 152 | /* tslint:enable:no-string-literal */ 153 | 154 | shader.use(); 155 | shader.bindUniformsAndAttributes(); 156 | 157 | gl.enable(gl.BLEND); 158 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 159 | 160 | this._needToRedraw = false; 161 | this._mustClear = false; 162 | } 163 | } 164 | 165 | public get iteration(): number { 166 | return this._iteration; 167 | } 168 | 169 | public get needToRedraw(): boolean { 170 | return this._needToRedraw; 171 | } 172 | 173 | public get needToUpdate(): boolean { 174 | return this._needToRecomputeShader; 175 | } 176 | 177 | private recomputeUpdateShader(): void { 178 | ShaderManager.buildShader( 179 | { 180 | fragmentFilename: "update-2D.frag", 181 | vertexFilename: "update-2D.vert", 182 | injected: { rules: this.generateShaderRules() }, 183 | }, 184 | (shader) => { 185 | if (shader !== null) { 186 | if (this._updateShader) { 187 | this._updateShader.freeGLResources(); 188 | } 189 | 190 | /* tslint:disable:no-string-literal */ 191 | this._updateShader = shader; 192 | this._updateShader.a["aCorner"].VBO = this._vbo; 193 | /* tslint:enable:no-string-literal */ 194 | } 195 | }, 196 | ); 197 | this._needToRecomputeShader = false; 198 | } 199 | 200 | private generateShaderRules(): string { 201 | function generateRuleBlock(starting: number, ending: number, rule: string) { 202 | if (rule !== "alive") { 203 | const operation = (rule === "death") ? " -= " : " += "; 204 | let rangeCheck; 205 | if (starting === 0) { 206 | rangeCheck = "step(N, " + (ending + .5) + ")"; 207 | 208 | if (ending === 8) { 209 | rangeCheck = "1.0"; 210 | } 211 | } else if (ending === 8) { 212 | rangeCheck = "step(" + (starting - .5) + ", N)"; 213 | } else { 214 | rangeCheck = "step(" + (starting - .5) + ", N) * step(N, " + (ending + .5) + ")"; 215 | } 216 | 217 | return "currentState" + operation + rangeCheck + ";\n"; 218 | } 219 | return ""; 220 | } 221 | 222 | let result = ""; 223 | 224 | const rules = Parameters.rules; 225 | let currentRule = rules[0]; 226 | let from = 0; 227 | for (let i = 1; i < 9; ++i) { 228 | if (rules[i] !== currentRule) { 229 | result += generateRuleBlock(from, i - 1, currentRule); 230 | currentRule = rules[i]; 231 | from = i; 232 | } 233 | } 234 | result += generateRuleBlock(from, 8, currentRule); 235 | 236 | return result; 237 | } 238 | 239 | private recomputeVisibleSubTexture(): void { 240 | const canvasSize = Page.Canvas.getSize(); 241 | this._visibleSubTexture[2] = canvasSize[0] / this._gridSize[0] / Parameters.scale; 242 | this._visibleSubTexture[3] = canvasSize[1] / this._gridSize[1] / Parameters.scale; 243 | 244 | for (let i = 0; i < 2; ++i) { 245 | this._visibleSubTexture[i] -= Math.min(0, this._visibleSubTexture[i]); 246 | this._visibleSubTexture[i] -= Math.max(0, this._visibleSubTexture[i] + this._visibleSubTexture[i + 2] - 1); 247 | } 248 | } 249 | 250 | private freeTextures(): void { 251 | for (let i = 0; i < 2; ++i) { 252 | if (this._textures[i]) { 253 | gl.deleteTexture(this._textures[i]); 254 | this._textures[i] = null; 255 | } 256 | } 257 | } 258 | 259 | private initializeTextures(width: number, height: number): void { 260 | function upperPowerOfTwo(n: number): number { 261 | return Math.pow(2, Math.ceil(Math.log(n) * Math.LOG2E)); 262 | } 263 | 264 | width = upperPowerOfTwo(width); 265 | height = upperPowerOfTwo(height); 266 | 267 | this._gridSize = [width, height]; 268 | 269 | const physicalWidth = width / 4; 270 | const physicalHeight = height / 4; 271 | 272 | this._FBO.width = physicalWidth; 273 | this._FBO.height = physicalHeight; 274 | 275 | this._textureSize = [physicalWidth, physicalHeight]; 276 | 277 | const data = new Uint8Array(4 * physicalWidth * physicalHeight); 278 | for (let i = data.length - 1; i >= 0; --i) { 279 | data[i] = Math.floor(256 * Math.random()); 280 | } 281 | 282 | for (let i = 0; i < 2; ++i) { 283 | if (this._textures[i] === null) { 284 | this._textures[i] = gl.createTexture(); 285 | } 286 | gl.bindTexture(gl.TEXTURE_2D, this._textures[i]); 287 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, physicalWidth, physicalHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); 288 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 289 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 290 | } 291 | 292 | this._currentIndex = 0; 293 | this._iteration = 0; 294 | this._needToRedraw = true; 295 | Page.Canvas.setIndicatorText("grid-size", width + "x" + height); 296 | } 297 | } 298 | 299 | export default Automaton2D; 300 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Game of Life 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 25 |
26 |
27 |
28 |

Game of Life

29 | 30 |
31 |

This project is a simple simulation of Conway's Game of Life, running on GPU.

32 |

The rules can be changed to see how the world evolves. You can use the mouse to zoom in and explore the world.

33 | 34 |
35 | 36 | 39 |
40 |
41 |
42 | 45 |
46 | 47 | 48 |
49 | 50 |
51 | 52 |
53 | 54 |
55 |
56 |
57 |
58 | Iterations per second: 59 |
60 |
61 | Iteration: 62 |
63 |
64 | Grid size: 65 |
66 |
67 |
68 | 69 | 70 |
71 |
72 |
73 | 79 |
80 |

Simulation

81 | 82 |
83 |
84 | 85 |
86 | 87 | 88 |
89 |
90 |
91 | 92 |
93 | 94 |
95 |
96 | 97 |
98 |
99 |
100 | 101 | 102 | 103 |
104 |
105 | 106 |
107 |
108 |
109 | 110 |
111 |
112 |
113 |
114 |
115 | 116 |
117 |
118 | 119 |
120 |
121 |
122 |
123 |
124 |

Rendering

125 | 126 |
127 |
128 | 129 |
130 | 131 |
132 |
133 | 134 |
135 |
136 |
137 | 138 | 139 | 140 |
141 |
142 | 143 |
144 |
145 |
146 | 147 |
148 |
149 |
150 |
151 |
152 | 153 |
154 | 155 | 156 |
157 |
158 |
159 |
160 |
161 |
162 |

Rules

163 | 164 |
165 |
166 | 167 |
168 | 169 | 170 | 171 | 172 | 173 | 174 |
175 |
176 |
177 | 178 |
179 | 180 | 181 | 182 | 183 | 184 | 185 |
186 |
187 |
188 | 189 |
190 | 191 | 192 | 193 | 194 | 195 | 196 |
197 |
198 |
199 | 200 |
201 | 202 | 203 | 204 | 205 | 206 | 207 |
208 |
209 |
210 | 211 |
212 | 213 | 214 | 215 | 216 | 217 | 218 |
219 |
220 |
221 | 222 |
223 | 224 | 225 | 226 | 227 | 228 | 229 |
230 |
231 |
232 | 233 |
234 | 235 | 236 | 237 | 238 | 239 | 240 |
241 |
242 |
243 | 244 |
245 | 246 | 247 | 248 | 249 | 250 | 251 |
252 |
253 |
254 | 255 |
256 | 257 | 258 | 259 | 260 | 261 | 262 |
263 |
264 |
265 | 266 |
267 |
268 |
269 |
270 |
271 |
272 | 273 | 296 | 303 | 304 | 305 | 306 | 307 | -------------------------------------------------------------------------------- /docs/script/page.min.js: -------------------------------------------------------------------------------- 1 | var Page;!function(e){var e=e.Demopage||(e.Demopage={}),r="error-messages",t=document.getElementById(r);if(!t)throw new Error("Cannot find element '"+r+"'.");function a(e){return t?t.querySelector("span[id=error-message-"+e+"]"):null}e.setErrorMessage=function(e,r){var n;t&&((n=a(e))?n.innerHTML=r:((n=document.createElement("span")).id="error-message-"+e,n.innerText=r,t.appendChild(n),t.appendChild(document.createElement("br"))))},e.removeErrorMessage=function(e){var r;t&&(e=a(e))&&((r=e.nextElementSibling)&&t.removeChild(r),t.removeChild(e))}}(Page=Page||{}); 2 | var Page;!function(n){var e,t,i,a;function s(e){this.queryParameters={};var t=e.indexOf(s.queryDelimiter);if(t<0)this.baseUrl=e;else{this.baseUrl=e.substring(0,t);for(var r=0,n=e.substring(t+s.queryDelimiter.length).split(s.parameterDelimiter);re.length&&(i=this.queryParameters[a],t(a.substring(e.length),i))}},s.prototype.buildUrl=function(){for(var e=[],t=0,r=Object.keys(this.queryParameters);t input[type=checkbox][id]").map(function(e){return new r(e)})}),n=new e.Helpers.Storage("checkbox",function(e){return e.checked?"true":"false"},function(e,t){e=c.getByIdSafe(e);return!(!e||"true"!==t&&"false"!==t||(e.checked="true"===t,e.callObservers(),0))}),e.Helpers.Events.callAfterDOMLoaded(function(){c.load(),n.applyStoredState()}),t.addObserver=function(e,t){c.getById(e).observers.push(t)},t.setChecked=function(e,t){c.getById(e).checked=t},t.isChecked=function(e){return c.getById(e).checked},t.storeState=function(e){e=c.getById(e),n.storeState(e)},t.clearStoredState=function(e){e=c.getById(e),n.clearStoredState(e)}}(Page=Page||{}); 5 | var Page;!function(r){var e,t,n,s,i;function l(e){var t=this,e=(this.onInputObservers=[],this.onChangeObservers=[],this.inputElement=r.Helpers.Utils.selector(e,"input[type='range']"),this.progressLeftElement=r.Helpers.Utils.selector(e,".range-progress-left"),this.tooltipElement=r.Helpers.Utils.selector(e,"output.range-tooltip"),this.id=this.inputElement.id,+this.inputElement.min),n=+this.inputElement.max,i=+this.inputElement.step;this.nbDecimalsToDisplay=l.getMaxNbDecimals(e,n,i),this.inputElement.addEventListener("input",function(e){e.stopPropagation(),t.reloadValue(),t.callSpecificObservers(t.onInputObservers)}),this.inputElement.addEventListener("change",function(e){e.stopPropagation(),t.reloadValue(),s.storeState(t),t.callSpecificObservers(t.onChangeObservers)}),this.reloadValue()}e=r.Range||(r.Range={}),Object.defineProperty(l.prototype,"value",{get:function(){return this._value},set:function(e){this.inputElement.value=""+e,this.reloadValue()},enumerable:!1,configurable:!0}),l.prototype.callObservers=function(){this.callSpecificObservers(this.onInputObservers),this.callSpecificObservers(this.onChangeObservers)},l.prototype.callSpecificObservers=function(e){for(var t=0,n=e;t input[type='range']").map(function(e){e=e.parentElement;return new t(e)})}),s=new r.Helpers.Storage("range",function(e){return""+e.value},function(e,t){e=n.getByIdSafe(e);return!!e&&(e.value=+t,e.callObservers(),!0)}),r.Helpers.Events.callAfterDOMLoaded(function(){n.load(),s.applyStoredState()}),i=!!window.MSInputMethodContext&&!!document.documentMode,e.addObserver=function(e,t){e=n.getById(e),(i?e.onChangeObservers:e.onInputObservers).push(t)},e.addLazyObserver=function(e,t){n.getById(e).onChangeObservers.push(t)},e.getValue=function(e){return n.getById(e).value},e.setValue=function(e,t){n.getById(e).value=t},e.storeState=function(e){e=n.getById(e),s.storeState(e)},e.clearStoredState=function(e){e=n.getById(e),s.clearStoredState(e)}}(Page=Page||{}); 6 | var Page;!function(e){var t,n,r;function o(e){var r=this;this.observers=[],this.id=e.id,this.element=e,this.element.addEventListener("click",function(e){e.stopPropagation();for(var t=0,n=r.observers;tcanvas{width:100%;height:100%;z-index:10}#canvas-container>.loader{display:none}#indicators{display:flex;position:absolute;top:1px;left:1px;flex-direction:column;align-items:flex-start;color:#fff;font-family:"Lucida Console",Monaco,monospace;text-align:left;z-index:20}#indicators>div{flex:0 0 1em;margin:1px;padding:1px 4px;background:#000}#canvas-buttons-column{position:absolute;top:0;right:0;width:32px;z-index:30}#fullscreen-toggle-id{display:block;background-image:url("../images/resize.svg");background-position:0 0;background-size:200%}#fullscreen-toggle-id:hover{background-position-x:100%}#side-pane-toggle-id{display:none;background-image:url("../images/gear.svg");transition:transform .1s ease-in-out;-webkit-transition:transform .1s ease-in-out}#side-pane-toggle-id:hover{transform:rotate(-30deg);-webkit-transform:rotate(-30deg);-ms-transform:rotate(-30deg)}#side-pane-checkbox-id:checked+#canvas-container #side-pane-toggle-id:hover{transform:rotate(30deg);-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg)}.hidden{display:none}#fullscreen-checkbox-id:checked+.demo{position:fixed;overflow:hidden}#fullscreen-checkbox-id:checked+.demo #canvas-container{position:fixed;top:0;left:0;width:100vw;height:100vh;margin:0;overflow:hidden;z-index:5}#fullscreen-checkbox-id:checked+.demo #canvas-container #canvas-buttons-column{transition:transform .2s ease-in-out;-webkit-transition:transform .2s ease-in-out}#fullscreen-checkbox-id:checked+.demo #canvas-container #fullscreen-toggle-id{background-position-y:100%}@media only screen and (min-width: 500px){#fullscreen-checkbox-id:checked+.demo #canvas-container #side-pane-toggle-id{display:block}}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id:checked+#canvas-container #canvas-buttons-column{transform:translateX(-400px)} 6 | .loader{position:absolute;top:0;right:0;bottom:0;left:0;width:120px;height:120px;margin:auto}.loader>span{color:#fff;font-size:32px;line-height:120px;text-shadow:1px 1px #000,-1px 1px #000,1px -1px #000,-1px -1px #000,1px 0 #000,-1px 0 #000,0 1px #000,0 -1px #000}.loader-animation{position:absolute;top:0;left:0;width:120px;height:120px;animation:spin 1.1s linear infinite}.loader-animation:before{position:absolute;top:-1px;left:-1px;width:122px;height:122px;border:6px solid rgba(0,0,0,0);border-top:6px solid #000;border-radius:50%;content:"";z-index:50;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.loader-animation:after{position:absolute;top:0;left:0;width:120px;height:120px;border:4px solid rgba(0,0,0,0);border-top:4px solid #fff;border-radius:50%;content:"";z-index:51;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}} 7 | .canvas-button{width:32px;height:32px;cursor:pointer}.controls-block{flex:1 0 0;max-width:36em;margin:16px 0;padding:12px 0;border-radius:8px;border:1px solid #c9c9c9;border:var(--var-color-block-border, 1px solid #c9c9c9);background:#eeeeee;background:var(--var-color-block-background, #eeeeee);z-index:0}@media only screen and (min-width: 540px){.controls-block{margin:16px}}.controls-block>hr{margin:12px 0;clear:both;border:none;border-top:1px solid #c9c9c9;border-top:var(--var-color-block-border, 1px solid #c9c9c9)}.controls-section{display:flex;flex-flow:row wrap;align-items:baseline;margin:0 16px}.controls-section>h2{width:7em;margin:0;font-size:medium;font-weight:bold;line-height:2em;text-align:left}.controls-section>.controls-list{display:flex;flex-direction:column;flex-grow:1}.controls-list>.control{display:flex;flex-flow:row wrap;align-items:center;min-width:300px;padding:3px 0}.control>label{min-width:8em;font-size:95%;line-height:95%;text-align:left}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block{position:fixed;top:0;left:100%;width:400px;max-height:calc(100% - 48px);margin:0;border-width:0 0 1px 1px;border-radius:0 0 0 8px;z-index:50;overflow-x:hidden;overflow-y:auto;transition:transform .2s ease-in-out;-webkit-transition:transform .2s ease-in-out}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block::-webkit-scrollbar{width:16px}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block::-webkit-scrollbar-track{border-radius:8px;background-color:#eeeeee;background-color:var(--var-color-block-background, #eeeeee)}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block::-webkit-scrollbar-thumb{border-width:3px 5px;border-style:solid;border-radius:8px;border-color:#eeeeee;border-color:var(--var-color-block-background, #eeeeee);background-color:#a5a5a5;background-color:var(--var-color-scrollbar, #a5a5a5)}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block::-webkit-scrollbar-thumb:focus,#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block::-webkit-scrollbar-thumb:hover{background-color:#b2b2b2;background-color:var(--var-color-scrollbar-hover, #b2b2b2)}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block::-webkit-scrollbar-thumb:active{background-color:#959595;background-color:var(--var-color-scrollbar-active, #959595)}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id~.controls-block:hover::-webkit-scrollbar-thumb{border-width:3px}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id:checked~.controls-block{transform:translateX(-100%);-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%)}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id:checked~.controls-block .tooltip{transform:translateX(-100vw) translateX(400px);-webkit-transform:translateX(-100vw) translateX(400px);-ms-transform:translateX(-100vw) translateX(400px)}#fullscreen-checkbox-id:checked+.demo #side-pane-checkbox-id:checked~.controls-block>#side-pane-close-toggle-id{display:block}#side-pane-close-toggle-id{display:none;position:absolute;top:0;right:0}#side-pane-close-toggle-id svg{stroke:#5e5e5e;stroke:var(--var-color-block-actionitem, #5e5e5e)}#side-pane-close-toggle-id svg:focus,#side-pane-close-toggle-id svg:hover{stroke:#7e7e7e;stroke:var(--var-color-block-actionitem-hover, #7e7e7e)}#side-pane-close-toggle-id svg:active{stroke:#535353;stroke:var(--var-color-block-actionitem-active, #535353)} 8 | .checkbox{display:block;position:relative;text-align:left;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.checkbox>input[type=checkbox]{width:1px;height:1px;opacity:0}.checkbox>input[type=checkbox]+label.checkmark,.checkbox>input[type=checkbox]+label.checkmark-line{margin-left:24px;line-height:26px;cursor:pointer}.checkbox>input[type=checkbox]:disabled+label.checkmark,.checkbox>input[type=checkbox]:disabled+label.checkmark-line{cursor:default}.checkbox>input[type=checkbox]+label.checkmark::before{position:absolute;top:calc(.5*(100% - 20px));left:0;width:20px;height:20px;border-width:2px;border-style:solid;border-radius:2px;background:none;content:"";box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.checkbox>input[type=checkbox]+label.checkmark::after{position:absolute;top:calc(.5*(100% - 20px) + .5*(20px - 14px));right:0;bottom:0;left:6.5px;width:7px;height:14px;border:solid #fff;border-width:0 3px 3px 0;background:none;content:"";box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;transform:translate(0, -1px) scale(0) rotate(45deg);-webkit-transform:translate(0, -1px) scale(0) rotate(45deg);-ms-transform:translate(0, -1px) scale(0) rotate(45deg)}.checkbox>input[type=checkbox]:checked+label.checkmark::after{transform:translate(0, -1px) scale(1) rotate(45deg);-webkit-transform:translate(0, -1px) scale(1) rotate(45deg);-ms-transform:translate(0, -1px) scale(1) rotate(45deg)}.checkbox>input[type=checkbox]+label.checkmark::before{border-color:#009688;border-color:var(--var-color-control-accent, #009688)}.checkbox>input[type=checkbox]:checked+label.checkmark::before{background:#009688;background:var(--var-color-control-accent, #009688)}.checkbox>input[type=checkbox]:hover+label.checkmark::before,.checkbox>input[type=checkbox]:focus+label.checkmark::before{border-color:#26a69a;border-color:var(--var-color-control-accent-hover, #26a69a)}.checkbox>input[type=checkbox]:hover:checked+label.checkmark::before,.checkbox>input[type=checkbox]:focus:checked+label.checkmark::before{background:#26a69a;background:var(--var-color-control-accent-hover, #26a69a)}.checkbox>input[type=checkbox]:active+label.checkmark::before{border-color:#00897b;border-color:var(--var-color-control-accent-active, #00897b)}.checkbox>input[type=checkbox]:active:checked+label.checkmark::before{background:#00897b;background:var(--var-color-control-accent-active, #00897b)}.checkbox>input[type=checkbox]:disabled+label.checkmark::before{border-color:#a5a5a5}.checkbox>input[type=checkbox]:disabled:checked+label.checkmark::before{background:#a5a5a5} 9 | .range-container{display:inline-block;position:relative;flex:1 1 0%;width:100%;min-width:15px;height:26px}.range-container input[type=range]{width:100%;min-width:128px;height:100%;margin:0;padding:0;opacity:0}.range-container input[type=range]:not(:disabled){cursor:pointer}.range-container .range-skin-container{display:flex;position:absolute;top:0;left:0;flex-flow:nowrap;width:100%;height:100%;pointer-events:none;user-select:none}.range-container .range-stub{position:relative;flex-grow:0;flex-shrink:0;width:7px}.range-container .range-progress{display:flex;flex:1;flex-flow:row nowrap}.range-container .range-progress-left{position:relative;flex-grow:0;flex-shrink:0;width:85%}.range-container .range-progress-right{position:relative;flex-grow:1}.range-container .range-bar{position:absolute;left:0;width:100%;z-index:0}.range-container .range-bar.range-bar-left{top:12px;height:3px}.range-container .range-bar.range-bar-right{top:12px;height:3px;background:#c9c9c9;background:var(--var-color-control-neutral, #c9c9c9)}.range-container .range-bar.range-stub-left{border-radius:3px 0 0 3px}.range-container .range-bar.range-stub-right{border-radius:0 3px 3px 0}.range-container .range-handle{position:absolute;top:5.5px;right:-7.5px;width:15px;height:15px;border-radius:50%;z-index:1}.range-container .range-bar-left,.range-container .range-handle{background:#009688;background:var(--var-color-control-accent, #009688)}.range-container input[type=range]:not(:disabled):hover+.range-skin-container .range-handle,.range-container input[type=range]:not(:disabled):focus+.range-skin-container .range-handle{background:#26a69a;background:var(--var-color-control-accent-hover, #26a69a)}.range-container input[type=range]:not(:disabled):active+.range-skin-container .range-handle{background:#00897b;background:var(--var-color-control-accent-active, #00897b)}.range-container input[type=range]:disabled+.range-skin-container .range-bar-left,.range-container input[type=range]:disabled+.range-skin-container .range-handle{background:#a5a5a5}.range-container .range-tooltip{position:absolute;top:-28px;right:0;min-width:24px;padding:4px;transform:translateX(50%);transition:opacity .1s ease-in-out;border-radius:4px;background:#535353;color:#eee;font-size:87.5%;text-align:center;opacity:0;z-index:2}.range-container input[type=range]:hover+.range-skin-container .range-tooltip,.range-container input[type=range]:active+.range-skin-container .range-tooltip,.range-container input[type=range]:focus+.range-skin-container .range-tooltip{opacity:1}.range-container .range-tooltip::after{position:absolute;top:100%;left:50%;width:0px;height:12px;margin-left:-6px;border-width:6px;border-style:solid;border-color:#535353 rgba(0,0,0,0) rgba(0,0,0,0);content:""} 10 | .button{position:relative;padding:8px 14px;border:none;border-radius:4px;font-size:87.5%;font-weight:bold;cursor:pointer}.button.compact{padding:6px 14px;font-size:75%}.button.flat{padding:6px 12px;border-width:2px;border-style:solid;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.button.flat.compact{padding:4px 12px}.button{background:#009688;background:var(--var-color-control-accent, #009688);color:#fff}.button:focus,.button:hover:not(:disabled){outline:0px;background:#26a69a;background:var(--var-color-control-accent-hover, #26a69a)}.button:active:not(:disabled){outline:0px;background:#00897b;background:var(--var-color-control-accent-active, #00897b)}.button:disabled{background:#a5a5a5;cursor:default}.button.flat{border-color:#009688;border-color:var(--var-color-control-accent, #009688);background:none;color:#009688;color:var(--var-color-control-accent, #009688)}.button.flat:focus,.button.flat:hover:not(:disabled){border-color:#26a69a;border-color:var(--var-color-control-accent-hover, #26a69a);background:none;color:#26a69a;color:var(--var-color-control-accent-hover, #26a69a)}.button.flat:active:not(:disabled){border-color:#00897b;border-color:var(--var-color-control-accent-active, #00897b);background:rgba(0,150,136,.1);color:#00897b;color:var(--var-color-control-accent-active, #00897b)}.button.flat:disabled{border-color:#a5a5a5;background:none;color:#a5a5a5} 11 | .tabs{display:flex;position:relative;flex-flow:row wrap;flex-grow:1;width:auto;border-radius:4px;background:none;overflow:hidden}.tabs::after{position:absolute;top:0;left:0;width:100%;height:100%;border-width:2px;border-style:solid;border-color:#c9c9c9;border-color:var(--var-color-control-neutral, #c9c9c9);border-radius:4px;content:"";z-index:1;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.tabs.compact>input+label{padding:6px 14px;font-size:75%}.tabs>input{position:absolute;top:0;left:0;width:1px;height:1px;opacity:0}.tabs>input+label{flex:1;padding:8px 14px;font-size:87.5%;font-weight:bold;text-align:center;white-space:nowrap;cursor:pointer;z-index:2}.tabs>input:disabled+label,.tabs>input[type=radio]:checked+label{cursor:default}.tabs>input+label{background:none;color:#009688;color:var(--var-color-control-accent, #009688)}.tabs>input:checked+label{background:#009688;background:var(--var-color-control-accent, #009688);color:#fff}.tabs>input:disabled+label{background:none;color:#a5a5a5}.tabs>input:disabled:checked+label{background:#a5a5a5;color:#fff}.tabs>input[type=checkbox]:not(:disabled):hover+label,.tabs>input[type=checkbox]:not(:disabled):focus+label{background:rgba(0,150,136,.05)}.tabs>input[type=checkbox]:not(:disabled):hover:checked+label,.tabs>input[type=checkbox]:not(:disabled):focus:checked+label{background:#26a69a;background:var(--var-color-control-accent-hover, #26a69a)}.tabs>input[type=checkbox]:not(:disabled):active+label{background:rgba(0,150,136,.1)}.tabs>input[type=checkbox]:not(:disabled):active:checked+label{background:#00897b;background:var(--var-color-control-accent-active, #00897b)}.tabs>input[type=radio]:not(:disabled):not(:checked):hover+label,.tabs>input[type=radio]:not(:disabled):not(:checked):focus+label{background:rgba(0,150,136,.05)}.tabs>input[type=radio]:not(:disabled):not(:checked):active+label{background:rgba(0,150,136,.1)} 12 | :root{--var-color-theme:white;--var-color-page-background:#ededed;--var-page-background-image:url("../images/noise-light.png");--var-color-block-background:#eeeeee;--var-color-block-border:1px solid #c9c9c9;--var-color-title:#535353;--var-color-text:#676767;--var-color-block-actionitem:#5e5e5e;--var-color-block-actionitem-hover:#7e7e7e;--var-color-block-actionitem-active:#535353;--var-color-scrollbar:#a5a5a5;--var-color-scrollbar-hover:#b2b2b2;--var-color-scrollbar-active:#959595;--var-color-control-neutral:#c9c9c9;--var-color-control-accent:#009688;--var-color-control-accent-hover:#26a69a;--var-color-control-accent-active:#00897b}@media(prefers-color-scheme: dark){:root{--var-color-theme:black;--var-color-page-background:#232323;--var-page-background-image:url("../images/noise-dark.png");--var-color-block-background:#202020;--var-color-block-border:1px solid #535353;--var-color-title:#eeeeee;--var-color-text:#dbdbdb;--var-color-block-actionitem:#dbdbdb;--var-color-block-actionitem-hover:#eeeeee;--var-color-block-actionitem-active:#c9c9c9;--var-color-scrollbar:#7e7e7e;--var-color-scrollbar-hover:#959595;--var-color-scrollbar-active:#676767;--var-color-control-neutral:#5e5e5e;--var-color-control-accent:#26a69a;--var-color-control-accent-hover:#4db6ac;--var-color-control-accent-active:#009688}}:root{color-scheme:light dark}html{display:flex;min-height:100%;font-family:Arial,Helvetica,sans-serif}body{display:flex;flex:1;flex-direction:column;min-height:100vh;margin:0px;background-attachment:fixed;background:#ededed;background:var(--var-color-page-background, #ededed);background-image:url("../images/noise-light.png");background-image:var(--var-page-background-image, url("../images/noise-light.png"));color:#676767;color:var(--var-color-text, #676767)}main{display:block;flex-grow:1;padding-bottom:32px}h1,h2,h3{color:#535353;color:var(--var-color-title, #535353)} 13 | .badge{width:32px;height:32px;margin:8px 12px;border:none}.badge>svg{width:32px;height:32px}.badge,.badge:hover,.badge:focus,.badge:active{border:none}.badge svg{fill:#5e5e5e;fill:var(--var-color-block-actionitem, #5e5e5e)}.badge svg:focus,.badge svg:hover{fill:#7e7e7e;fill:var(--var-color-block-actionitem-hover, #7e7e7e)}.badge svg:active{fill:#535353;fill:var(--var-color-block-actionitem-active, #535353)}.badge-shelf{display:flex;flex-flow:row;justify-content:center}footer{align-items:center;padding:8px;text-align:center;border-top:1px solid #c9c9c9;border-top:var(--var-color-block-border, 1px solid #c9c9c9);background:#eeeeee;background:var(--var-color-block-background, #eeeeee)} 14 | -------------------------------------------------------------------------------- /docs/script/main.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";var e={324:function(e,t,r){var n,i=this&&this.__extends||(n=function(e,t){return n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])},n(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function r(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}),a=this&&this.__createBinding||(Object.create?function(e,t,r,n){void 0===n&&(n=r);var i=Object.getOwnPropertyDescriptor(t,r);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,n,i)}:function(e,t,r,n){void 0===n&&(n=r),e[n]=t[r]}),o=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),u=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)"default"!==r&&Object.prototype.hasOwnProperty.call(e,r)&&a(t,e,r);return o(t,e),t},s=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var l=s(r(72)),c=r(753),f=s(r(771)),d=u(r(748)),h=s(r(62)),p=s(r(61));r(457);var b=function(e){function t(){var t=e.call(this,c.gl)||this;t._FBO=new l.default(c.gl,512,512),t._vbo=h.default.createQuad(c.gl,-1,-1,1,1);var r=function(){var e=Page.Canvas.getSize();t.initializeTextures(e[0],e[1]),t.recomputeVisibleSubTexture()};t._needToRecomputeShader=!0,t._mustClear=!0,t._textures=[null,null],t._visibleSubTexture=[0,0,1,1],r(),Page.Canvas.Observers.mouseDrag.push((function(e,r){t._visibleSubTexture[0]-=e*t._visibleSubTexture[2],t._visibleSubTexture[1]-=r*t._visibleSubTexture[3],t.recomputeVisibleSubTexture(),t._needToRedraw=!0,t._mustClear=!0})),Page.Canvas.Observers.canvasResize.push(r),p.default.resetObservers.push(r),p.default.rulesObservers.push((function(){return t._needToRecomputeShader=!0}));var n=p.default.scale;return p.default.scaleObservers.push((function(e,r){t._needToRedraw=!0,t._mustClear=!0,t._visibleSubTexture[0]+=r[0]*(1-n/e)*t._visibleSubTexture[2],t._visibleSubTexture[1]+=r[1]*(1-n/e)*t._visibleSubTexture[3],n=e,t.recomputeVisibleSubTexture()})),d.buildShader({fragmentFilename:"display-2D.frag",vertexFilename:"display-2D.vert",injected:{}},(function(e){null!==e&&(t._displayShader=e,t._displayShader.a.aCorner.VBO=t._vbo,t._displayShader.u.uSubTexture.value=t._visibleSubTexture)})),t}return i(t,e),t.prototype.freeGLResources=function(){this._FBO&&this._FBO.freeGLResources(),this._vbo&&(this._vbo.freeGLResources(),this._vbo=null),this._displayShader&&(this._displayShader.freeGLResources(),this._displayShader=null),this._updateShader&&(this._updateShader.freeGLResources(),this._updateShader=null),this.freeTextures()},t.prototype.update=function(){this._needToRecomputeShader&&this.recomputeUpdateShader();var e=this._updateShader;if(e){var t=this._textures[this._currentIndex],r=this._textures[(this._currentIndex+1)%2];this._FBO.bind([r]),e.u.uPrevious.value=t,e.u.uPhysicalCellSize.value=[1/this._textureSize[0],1/this._textureSize[1]],e.use(),e.bindUniformsAndAttributes(),c.gl.disable(c.gl.BLEND),c.gl.drawArrays(c.gl.TRIANGLE_STRIP,0,4),this._currentIndex=(this._currentIndex+1)%2,this._iteration++}},t.prototype.draw=function(){var e=this._displayShader;e&&(e.u.uClearFactor.value=this._mustClear?1:1-p.default.persistence,e.u.uTexture.value=this._textures[this._currentIndex],e.u.uGridSize.value=this._gridSize,e.use(),e.bindUniformsAndAttributes(),c.gl.enable(c.gl.BLEND),c.gl.drawArrays(c.gl.TRIANGLE_STRIP,0,4),this._needToRedraw=!1,this._mustClear=!1)},Object.defineProperty(t.prototype,"iteration",{get:function(){return this._iteration},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"needToRedraw",{get:function(){return this._needToRedraw},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"needToUpdate",{get:function(){return this._needToRecomputeShader},enumerable:!1,configurable:!0}),t.prototype.recomputeUpdateShader=function(){var e=this;d.buildShader({fragmentFilename:"update-2D.frag",vertexFilename:"update-2D.vert",injected:{rules:this.generateShaderRules()}},(function(t){null!==t&&(e._updateShader&&e._updateShader.freeGLResources(),e._updateShader=t,e._updateShader.a.aCorner.VBO=e._vbo)})),this._needToRecomputeShader=!1},t.prototype.generateShaderRules=function(){function e(e,t,r){if("alive"!==r){var n=void 0;return 0===e?(n="step(N, "+(t+.5)+")",8===t&&(n="1.0")):n=8===t?"step("+(e-.5)+", N)":"step("+(e-.5)+", N) * step(N, "+(t+.5)+")","currentState"+("death"===r?" -= ":" += ")+n+";\n"}return""}for(var t="",r=p.default.rules,n=r[0],i=0,a=1;a<9;++a)r[a]!==n&&(t+=e(i,a-1,n),n=r[a],i=a);return t+e(i,8,n)},t.prototype.recomputeVisibleSubTexture=function(){var e=Page.Canvas.getSize();this._visibleSubTexture[2]=e[0]/this._gridSize[0]/p.default.scale,this._visibleSubTexture[3]=e[1]/this._gridSize[1]/p.default.scale;for(var t=0;t<2;++t)this._visibleSubTexture[t]-=Math.min(0,this._visibleSubTexture[t]),this._visibleSubTexture[t]-=Math.max(0,this._visibleSubTexture[t]+this._visibleSubTexture[t+2]-1)},t.prototype.freeTextures=function(){for(var e=0;e<2;++e)this._textures[e]&&(c.gl.deleteTexture(this._textures[e]),this._textures[e]=null)},t.prototype.initializeTextures=function(e,t){function r(e){return Math.pow(2,Math.ceil(Math.log(e)*Math.LOG2E))}e=r(e),t=r(t),this._gridSize=[e,t];var n=e/4,i=t/4;this._FBO.width=n,this._FBO.height=i,this._textureSize=[n,i];for(var a=new Uint8Array(4*n*i),o=a.length-1;o>=0;--o)a[o]=Math.floor(256*Math.random());for(o=0;o<2;++o)null===this._textures[o]&&(this._textures[o]=c.gl.createTexture()),c.gl.bindTexture(c.gl.TEXTURE_2D,this._textures[o]),c.gl.texImage2D(c.gl.TEXTURE_2D,0,c.gl.RGBA,n,i,0,c.gl.RGBA,c.gl.UNSIGNED_BYTE,a),c.gl.texParameteri(c.gl.TEXTURE_2D,c.gl.TEXTURE_MAG_FILTER,c.gl.NEAREST),c.gl.texParameteri(c.gl.TEXTURE_2D,c.gl.TEXTURE_MIN_FILTER,c.gl.NEAREST);this._currentIndex=0,this._iteration=0,this._needToRedraw=!0,Page.Canvas.setIndicatorText("grid-size",e+"x"+t)},t}(f.default);t.default=b},72:function(e,t,r){var n,i=this&&this.__extends||(n=function(e,t){return n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])},n(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function r(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}),a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var o=function(e){function t(t,r,n){var i=e.call(this,t)||this;return i.id=t.createFramebuffer(),i.width=r,i.height=n,i}return i(t,e),t.bindDefault=function(e,t){void 0===t&&(t=null),e.bindFramebuffer(e.FRAMEBUFFER,null),null===t?e.viewport(0,0,e.drawingBufferWidth,e.drawingBufferHeight):e.viewport(t.left,t.lower,t.width,t.height)},t.prototype.bind=function(t,r){void 0===r&&(r=null);var n=e.prototype.gl.call(this);n.bindFramebuffer(n.FRAMEBUFFER,this.id),n.viewport(0,0,this.width,this.height);for(var i=0;id.default.updateWaitTime;f&&(a=o,t.update(),n=!1),(f||t.needToRedraw)&&(u.default.bindDefault(l.gl),e&&(s.adjustSize(),e=!1),c.default.setFullCanvas(l.gl),t.draw(),i&&(i=!1,Page.Canvas.showLoader(!1))),requestAnimationFrame(r)}))}}()},457:function(){},61:function(e,t,r){var n;Object.defineProperty(t,"__esModule",{value:!0}),r(457),function(e){e.DEATH="death",e.ALIVE="alive",e.BIRTH="birth"}(n||(n={}));var i=[n.DEATH,n.DEATH,n.ALIVE,n.BIRTH,n.DEATH,n.DEATH,n.DEATH,n.DEATH,n.DEATH],a=i.slice();function o(e){var t="neighbours-tabs-".concat(e);a[e]===n.DEATH?Page.Tabs.setValues(t,["death"]):a[e]===n.ALIVE?Page.Tabs.setValues(t,["alive"]):a[e]===n.BIRTH&&Page.Tabs.setValues(t,["alive","birth"]),Page.Tabs.storeState(t)}var u=[];function s(){for(var e=0,t=u;e=0?a[t]=n.DEATH:a[t]!==n.ALIVE&&e.indexOf(n.ALIVE)>=0?a[t]=n.ALIVE:a[t]!==n.BIRTH&&e.indexOf(n.BIRTH)>=0&&(a[t]=n.BIRTH),o(t),function(){if("undefined"!=typeof URLSearchParams){var e=new URLSearchParams(window.location.search);e.set(c,"true");var t=e.toString(),r=window.location.origin+window.location.pathname+(t?"?".concat(t):"");window.history.replaceState("","",r)}for(var n=0;n<9;n++){var i="neighbours-tabs-".concat(n);Page.Tabs.storeState(i)}}(),r!==a[t]&&s()})),e){var i=Page.Tabs.getValues(r);i.indexOf(n.BIRTH)>=0?a[t]=n.BIRTH:i.indexOf(n.ALIVE)>=0?a[t]=n.ALIVE:a[t]=n.DEATH}o(t)},r=0;r<9;++r)t(r);s()})),Page.Button.addObserver("reset-rules-button-id",(function(){for(var e=!1,t=0;t<9;t++)e=e||a[t]!==i[t],a[t]=i[t],o(t),Page.Tabs.clearStoredState("neighbours-tabs-".concat(t));!function(){if("undefined"!=typeof URLSearchParams){var e=new URLSearchParams(window.location.search);e.delete(c);var t=e.toString(),r=window.location.origin+window.location.pathname+(t?"?".concat(t):"");window.history.replaceState("","",r)}}(),e&&s()}));var f,d="autorun-checkbox-id";Page.Checkbox.addObserver(d,(function(e){l=e})),l=Page.Checkbox.isChecked(d);var h=[1e3,500,200,1e3/11,1e3/31,0],p="speed-range-id";Page.Range.addObserver(p,(function(e){f=e})),f=Page.Range.getValue(p);var b=[];Page.Button.addObserver("next-button-id",(function(){for(var e=0,t=b;e