├── .gitignore ├── LICENSE ├── README.md ├── images └── anim.gif ├── noise.js ├── package.json ├── wireframe_world.js └── www ├── demo.html └── demo.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules/* 16 | *.DS_Store 17 | 18 | *.mp4 19 | *.jpg 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2016 Eric Arnebäck 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wireframe-world 2 | 3 | This demo draws an infinite, vaporwave-like world using the WebGL 4 | framework [regl](https://github.com/mikolalysenko/regl). A link to the 5 | demo is [here](http://erkaman.github.io/wireframe-world/www/demo.html). It should look 6 | like the below: 7 | 8 | ![Animated](images/anim.gif) 9 | 10 | And click below for a longer video of the demo: 11 | 12 | [![Result](http://img.youtube.com/vi/tE9geTQxgZc/0.jpg)](https://www.youtube.com/watch?v=tE9geTQxgZc) 13 | 14 | 15 | ## Implementation Details 16 | 17 | As for the implementation, it is not very difficult stuff; I divide up 18 | the world into chunks(just like in Minecraft), and as the camera 19 | traverses the world, the chunks that become out of range are thrown 20 | away and are no longer rendered. And in the far away horizon I keep adding 21 | new chunks, to give the illusion that the world is infinite. 22 | 23 | ## Build 24 | 25 | To run the demo locally on your computer, first change your directory to the directory of the project, then run 26 | 27 | ```bash 28 | npm install 29 | ``` 30 | 31 | To then run the demo, do 32 | 33 | ```bash 34 | npm start 35 | ``` 36 | 37 | 38 | ## TODO 39 | 40 | Port the program into screensavers for OS X, Windows and Linux. 41 | -------------------------------------------------------------------------------- /images/anim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erkaman/wireframe-world/d1a305dae50f45125ac152b7bc76ffba0c095667/images/anim.gif -------------------------------------------------------------------------------- /noise.js: -------------------------------------------------------------------------------- 1 | /* 2 | This code was taken from here: 3 | https://github.com/josephg/noisejs 4 | */ 5 | 6 | function Grad(x, y, z) { 7 | this.x = x; this.y = y; this.z = z; 8 | } 9 | 10 | Grad.prototype.dot2 = function(x, y) { 11 | return this.x * x + this.y * y; 12 | }; 13 | 14 | var grad3 = [new Grad(1, 1, 0), new Grad(-1, 1, 0), new Grad(1, -1, 0), new Grad(-1, -1, 0), 15 | new Grad(1, 0, 1), new Grad(-1, 0, 1), new Grad(1, 0, -1), new Grad(-1, 0, -1), 16 | new Grad(0, 1, 1), new Grad(0, -1, 1), new Grad(0, 1, -1), new Grad(0, -1, -1)]; 17 | 18 | var p = [151, 160, 137, 91, 90, 15, 19 | 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 20 | 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 21 | 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 22 | 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 23 | 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 24 | 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 25 | 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 26 | 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 27 | 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 28 | 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 29 | 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 30 | 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180]; 31 | // To remove the need for index wrapping, double the permutation table length 32 | var perm = new Array(512); 33 | var gradP = new Array(512); 34 | 35 | // This isn't a very good seeding function, but it works ok. It supports 2^16 36 | // different seed values. Write something better if you need more seeds. 37 | seed = function(seed) { 38 | if(seed > 0 && seed < 1) { 39 | // Scale the seed out 40 | seed *= 65536; 41 | } 42 | 43 | seed = Math.floor(seed); 44 | if(seed < 256) { 45 | seed |= seed << 8; 46 | } 47 | 48 | for(var i = 0; i < 256; i++) { 49 | var v; 50 | if (i & 1) { 51 | v = p[i] ^ (seed & 255); 52 | } else { 53 | v = p[i] ^ ((seed>>8) & 255); 54 | } 55 | 56 | perm[i] = perm[i + 256] = v; 57 | gradP[i] = gradP[i + 256] = grad3[v % 12]; 58 | } 59 | }; 60 | 61 | seed(0); 62 | 63 | function fade(t) { 64 | // return t * t * t * (t * (t * 6-15)+10); 65 | return t 66 | } 67 | 68 | function lerp(a, b, t) { 69 | return (1-t) * a + t * b; 70 | } 71 | 72 | // 2D Perlin Noise 73 | module.exports = function(x, y) { 74 | // Find unit grid cell containing point 75 | var X = Math.floor(x), Y = Math.floor(y); 76 | // Get relative xy coordinates of point within that cell 77 | x = x - X; y = y - Y; 78 | // Wrap the integer cells at 255 (smaller integer period can be introduced here) 79 | X = X & 255; Y = Y & 255; 80 | 81 | // Calculate noise contributions from each of the four corners 82 | var n00 = gradP[X+perm[Y]].dot2(x, y); 83 | var n01 = gradP[X+perm[Y+1]].dot2(x, y-1); 84 | var n10 = gradP[X+1+perm[Y]].dot2(x-1, y); 85 | var n11 = gradP[X+1+perm[Y+1]].dot2(x-1, y-1); 86 | 87 | // Compute the fade curve value for x 88 | var u = fade(x); 89 | 90 | // Interpolate the four results 91 | return lerp( 92 | lerp(n00, n10, u), 93 | lerp(n01, n11, u), 94 | fade(y)); 95 | }; 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wireframe-world", 3 | "version": "0.1.0", 4 | "description": "Demo that draws an infinite world", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "budo": "^8.3.0", 12 | "canvas-fit": "^1.5.0", 13 | "canvas-orbit-camera": "^1.0.2", 14 | "gl": "^4.0.2", 15 | "gl-camera-pos-from-view-matrix": "^1.0.1", 16 | "gl-mat4": "^1.1.4", 17 | "regl": "^0.10.0", 18 | "regl-recorder": "^0.2.0", 19 | "standard": "^7.1.2" 20 | }, 21 | "scripts": { 22 | "example": "node example/index.js", 23 | "test": "standard", 24 | "start": "budo --live --open wireframe_world.js" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/Erkaman/wireframe-world.git" 29 | }, 30 | "keywords": [ 31 | "movie", 32 | "recorder", 33 | "animation", 34 | "regl" 35 | ], 36 | "author": "Eric Arnebäck", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/Erkaman/wireframe-world/issues" 40 | }, 41 | "homepage": "https://github.com/Erkaman/wireframe-world" 42 | } 43 | -------------------------------------------------------------------------------- /wireframe_world.js: -------------------------------------------------------------------------------- 1 | const canvas = document.body.appendChild(document.createElement('canvas')) 2 | const fit = require('canvas-fit') 3 | 4 | var str = `Fork me on GitHub` 5 | 6 | const regl = require('regl')({ 7 | canvas: canvas, 8 | onDone: (err, regl) => { 9 | if (err) { 10 | document.body.innerHTML = ` 11 | Failed to initialize the demo because:

12 | ` + err + '

' + 13 | ` 14 | But you can find a recorded gif of the demo by clicking here. 15 | ` 16 | throw err 17 | } 18 | } 19 | }) 20 | 21 | var container = document.createElement('div') 22 | container.innerHTML = str 23 | document.body.appendChild(container) 24 | 25 | window.addEventListener('resize', fit(canvas), false) 26 | 27 | const mat4 = require('gl-mat4') 28 | const noise2 = require('./noise.js') 29 | var cameraPosFromViewMatrix = require('gl-camera-pos-from-view-matrix') 30 | 31 | // projection matrix settings. 32 | const Z_FAR = 120000 33 | const Z_NEAR = 0.01 34 | const FOV = Math.PI / 4 35 | 36 | // these variables are used all over the place. Declare them here, 37 | // once and for all. 38 | var x 39 | var z 40 | var y 41 | var r 42 | var i 43 | 44 | function makeWireframeTexture () { 45 | var texData = [] 46 | 47 | // 48 | // make base image, 49 | // 50 | var lw = 10 // line width 51 | for (y = 0; y < 256; y++) { 52 | r = [] 53 | for (x = 0; x < 256; x++) { 54 | if (y < lw || y > (256 - lw) || x < lw || x > (256 - lw)) { 55 | r.push([255, 255, 255, 255]) 56 | } else { 57 | r.push([0, 0, 0, 255]) 58 | } 59 | } 60 | texData.push(r) 61 | } 62 | 63 | // 64 | // do box filter blur on the base image: 65 | // 66 | var tempTexData = [] 67 | for (y = 0; y < 256; y++) { 68 | r = [] 69 | for (x = 0; x < 256; x++) { 70 | var c = [0, 0, 0, 0] 71 | 72 | for (var ax = -3; ax <= +3; ax++) { 73 | for (var ay = -3; ay <= +3; ay++) { 74 | var wy = y + ay 75 | var wx = x + ax 76 | 77 | if (wy < 0 || wx < 0 || wy > 255 || wx > 255) { 78 | // avoid out-of-range access. 79 | continue 80 | } 81 | 82 | var d = texData[wy][wx] 83 | c = [ 84 | c[0] + d[0], 85 | c[1] + d[1], 86 | c[2] + d[2], 87 | c[3] + d[3] 88 | ] 89 | } 90 | } 91 | 92 | var u = 49.0 93 | r.push([c[0] / u, c[1] / u, c[2] / u, c[3] / u]) 94 | } 95 | tempTexData.push(r) 96 | } 97 | texData = tempTexData 98 | 99 | return texData 100 | } 101 | 102 | // lerp between two colors 103 | function lerp (c0, c1, x) { 104 | return [ 105 | c1[0] * x + c0[0] * (1.0 - x), 106 | c1[1] * x + c0[1] * (1.0 - x), 107 | c1[2] * x + c0[2] * (1.0 - x), 108 | c1[3] * x + c0[3] * (1.0 - x) 109 | ] 110 | } 111 | 112 | function makeSunTexture () { 113 | var texData = [] 114 | 115 | // the color of the circle is based on this palette. 116 | // and the palette uses the distance from the center to 117 | // smoothly interpolate between colors. 118 | var palette = [ 119 | [0.0, [246.0, 125.0, 202.0, 255.0]], 120 | [0.6, [247.0, 27.0, 111.0, 255.0]], 121 | [0.9, [247.0, 27.0, 111.0, 255.0]], 122 | [1.0, [0.0, 0.0, 0.0, 255.0]] 123 | ] 124 | 125 | for (y = 0; y < 256; y++) { 126 | r = [] // row of pixel data 127 | for (x = 0; x < 256; x++) { 128 | // convert (x,y) to range [-1, +1] 129 | var ox = (x - 128) / 127 130 | var oy = (y - 128) / 127 131 | 132 | var R = Math.sqrt(ox * ox + oy * oy) // distance from center. 133 | var c 134 | 135 | if (R >= 1.0) { 136 | c = [0.0, 0.0, 0.0, 0.0] 137 | } else { 138 | var ip 139 | // find the two colors in the palette, which we should 140 | // interpolate between. 141 | for (ip = 0; ip < palette.length - 1; ip++) { 142 | if (palette[ip][0] <= R && palette[ip + 1][0] >= R) { 143 | break 144 | } 145 | } 146 | 147 | var c0 = palette[ip + 0] 148 | var c1 = palette[ip + 1] 149 | c = lerp(c0[1], c1[1], (R - c0[0]) / (c1[0] - c0[0])) 150 | } 151 | r.push(c) 152 | } 153 | texData.push(r) 154 | } 155 | 156 | return texData 157 | } 158 | 159 | const elements = [] // faces 160 | var texCoord = [] // texCoords 161 | 162 | const H = 80 // number of squares on the height 163 | const W = 60 // number of squares on the width 164 | 165 | var size = 100.0 // the sidelength of a square. 166 | var xmin = -(W / 2.0) * size 167 | var xmax = +(W / 2.0) * size 168 | var zmin = -(H / 2.0) * size 169 | var zmax = +(H / 2.0) * size 170 | 171 | var row 172 | var col 173 | 174 | function Chunk () { 175 | this.position = [] 176 | this.positionBuffer = regl.buffer({ 177 | length: (H + 1) * (W + 1) * 3 * 4, 178 | type: 'float', 179 | usage: 'dynamic' 180 | }) 181 | } 182 | var chunkPool = [] 183 | function freeChunk (chunk) { 184 | chunkPool.push(chunk) 185 | } 186 | 187 | // every time we add a new chunk, we increment this number. 188 | // it is used to determine the z-position of the chunk. 189 | var N = 0 190 | 191 | function makeChunk () { 192 | // retrieve chunk from the pool, or create one if necessary. 193 | var chunk = chunkPool.pop() || new Chunk() 194 | 195 | var j = 0 196 | for (row = 0; row <= H; ++row) { 197 | z = (row / H) * (zmax - zmin) + zmin 198 | // If N==0, then this is the first chunk that we see. 199 | // If N==1, it is the second chunk that we see, and so on. 200 | z += (zmax - zmin) * -N 201 | 202 | for (col = 0; col <= W; ++col) { 203 | x = (col / W) * (xmax - xmin) + xmin 204 | 205 | var f = 0.0015974 206 | var amp = 100.0 207 | var n = 0 208 | 209 | // FBM of two octaves. 210 | for (var i = 0; i < 2; i++) { 211 | n += amp * noise2(x * f, z * f) 212 | 213 | amp *= 6.0 214 | f *= 0.5 215 | } 216 | 217 | // make the terrain less smooth looking. 218 | y = Math.round(n / 60) * 60 219 | 220 | chunk.position[j++] = [x, y, z] 221 | } 222 | } 223 | // upload vertex data to the GPU. 224 | chunk.positionBuffer.subdata(chunk.position) 225 | 226 | chunk.N = N 227 | 228 | N++ 229 | return chunk 230 | } 231 | 232 | // render distance of chunks. 233 | var RENDER_N = 10 234 | var chunks = [] 235 | 236 | // create all the chunks we need. 237 | for (i = 0; i < RENDER_N; i++) { 238 | chunks[i] = makeChunk() 239 | } 240 | 241 | // create texCoords. 242 | for (row = 0; row <= H; ++row) { 243 | z = (row) 244 | for (col = 0; col <= W; ++col) { 245 | x = (col) 246 | texCoord.push([x, z]) 247 | } 248 | } 249 | 250 | // create faces. 251 | for (row = 0; row <= (H - 1); ++row) { 252 | for (col = 0; col <= (W - 1); ++col) { 253 | i = row * (W + 1) + col 254 | 255 | var i0 = i + 0 256 | var i1 = i + 1 257 | var i2 = i + (W + 1) + 0 258 | var i3 = i + (W + 1) + 1 259 | 260 | elements.push([i3, i1, i0]) 261 | elements.push([i0, i2, i3]) 262 | } 263 | } 264 | 265 | // this global scope encapsulates all state common to all drawCommands. 266 | const globalScope = regl({ 267 | uniforms: { 268 | projection: ({viewportWidth, viewportHeight}) => { 269 | return mat4.perspective([], FOV, viewportWidth / viewportHeight, Z_NEAR, Z_FAR) 270 | } 271 | }, 272 | cull: { 273 | enable: true 274 | } 275 | }) 276 | 277 | // encapsulates state needed for drawing chunks. 278 | const chunkScope = regl({ 279 | uniforms: { 280 | view: (_, props) => props.view, 281 | 282 | tex: regl.texture({ 283 | min: 'linear mipmap linear', 284 | mag: 'linear', 285 | wrap: 'repeat', 286 | data: makeWireframeTexture() 287 | }), 288 | cameraPos: (_, props) => { 289 | return cameraPosFromViewMatrix([], props.view) 290 | }, 291 | tick: ({tick}) => tick 292 | }, 293 | 294 | frag: ` 295 | precision mediump float; 296 | 297 | varying vec2 vTexCoord; 298 | varying vec3 vPosition; 299 | 300 | uniform sampler2D tex; 301 | uniform vec3 cameraPos; 302 | uniform float tick; 303 | 304 | void main () { 305 | vec3 d = vec3( 306 | (sin(tick*0.02 + 0.0) + 1.0) * 0.5 + 0.5, 307 | (sin(tick*0.02 + 2.0) + 1.0) * 0.5 + 0.5, 308 | (sin(tick*0.01 + 4.0) + 1.0) * 0.5 + 0.5 309 | ); 310 | 311 | vec3 c = texture2D(tex, vTexCoord).x * d; 312 | gl_FragColor = vec4(c.xyz, 1.0); 313 | }`, 314 | vert: ` 315 | precision mediump float; 316 | 317 | attribute vec3 position; 318 | attribute vec2 texCoord; 319 | 320 | varying vec2 vTexCoord; 321 | varying vec3 vPosition; 322 | 323 | uniform mat4 projection, view; 324 | uniform vec3 cameraPos; 325 | 326 | void main() { 327 | vTexCoord = texCoord; 328 | vPosition = position.xyz; 329 | 330 | float dist = distance(cameraPos.xz, vPosition.xz); 331 | float curveAmount = 0.3; 332 | 333 | // we lower all vertices down a bit, to create a slightly curved horizon. 334 | gl_Position = projection * view * vec4(position - vec3(0.0, dist*curveAmount * 0.0, 0.0), 1); 335 | }`, 336 | 337 | attributes: { 338 | texCoord: texCoord 339 | }, 340 | elements: elements 341 | }) 342 | 343 | const drawSun = regl({ 344 | uniforms: { 345 | view: (_, props) => { 346 | var m = mat4.copy([], props.view) 347 | // the sun should always stay where it is, so do this: 348 | m[12] = 0 349 | m[13] = 0 350 | m[14] = 0 351 | return m 352 | }, 353 | tex: regl.texture({ 354 | data: makeSunTexture(), 355 | mag: 'linear' 356 | }) 357 | }, 358 | 359 | frag: ` 360 | precision mediump float; 361 | 362 | varying vec2 vTexCoord; 363 | 364 | uniform sampler2D tex; 365 | 366 | void main () { 367 | gl_FragColor = vec4(texture2D(tex, vTexCoord).xyz, 1.0); 368 | }`, 369 | vert: ` 370 | precision mediump float; 371 | 372 | attribute vec3 position; 373 | attribute vec2 texCoord; 374 | 375 | uniform mat4 projection, view; 376 | uniform vec3 cameraPos; 377 | 378 | varying vec2 vTexCoord; 379 | 380 | void main() { 381 | vec3 q = position; 382 | // scale and translate the sun: 383 | q += vec3(0.0, 0.1, 0.0); 384 | q *= vec3(vec2(0.4), -1.0); 385 | vec4 p = view * vec4(q, 1); 386 | 387 | vTexCoord = texCoord; 388 | gl_Position = projection * p; 389 | }`, 390 | 391 | attributes: { 392 | position: [ 393 | [-0.5, -0.5, 1.0], 394 | [+0.5, -0.5, 1.0], 395 | [+0.5, +0.5, 1.0], 396 | 397 | [+0.5, +0.5, 1.0], 398 | [-0.5, +0.5, 1.0], 399 | [-0.5, -0.5, 1.0] 400 | ], 401 | texCoord: [ 402 | [0.0, 0.0], 403 | [1.0, 0.0], 404 | [1.0, 1.0], 405 | 406 | [1.0, 1.0], 407 | [0.0, 1.0], 408 | [0.0, 0.0] 409 | ] 410 | }, 411 | 412 | count: 6, 413 | depth: { 414 | enable: false // the sun will be behind everything else. 415 | } 416 | }) 417 | 418 | // used for drawing a single chunk. 419 | const drawChunk = regl({ 420 | attributes: { 421 | position: regl.prop('pos') 422 | } 423 | }) 424 | 425 | // make sure that we actually upload all the vertex-data before starting. 426 | regl._gl.flush() 427 | regl._gl.finish() 428 | 429 | regl.frame(({tick, viewportWidth, viewportHeight}) => { 430 | regl.clear({color: [0.0, 0.0, 0.0, 1.0], depth: 1}) 431 | 432 | // create a moving camera. 433 | var view = [] 434 | var speed = 40.0 435 | var startZ = 5100 436 | var down = -1000 437 | var cameraPos = [0, 410, startZ - tick * speed] 438 | mat4.lookAt(view, cameraPos, [0, down, -startZ - tick * speed], [0, 1, 0]) 439 | 440 | globalScope(() => { 441 | drawSun({view: view}) 442 | 443 | chunkScope({view: view}, () => { 444 | for (i = 0; i < chunks.length; i++) { 445 | drawChunk({pos: {buffer: chunks[i].positionBuffer}}) 446 | } 447 | }) 448 | }) 449 | 450 | // If the first chunk can't be seen anymore, remove it. 451 | // Then way back in the horizon we place a new chunk, 452 | // so that the world goes on forever. 453 | if (chunks.length > 0) { 454 | z = zmin + (zmax - zmin) * -chunks[0].N 455 | if (cameraPos[2] < z) { 456 | freeChunk(chunks.shift()) 457 | chunks.push(makeChunk()) 458 | } 459 | } 460 | }) 461 | -------------------------------------------------------------------------------- /www/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Infinite Wireframe 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------