Prev:WEB 4 Top:WEB 0 Next:WEB 6
Jon Breuer - September 13, 2024.
I think my implementation of WEB has enough features for some serious use cases and I'm tired of chasing the bootstrapping oroboros with each version of this tool. With this article, I'm going to show how to build a little Javascript game.
This is the source *.WEB file, and this is the generated HTML.
I've recently seen some cool Voronoi cell (wiki) games and videos and thought it would be fun to build one.
Here's a picture from Wikipedia:
We're going to need an HTML canvas element and to render some stuff into it.
Starting HTML
<canvas width="100" height="100" style="border:1px solid black"></canvas> <script src="literate_programming_5.js"></script>
We'll be using Javascript to draw into it. Grab the drawable 2d context on the canvas and fill a rect.
Basic drawing on canvas
let canvas = document.getElementById("canvas1").getContext("2d") canvas.fillStyle="blue" canvas.rect(10,10,75,57); canvas.fill();
Points Now let's create some points on the canvas. For some purposes I'll treat these as 2D lying in the plane of the canvas/screen and for later uses I'll treat them in 3D extending above and below the screen. [3D makes this all around slower. I needed more points to saturate the cube instead of the plane. There's an interesting effect of some cells appearing and disappearing, but it's not significant.] Point List Definition let VoronoiPoints = [] for(let i = 0; i < 20; i++) { let point = { x: Math.random() * CANVAS_WIDTH, y: Math.random() * CANVAS_HEIGHT, //z: Math.random() * CANVAS_HEIGHT, } VoronoiPoints.push( point) Future Point Initialization } Store off the size of the screen so I don't have to interrogate the canvas every time I draw. I've also gotten fond of working in TURNS of a circle instead of 2 * PI radians or 1 Taus. Useful Constants let CANVAS_WIDTH = 500 let CANVAS_HEIGHT = 500 let ONE_TURN = Math.PI * 2 Fill the background of the canvas canvas.fillStyle="#008" canvas.rect(0,0,CANVAS_WIDTH,CANVAS_HEIGHT); canvas.fill(); Plot each point as a little filled circle for(let i in VoronoiPoints) { let point = VoronoiPoints[i] canvas.beginPath(); canvas.fillStyle = point.color || "#800" canvas.moveTo(point.x, point.y); canvas.arc(point.x, point.y, 5, 0, ONE_TURN); canvas.fill(); } And tie it all together. Draw Points As Basic Circles Fill the background of the canvas Plot each point as a little filled circle Animated Points To create an animation, we'll need to redraw our canvas repeatedly as we move the points. I default to setInterval( ...callback..., 30), but requestAnimationFrame(...callback...) is also popular. Set up a do_frame function to execute 30 times per second. setInterval(do_frame,30); function do_frame() { Move points around Draw Points Each Frame } For the animation to be visible, our points will need to be moving, so add a random initial velocity to each point. Future Point Initialization...+ const POINT_SPEED = 3; point.vel = { x:Math.random() * POINT_SPEED - POINT_SPEED / 2, y:Math.random() * POINT_SPEED - POINT_SPEED / 2, y:Math.random() * POINT_SPEED - POINT_SPEED / 2, } And add that velocity to the positions on each frame. Move points around for(let i in VoronoiPoints) { let point = VoronoiPoints[i] point.x += point.vel.x; point.y += point.vel.y; Keep points on screen } I also want the points to be different colors. Future Point Initialization...+ point.color = "#" + Math.floor(Math.random()*10) + Math.floor(Math.random()*10) + Math.floor(Math.random()*10) + "" If we just let the points move with a given velocity, they'd quickly run off the edges of the canvas. Use the math modulo operation to keep them all between 0 and 500. Keep points on screen point.x = (point.x + CANVAS_WIDTH) % CANVAS_WIDTH; point.y = (point.y + CANVAS_HEIGHT) % CANVAS_HEIGHT; Create a function to draw the points and call that from our callback. Define Draw Points Function function draw_points(canvas) { Draw Points As Basic Circles } Draw Points Each Frame canvas = document.getElementById("canvas3").getContext("2d") if(canvas.__is_visible != false) { draw_points(canvas) } Voronoi Grid Now that we can see where our initial points are, let's generate a Voronoi grid from our points. A Voronoi grid is generated by coloring all the points on the screen with the color of the nearest control point. At each pixel, find the control point that's the nearest. Initialize the nearest control point let bestPoint = VoronoiPoints[0]; let bestDistSq = squared(bestPoint.x-x) + squared(bestPoint.y-y) // + squared(bestPoint.z-RENDER_Z); Loop over all the control points, calculating the new distance for(let i in VoronoiPoints) { let point = VoronoiPoints[i]; let distSq = squared(point.x-x) + squared(point.y-y) //+ squared(point.z-RENDER_Z) If the new control point is closer to our pixel than the previous control point, record the new best match. if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } The control points are floating through 3D space, but we're rendering a slice at a specific Z level. Useful Constants...+ const RENDER_Z = 200; Iterate over every point in the canvas. I already know this process can be slow, so I'm skipping over a grid of 4x4 to simplify the calculations. Voronoi Grid Iteration Start Performance Timing const CELL_SIZE = 4; for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { Initialize the nearest control point Loop over all the control points, calculating the new distance If the new control point is closer to our pixel than the previous control point, record the new best match. Draw the grid cell } } End Performance Timing Draw the grid cell canvas.fillStyle=bestPoint.color; canvas.fillRect(x, y, CELL_SIZE, CELL_SIZE); Add our Voronoi Grid draw code to the setInterval do_frame callback. Draw Points Each Frame...+ { canvas = document.getElementById("canvas4").getContext("2d") const z = 200; if(canvas.__is_visible != false) { Voronoi Grid Iteration } } Utility Functions...+ function squared(value) { return value * value; } Optimized Voronoi Grid This didn't work as well as I hoped. The intricacy of the Voronoi grid comes from the fine variation of possible control points against pixels. The outer loop of the optimized form doesn't really change from the previous form. Draw Voronoi Grid Optimized Start Performance Timing const CELL_SIZE = 20; const SMALL_CELL_SIZE = 2; distance to point helper function for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { let cx = x + CELL_SIZE / 2; for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { let cy = y + CELL_SIZE / 2; Attempt to render a chunky pixel more quickly and more accurately. } } End Performance Timing What I'm going to rely on for optimization is that for any chunky area, the list of closest control points doesn't change much, even if the exact closest control point does change pixel by pixel. Attempt to render a chunky pixel more quickly and more accurately. For a cell, calculate the list of nearish control points Within a single cell, calculate the actual best point per pixel. For a cell, calculate the list of nearish control points Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations Sort the points based on the pre-calculated distances. Limit the control points to only the very nearest points. The pythagorean therom for distance between two points is the square root of (The sum of (The squares of (the distance between points))). It's a game programming trick that when comparing distances, choosing to compare all the distances or all the distances squared doesn't matter because the relative distances still sort the same. So calculate the distance squared to skip the expensive and unneccessary square root function. distance to point helper function function distance_to_point(x, y, point) { return squared(point.x-x) + squared(point.y-y)// + squared(point.z-RENDER_Z) } Calculate the distance from the current pixel cell to the control point and cache it in a pair point => [ distance_to_point(cx, cy, point), point ] Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations let pointsWithPrecalculatedDistances = VoronoiPoints.map( Calculate the distance from the current pixel cell to the control point and cache it in a pair ) Sort the points based on the pre-calculated distances. let sortedPoints = pointsWithPrecalculatedDistances.sort((a,b) => a[0] - b[0]); Limit the control points to only the very nearest points. sortedPoints.length = Math.min(20, sortedPoints.length); Limiting the points too far didn't help. It made everything very chunky because in three dimensions, there are more active points than I think there are. Within a single cell, calculate the actual best point per pixel. for(let xx = 0; xx < CELL_SIZE; xx += SMALL_CELL_SIZE) { for(let yy = 0; yy < CELL_SIZE; yy += SMALL_CELL_SIZE) { Calculate best control point per pixel canvas.fillStyle = bestPoint.color; canvas.fillRect(x + xx, y + yy, SMALL_CELL_SIZE, SMALL_CELL_SIZE); } } This is the same code as for the unoptimized Voronoi grid, but now we're checking fewer adjacent control points. Calculate best control point per pixel let bestPoint = sortedPoints[0][1]; let bestDistSq = distance_to_point(x + xx, y + yy, bestPoint) for(let i in sortedPoints) { let point = sortedPoints[i][1]; let distSq = distance_to_point(x + xx, y + yy, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Each Frame...+ { canvas = document.getElementById("canvas5").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Grid Optimized } } Performance Improvements I'm still not happy with the performance, so I'm adding a performance tracker to the displays. Start Performance Timing const startTime = Date.now() End Performance Timing const elapsedTime = Date.now().valueOf() - startTime.valueOf(); const text = "" + elapsedTime + "ms"; canvas.strokeStyle="black"; canvas.fillStyle="white"; canvas.font = "24px arial"; canvas.fillText(text, 0,CANVAS_HEIGHT); canvas.strokeText(text, 0,CANVAS_HEIGHT); On my PC, the chunky naive renderer runs 30ms and the optimized more detailed version runs 60-70ms. My phone is running at the "optimized" version at 380ms. I'm going to start tracking which canvas is actually visible and stop processing the ones which aren't. Even if all this is expensive or inefficient, I don't need to process things that aren't visible. Set up an Itersection Observer to check which canvas elements are visible const observer = new IntersectionObserver(entries => { entries.forEach(canvas => { canvas.__is_visible = canvas.isVisible; }) } ); const allCanvasElements = document.querySelectorAll('canvas'); allCanvasElements.forEach(canvas => observer.observe(canvas)); Per-Pixel Voronoi Grid The renderer is still too slow. Let's see if rectFill is slow and pixel rendering is faster. Utility Functions...+ function putPixel(imageData, x, y, colorRGBA) { const pixelStart = (x + y * imageData.width) * 4; for(let i = 0; i < 4; i++) { imageData.data[pixelStart + i] = colorRGBA[i]; } //imageData.data[ + 0] = (colorRGBA & 0xFF0000) >> 16; //imageData.data[(x + y * imageData.width) * 4 + 1] = (colorRGBA & 0x00FF00) >> 8; //imageData.data[(x + y * imageData.width) * 4 + 2] = colorRGBA & 0x0000FF; //imageData.data[(x + y * imageData.width) * 4 + 3] = 255;; } Utility Functions...+ function color_uint_from_string(colorString) { if(colorString.length == 4) { // "#FDA" // 17 = 16 + 1 :: F => FF , 8 => 88 (in hex) let r = parseInt(colorString.slice(1,2), 16) * 17 let g = parseInt(colorString.slice(2,3), 16) * 17 let b = parseInt(colorString.slice(3,4), 16) * 17 return [r, g, b, 255]; //return r << 16 | g << 8 | b; } else if(colorString.length == 7) { // "#FFDDAA" let r = parseInt(colorString.slice(1,3), 16) let g = parseInt(colorString.slice(3,5), 16) let b = parseInt(colorString.slice(5,7), 16) return [r, g, b, 255]; //return r << 16 | g << 8 | b ; } else { // ??? let r = 255 let g = 0 let b = 255 return [r, g, b, 255]; //return r << 16 | g << 8 | b ; } } Convert the points to RGBA uint values once. Future Point Initialization...+ point.colorUint = color_uint_from_string(point.color) Draw Voronoi Grid Per Pixel Start Performance Timing distance to point helper function const imageData = canvas.createImageData(canvas.canvas.width, canvas.canvas.height); //const imageData = canvas.getImageData(0, 0, canvas.canvas.width, canvas.canvas.height); for(let x = 0; x < CANVAS_WIDTH; x++) { for(let y = 0; y < CANVAS_HEIGHT; y ++) { let bestPoint = VoronoiPoints[0]; let bestDistSq = distance_to_point(x, y, bestPoint) for(let i in VoronoiPoints) { let point = VoronoiPoints[i]; let distSq = distance_to_point(x, y, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } putPixel(imageData, x, y, bestPoint.colorUint); } } canvas.putImageData(imageData, 0, 0); End Performance Timing Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Once...+ { canvas = document.getElementById("canvas6").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Grid Per Pixel } } O(Width * Height * NumControlPoints) = O(N^2*M) = 12,500,000 checks, which is 480ms on my PC. Let's experiment with the chunking again. Draw Voronoi Pixel Optimized Start Performance Timing const CELL_SIZE = 10; const SMALL_CELL_SIZE = 1; function distance_to_point(x, y, point) { return squared(point.x-x) + squared(point.y-y)// + squared(point.z-RENDER_Z) } const imageData = canvas.createImageData(canvas.canvas.width, canvas.canvas.height); for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { let cx = x + CELL_SIZE / 2; for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { let cy = y + CELL_SIZE / 2; let pointsWithPrecalculatedDistances = VoronoiPoints.map( point => [ distance_to_point(cx, cy, point), point ]) let sortedPoints = pointsWithPrecalculatedDistances.sort((a,b) => a[0] - b[0]); sortedPoints.length = Math.min(5, sortedPoints.length); for(let xx = 0; xx < CELL_SIZE; xx += SMALL_CELL_SIZE) { for(let yy = 0; yy < CELL_SIZE; yy += SMALL_CELL_SIZE) { let bestPoint = sortedPoints[0][1]; let bestDistSq = distance_to_point(x + xx, y + yy, bestPoint) for(let i in sortedPoints) { let point = sortedPoints[i][1]; let distSq = distance_to_point(x + xx, y + yy, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } for(let xxx = 0; xxx < SMALL_CELL_SIZE; xxx++) { for(let yyy = 0; yyy < SMALL_CELL_SIZE; yyy++) { putPixel(imageData, x + xx + xxx, y + yy + yyy, bestPoint.colorUint); } } } } } } canvas.putImageData(imageData, 0, 0); End Performance Timing Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Each Frame...+ { canvas = document.getElementById("canvas7").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Pixel Optimized } } This is comparable to the chunky version, but at 4 times the precision. A win, but not an incredible one. __main__ Useful Constants Utility Functions Basic drawing on canvas Point List Definition canvas = document.getElementById("canvas2").getContext("2d") Draw Points As Basic Circles Define Draw Points Function Set up a do_frame function to execute 30 times per second. Set up an Itersection Observer to check which canvas elements are visible Draw Points Once TODO I want to generate something different than HTML and D. I'm not happy with how the title of a code section is disconnected from the body. When I want to explain the code within the article, I have to repeat the text in both the description and the code. Extra newlines in code blocks. Long code lines can break their border and the screen. In javascript some keywords and some standard library functions aren't colored. I want to interlace descriptions and code. I named one section Draw Points Basic and another section Draw Points, and the append Draw Points...+ matched for both. Detect overly vague names. Index:Animated Points: (definition) Attempt to render a chunky pixel more quickly and more accurately.: (definition) 2Basic drawing on canvas: (definition) Calculate best control point per pixel: (definition) 2Calculate the distance from the current pixel cell to the control point and cache it in a pair: (definition) 2callback: 1canvas: 1Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations: (definition) 2Define Draw Points Function: (definition) distance to point helper function: (definition) 2 3do_frame: 1 2 3Draw Points As Basic Circles: 1 (definition) Draw Points Each Frame...+: (definition) (definition) (definition) Draw Points Each Frame: (definition) 2Draw Points Once...+: (definition) Draw the grid cell: (definition) 2Draw Voronoi Grid Optimized: 1 (definition) Draw Voronoi Grid Per Pixel: 1 (definition) Draw Voronoi Pixel Optimized: 1 (definition) End Performance Timing: 1 2 3 (definition) 5Fill the background of the canvas: 1 (definition) For a cell, calculate the list of nearish control points: 1 (definition) Future Point Initialization...+: (definition) (definition) (definition) Future Point Initialization: 1Getting Started: (definition) If the new control point is closer to our pixel than the previous control point, record the new best match.: (definition) 2Initialize the nearest control point: (definition) 2Inspiration: (definition) Javascript: 1Keep points on screen: (definition) 2Limit the control points to only the very nearest points.: 1 (definition) Loop over all the control points, calculating the new distance: (definition) 2Move points around: (definition) 2Optimized Voronoi Grid: (definition) Per-Pixel Voronoi Grid: (definition) Performance Improvements: (definition) Plot each point as a little filled circle: 1 (definition) Point List Definition: (definition) Points: (definition) Prologue: (definition) Set up a do_frame function to execute 30 times per second.: (definition) Set up an Itersection Observer to check which canvas elements are visible: (definition) setInterval: 1 2 3Sort the points based on the pre-calculated distances.: 1 (definition) Start Performance Timing: 1 2 3 (definition) 5Starting HTML: (definition) TODO: (definition) Useful Constants...+: (definition) Useful Constants: (definition) Utility Functions...+: (definition) (definition) (definition) Voronoi Grid Iteration: 1 (definition) Voronoi Grid: (definition) WEB: 1Within a single cell, calculate the actual best point per pixel.: 1 (definition) Prev:WEB 4 Top:WEB 0 Next:WEB 6
Now let's create some points on the canvas. For some purposes I'll treat these as 2D lying in the plane of the canvas/screen and for later uses I'll treat them in 3D extending above and below the screen. [3D makes this all around slower. I needed more points to saturate the cube instead of the plane. There's an interesting effect of some cells appearing and disappearing, but it's not significant.]
Point List Definition
let VoronoiPoints = [] for(let i = 0; i < 20; i++) { let point = { x: Math.random() * CANVAS_WIDTH, y: Math.random() * CANVAS_HEIGHT, //z: Math.random() * CANVAS_HEIGHT, } VoronoiPoints.push( point) Future Point Initialization }
Store off the size of the screen so I don't have to interrogate the canvas every time I draw. I've also gotten fond of working in TURNS of a circle instead of 2 * PI radians or 1 Taus.
Useful Constants
let CANVAS_WIDTH = 500 let CANVAS_HEIGHT = 500 let ONE_TURN = Math.PI * 2
Fill the background of the canvas
canvas.fillStyle="#008" canvas.rect(0,0,CANVAS_WIDTH,CANVAS_HEIGHT); canvas.fill();
Plot each point as a little filled circle
for(let i in VoronoiPoints) { let point = VoronoiPoints[i] canvas.beginPath(); canvas.fillStyle = point.color || "#800" canvas.moveTo(point.x, point.y); canvas.arc(point.x, point.y, 5, 0, ONE_TURN); canvas.fill(); }
And tie it all together.
Draw Points As Basic Circles
Fill the background of the canvas Plot each point as a little filled circle
Animated Points To create an animation, we'll need to redraw our canvas repeatedly as we move the points. I default to setInterval( ...callback..., 30), but requestAnimationFrame(...callback...) is also popular. Set up a do_frame function to execute 30 times per second. setInterval(do_frame,30); function do_frame() { Move points around Draw Points Each Frame } For the animation to be visible, our points will need to be moving, so add a random initial velocity to each point. Future Point Initialization...+ const POINT_SPEED = 3; point.vel = { x:Math.random() * POINT_SPEED - POINT_SPEED / 2, y:Math.random() * POINT_SPEED - POINT_SPEED / 2, y:Math.random() * POINT_SPEED - POINT_SPEED / 2, } And add that velocity to the positions on each frame. Move points around for(let i in VoronoiPoints) { let point = VoronoiPoints[i] point.x += point.vel.x; point.y += point.vel.y; Keep points on screen } I also want the points to be different colors. Future Point Initialization...+ point.color = "#" + Math.floor(Math.random()*10) + Math.floor(Math.random()*10) + Math.floor(Math.random()*10) + "" If we just let the points move with a given velocity, they'd quickly run off the edges of the canvas. Use the math modulo operation to keep them all between 0 and 500. Keep points on screen point.x = (point.x + CANVAS_WIDTH) % CANVAS_WIDTH; point.y = (point.y + CANVAS_HEIGHT) % CANVAS_HEIGHT; Create a function to draw the points and call that from our callback. Define Draw Points Function function draw_points(canvas) { Draw Points As Basic Circles } Draw Points Each Frame canvas = document.getElementById("canvas3").getContext("2d") if(canvas.__is_visible != false) { draw_points(canvas) } Voronoi Grid Now that we can see where our initial points are, let's generate a Voronoi grid from our points. A Voronoi grid is generated by coloring all the points on the screen with the color of the nearest control point. At each pixel, find the control point that's the nearest. Initialize the nearest control point let bestPoint = VoronoiPoints[0]; let bestDistSq = squared(bestPoint.x-x) + squared(bestPoint.y-y) // + squared(bestPoint.z-RENDER_Z); Loop over all the control points, calculating the new distance for(let i in VoronoiPoints) { let point = VoronoiPoints[i]; let distSq = squared(point.x-x) + squared(point.y-y) //+ squared(point.z-RENDER_Z) If the new control point is closer to our pixel than the previous control point, record the new best match. if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } The control points are floating through 3D space, but we're rendering a slice at a specific Z level. Useful Constants...+ const RENDER_Z = 200; Iterate over every point in the canvas. I already know this process can be slow, so I'm skipping over a grid of 4x4 to simplify the calculations. Voronoi Grid Iteration Start Performance Timing const CELL_SIZE = 4; for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { Initialize the nearest control point Loop over all the control points, calculating the new distance If the new control point is closer to our pixel than the previous control point, record the new best match. Draw the grid cell } } End Performance Timing Draw the grid cell canvas.fillStyle=bestPoint.color; canvas.fillRect(x, y, CELL_SIZE, CELL_SIZE); Add our Voronoi Grid draw code to the setInterval do_frame callback. Draw Points Each Frame...+ { canvas = document.getElementById("canvas4").getContext("2d") const z = 200; if(canvas.__is_visible != false) { Voronoi Grid Iteration } } Utility Functions...+ function squared(value) { return value * value; } Optimized Voronoi Grid This didn't work as well as I hoped. The intricacy of the Voronoi grid comes from the fine variation of possible control points against pixels. The outer loop of the optimized form doesn't really change from the previous form. Draw Voronoi Grid Optimized Start Performance Timing const CELL_SIZE = 20; const SMALL_CELL_SIZE = 2; distance to point helper function for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { let cx = x + CELL_SIZE / 2; for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { let cy = y + CELL_SIZE / 2; Attempt to render a chunky pixel more quickly and more accurately. } } End Performance Timing What I'm going to rely on for optimization is that for any chunky area, the list of closest control points doesn't change much, even if the exact closest control point does change pixel by pixel. Attempt to render a chunky pixel more quickly and more accurately. For a cell, calculate the list of nearish control points Within a single cell, calculate the actual best point per pixel. For a cell, calculate the list of nearish control points Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations Sort the points based on the pre-calculated distances. Limit the control points to only the very nearest points. The pythagorean therom for distance between two points is the square root of (The sum of (The squares of (the distance between points))). It's a game programming trick that when comparing distances, choosing to compare all the distances or all the distances squared doesn't matter because the relative distances still sort the same. So calculate the distance squared to skip the expensive and unneccessary square root function. distance to point helper function function distance_to_point(x, y, point) { return squared(point.x-x) + squared(point.y-y)// + squared(point.z-RENDER_Z) } Calculate the distance from the current pixel cell to the control point and cache it in a pair point => [ distance_to_point(cx, cy, point), point ] Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations let pointsWithPrecalculatedDistances = VoronoiPoints.map( Calculate the distance from the current pixel cell to the control point and cache it in a pair ) Sort the points based on the pre-calculated distances. let sortedPoints = pointsWithPrecalculatedDistances.sort((a,b) => a[0] - b[0]); Limit the control points to only the very nearest points. sortedPoints.length = Math.min(20, sortedPoints.length); Limiting the points too far didn't help. It made everything very chunky because in three dimensions, there are more active points than I think there are. Within a single cell, calculate the actual best point per pixel. for(let xx = 0; xx < CELL_SIZE; xx += SMALL_CELL_SIZE) { for(let yy = 0; yy < CELL_SIZE; yy += SMALL_CELL_SIZE) { Calculate best control point per pixel canvas.fillStyle = bestPoint.color; canvas.fillRect(x + xx, y + yy, SMALL_CELL_SIZE, SMALL_CELL_SIZE); } } This is the same code as for the unoptimized Voronoi grid, but now we're checking fewer adjacent control points. Calculate best control point per pixel let bestPoint = sortedPoints[0][1]; let bestDistSq = distance_to_point(x + xx, y + yy, bestPoint) for(let i in sortedPoints) { let point = sortedPoints[i][1]; let distSq = distance_to_point(x + xx, y + yy, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Each Frame...+ { canvas = document.getElementById("canvas5").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Grid Optimized } } Performance Improvements I'm still not happy with the performance, so I'm adding a performance tracker to the displays. Start Performance Timing const startTime = Date.now() End Performance Timing const elapsedTime = Date.now().valueOf() - startTime.valueOf(); const text = "" + elapsedTime + "ms"; canvas.strokeStyle="black"; canvas.fillStyle="white"; canvas.font = "24px arial"; canvas.fillText(text, 0,CANVAS_HEIGHT); canvas.strokeText(text, 0,CANVAS_HEIGHT); On my PC, the chunky naive renderer runs 30ms and the optimized more detailed version runs 60-70ms. My phone is running at the "optimized" version at 380ms. I'm going to start tracking which canvas is actually visible and stop processing the ones which aren't. Even if all this is expensive or inefficient, I don't need to process things that aren't visible. Set up an Itersection Observer to check which canvas elements are visible const observer = new IntersectionObserver(entries => { entries.forEach(canvas => { canvas.__is_visible = canvas.isVisible; }) } ); const allCanvasElements = document.querySelectorAll('canvas'); allCanvasElements.forEach(canvas => observer.observe(canvas)); Per-Pixel Voronoi Grid The renderer is still too slow. Let's see if rectFill is slow and pixel rendering is faster. Utility Functions...+ function putPixel(imageData, x, y, colorRGBA) { const pixelStart = (x + y * imageData.width) * 4; for(let i = 0; i < 4; i++) { imageData.data[pixelStart + i] = colorRGBA[i]; } //imageData.data[ + 0] = (colorRGBA & 0xFF0000) >> 16; //imageData.data[(x + y * imageData.width) * 4 + 1] = (colorRGBA & 0x00FF00) >> 8; //imageData.data[(x + y * imageData.width) * 4 + 2] = colorRGBA & 0x0000FF; //imageData.data[(x + y * imageData.width) * 4 + 3] = 255;; } Utility Functions...+ function color_uint_from_string(colorString) { if(colorString.length == 4) { // "#FDA" // 17 = 16 + 1 :: F => FF , 8 => 88 (in hex) let r = parseInt(colorString.slice(1,2), 16) * 17 let g = parseInt(colorString.slice(2,3), 16) * 17 let b = parseInt(colorString.slice(3,4), 16) * 17 return [r, g, b, 255]; //return r << 16 | g << 8 | b; } else if(colorString.length == 7) { // "#FFDDAA" let r = parseInt(colorString.slice(1,3), 16) let g = parseInt(colorString.slice(3,5), 16) let b = parseInt(colorString.slice(5,7), 16) return [r, g, b, 255]; //return r << 16 | g << 8 | b ; } else { // ??? let r = 255 let g = 0 let b = 255 return [r, g, b, 255]; //return r << 16 | g << 8 | b ; } } Convert the points to RGBA uint values once. Future Point Initialization...+ point.colorUint = color_uint_from_string(point.color) Draw Voronoi Grid Per Pixel Start Performance Timing distance to point helper function const imageData = canvas.createImageData(canvas.canvas.width, canvas.canvas.height); //const imageData = canvas.getImageData(0, 0, canvas.canvas.width, canvas.canvas.height); for(let x = 0; x < CANVAS_WIDTH; x++) { for(let y = 0; y < CANVAS_HEIGHT; y ++) { let bestPoint = VoronoiPoints[0]; let bestDistSq = distance_to_point(x, y, bestPoint) for(let i in VoronoiPoints) { let point = VoronoiPoints[i]; let distSq = distance_to_point(x, y, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } putPixel(imageData, x, y, bestPoint.colorUint); } } canvas.putImageData(imageData, 0, 0); End Performance Timing Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Once...+ { canvas = document.getElementById("canvas6").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Grid Per Pixel } } O(Width * Height * NumControlPoints) = O(N^2*M) = 12,500,000 checks, which is 480ms on my PC. Let's experiment with the chunking again. Draw Voronoi Pixel Optimized Start Performance Timing const CELL_SIZE = 10; const SMALL_CELL_SIZE = 1; function distance_to_point(x, y, point) { return squared(point.x-x) + squared(point.y-y)// + squared(point.z-RENDER_Z) } const imageData = canvas.createImageData(canvas.canvas.width, canvas.canvas.height); for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { let cx = x + CELL_SIZE / 2; for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { let cy = y + CELL_SIZE / 2; let pointsWithPrecalculatedDistances = VoronoiPoints.map( point => [ distance_to_point(cx, cy, point), point ]) let sortedPoints = pointsWithPrecalculatedDistances.sort((a,b) => a[0] - b[0]); sortedPoints.length = Math.min(5, sortedPoints.length); for(let xx = 0; xx < CELL_SIZE; xx += SMALL_CELL_SIZE) { for(let yy = 0; yy < CELL_SIZE; yy += SMALL_CELL_SIZE) { let bestPoint = sortedPoints[0][1]; let bestDistSq = distance_to_point(x + xx, y + yy, bestPoint) for(let i in sortedPoints) { let point = sortedPoints[i][1]; let distSq = distance_to_point(x + xx, y + yy, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } for(let xxx = 0; xxx < SMALL_CELL_SIZE; xxx++) { for(let yyy = 0; yyy < SMALL_CELL_SIZE; yyy++) { putPixel(imageData, x + xx + xxx, y + yy + yyy, bestPoint.colorUint); } } } } } } canvas.putImageData(imageData, 0, 0); End Performance Timing Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Each Frame...+ { canvas = document.getElementById("canvas7").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Pixel Optimized } } This is comparable to the chunky version, but at 4 times the precision. A win, but not an incredible one. __main__ Useful Constants Utility Functions Basic drawing on canvas Point List Definition canvas = document.getElementById("canvas2").getContext("2d") Draw Points As Basic Circles Define Draw Points Function Set up a do_frame function to execute 30 times per second. Set up an Itersection Observer to check which canvas elements are visible Draw Points Once TODO I want to generate something different than HTML and D. I'm not happy with how the title of a code section is disconnected from the body. When I want to explain the code within the article, I have to repeat the text in both the description and the code. Extra newlines in code blocks. Long code lines can break their border and the screen. In javascript some keywords and some standard library functions aren't colored. I want to interlace descriptions and code. I named one section Draw Points Basic and another section Draw Points, and the append Draw Points...+ matched for both. Detect overly vague names. Index:Animated Points: (definition) Attempt to render a chunky pixel more quickly and more accurately.: (definition) 2Basic drawing on canvas: (definition) Calculate best control point per pixel: (definition) 2Calculate the distance from the current pixel cell to the control point and cache it in a pair: (definition) 2callback: 1canvas: 1Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations: (definition) 2Define Draw Points Function: (definition) distance to point helper function: (definition) 2 3do_frame: 1 2 3Draw Points As Basic Circles: 1 (definition) Draw Points Each Frame...+: (definition) (definition) (definition) Draw Points Each Frame: (definition) 2Draw Points Once...+: (definition) Draw the grid cell: (definition) 2Draw Voronoi Grid Optimized: 1 (definition) Draw Voronoi Grid Per Pixel: 1 (definition) Draw Voronoi Pixel Optimized: 1 (definition) End Performance Timing: 1 2 3 (definition) 5Fill the background of the canvas: 1 (definition) For a cell, calculate the list of nearish control points: 1 (definition) Future Point Initialization...+: (definition) (definition) (definition) Future Point Initialization: 1Getting Started: (definition) If the new control point is closer to our pixel than the previous control point, record the new best match.: (definition) 2Initialize the nearest control point: (definition) 2Inspiration: (definition) Javascript: 1Keep points on screen: (definition) 2Limit the control points to only the very nearest points.: 1 (definition) Loop over all the control points, calculating the new distance: (definition) 2Move points around: (definition) 2Optimized Voronoi Grid: (definition) Per-Pixel Voronoi Grid: (definition) Performance Improvements: (definition) Plot each point as a little filled circle: 1 (definition) Point List Definition: (definition) Points: (definition) Prologue: (definition) Set up a do_frame function to execute 30 times per second.: (definition) Set up an Itersection Observer to check which canvas elements are visible: (definition) setInterval: 1 2 3Sort the points based on the pre-calculated distances.: 1 (definition) Start Performance Timing: 1 2 3 (definition) 5Starting HTML: (definition) TODO: (definition) Useful Constants...+: (definition) Useful Constants: (definition) Utility Functions...+: (definition) (definition) (definition) Voronoi Grid Iteration: 1 (definition) Voronoi Grid: (definition) WEB: 1Within a single cell, calculate the actual best point per pixel.: 1 (definition) Prev:WEB 4 Top:WEB 0 Next:WEB 6
To create an animation, we'll need to redraw our canvas repeatedly as we move the points. I default to setInterval( ...callback..., 30), but requestAnimationFrame(...callback...) is also popular.
Set up a do_frame function to execute 30 times per second.
setInterval(do_frame,30); function do_frame() { Move points around Draw Points Each Frame }
For the animation to be visible, our points will need to be moving, so add a random initial velocity to each point.
Future Point Initialization...+
const POINT_SPEED = 3; point.vel = { x:Math.random() * POINT_SPEED - POINT_SPEED / 2, y:Math.random() * POINT_SPEED - POINT_SPEED / 2, y:Math.random() * POINT_SPEED - POINT_SPEED / 2, }
And add that velocity to the positions on each frame.
Move points around
for(let i in VoronoiPoints) { let point = VoronoiPoints[i] point.x += point.vel.x; point.y += point.vel.y; Keep points on screen }
I also want the points to be different colors.
point.color = "#" + Math.floor(Math.random()*10) + Math.floor(Math.random()*10) + Math.floor(Math.random()*10) + ""
If we just let the points move with a given velocity, they'd quickly run off the edges of the canvas. Use the math modulo operation to keep them all between 0 and 500.
Keep points on screen
point.x = (point.x + CANVAS_WIDTH) % CANVAS_WIDTH; point.y = (point.y + CANVAS_HEIGHT) % CANVAS_HEIGHT;
Create a function to draw the points and call that from our callback.
Define Draw Points Function
function draw_points(canvas) { Draw Points As Basic Circles }
Draw Points Each Frame
canvas = document.getElementById("canvas3").getContext("2d") if(canvas.__is_visible != false) { draw_points(canvas) }
Voronoi Grid Now that we can see where our initial points are, let's generate a Voronoi grid from our points. A Voronoi grid is generated by coloring all the points on the screen with the color of the nearest control point. At each pixel, find the control point that's the nearest. Initialize the nearest control point let bestPoint = VoronoiPoints[0]; let bestDistSq = squared(bestPoint.x-x) + squared(bestPoint.y-y) // + squared(bestPoint.z-RENDER_Z); Loop over all the control points, calculating the new distance for(let i in VoronoiPoints) { let point = VoronoiPoints[i]; let distSq = squared(point.x-x) + squared(point.y-y) //+ squared(point.z-RENDER_Z) If the new control point is closer to our pixel than the previous control point, record the new best match. if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } The control points are floating through 3D space, but we're rendering a slice at a specific Z level. Useful Constants...+ const RENDER_Z = 200; Iterate over every point in the canvas. I already know this process can be slow, so I'm skipping over a grid of 4x4 to simplify the calculations. Voronoi Grid Iteration Start Performance Timing const CELL_SIZE = 4; for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { Initialize the nearest control point Loop over all the control points, calculating the new distance If the new control point is closer to our pixel than the previous control point, record the new best match. Draw the grid cell } } End Performance Timing Draw the grid cell canvas.fillStyle=bestPoint.color; canvas.fillRect(x, y, CELL_SIZE, CELL_SIZE); Add our Voronoi Grid draw code to the setInterval do_frame callback. Draw Points Each Frame...+ { canvas = document.getElementById("canvas4").getContext("2d") const z = 200; if(canvas.__is_visible != false) { Voronoi Grid Iteration } } Utility Functions...+ function squared(value) { return value * value; } Optimized Voronoi Grid This didn't work as well as I hoped. The intricacy of the Voronoi grid comes from the fine variation of possible control points against pixels. The outer loop of the optimized form doesn't really change from the previous form. Draw Voronoi Grid Optimized Start Performance Timing const CELL_SIZE = 20; const SMALL_CELL_SIZE = 2; distance to point helper function for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { let cx = x + CELL_SIZE / 2; for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { let cy = y + CELL_SIZE / 2; Attempt to render a chunky pixel more quickly and more accurately. } } End Performance Timing What I'm going to rely on for optimization is that for any chunky area, the list of closest control points doesn't change much, even if the exact closest control point does change pixel by pixel. Attempt to render a chunky pixel more quickly and more accurately. For a cell, calculate the list of nearish control points Within a single cell, calculate the actual best point per pixel. For a cell, calculate the list of nearish control points Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations Sort the points based on the pre-calculated distances. Limit the control points to only the very nearest points. The pythagorean therom for distance between two points is the square root of (The sum of (The squares of (the distance between points))). It's a game programming trick that when comparing distances, choosing to compare all the distances or all the distances squared doesn't matter because the relative distances still sort the same. So calculate the distance squared to skip the expensive and unneccessary square root function. distance to point helper function function distance_to_point(x, y, point) { return squared(point.x-x) + squared(point.y-y)// + squared(point.z-RENDER_Z) } Calculate the distance from the current pixel cell to the control point and cache it in a pair point => [ distance_to_point(cx, cy, point), point ] Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations let pointsWithPrecalculatedDistances = VoronoiPoints.map( Calculate the distance from the current pixel cell to the control point and cache it in a pair ) Sort the points based on the pre-calculated distances. let sortedPoints = pointsWithPrecalculatedDistances.sort((a,b) => a[0] - b[0]); Limit the control points to only the very nearest points. sortedPoints.length = Math.min(20, sortedPoints.length); Limiting the points too far didn't help. It made everything very chunky because in three dimensions, there are more active points than I think there are. Within a single cell, calculate the actual best point per pixel. for(let xx = 0; xx < CELL_SIZE; xx += SMALL_CELL_SIZE) { for(let yy = 0; yy < CELL_SIZE; yy += SMALL_CELL_SIZE) { Calculate best control point per pixel canvas.fillStyle = bestPoint.color; canvas.fillRect(x + xx, y + yy, SMALL_CELL_SIZE, SMALL_CELL_SIZE); } } This is the same code as for the unoptimized Voronoi grid, but now we're checking fewer adjacent control points. Calculate best control point per pixel let bestPoint = sortedPoints[0][1]; let bestDistSq = distance_to_point(x + xx, y + yy, bestPoint) for(let i in sortedPoints) { let point = sortedPoints[i][1]; let distSq = distance_to_point(x + xx, y + yy, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Each Frame...+ { canvas = document.getElementById("canvas5").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Grid Optimized } } Performance Improvements I'm still not happy with the performance, so I'm adding a performance tracker to the displays. Start Performance Timing const startTime = Date.now() End Performance Timing const elapsedTime = Date.now().valueOf() - startTime.valueOf(); const text = "" + elapsedTime + "ms"; canvas.strokeStyle="black"; canvas.fillStyle="white"; canvas.font = "24px arial"; canvas.fillText(text, 0,CANVAS_HEIGHT); canvas.strokeText(text, 0,CANVAS_HEIGHT); On my PC, the chunky naive renderer runs 30ms and the optimized more detailed version runs 60-70ms. My phone is running at the "optimized" version at 380ms. I'm going to start tracking which canvas is actually visible and stop processing the ones which aren't. Even if all this is expensive or inefficient, I don't need to process things that aren't visible. Set up an Itersection Observer to check which canvas elements are visible const observer = new IntersectionObserver(entries => { entries.forEach(canvas => { canvas.__is_visible = canvas.isVisible; }) } ); const allCanvasElements = document.querySelectorAll('canvas'); allCanvasElements.forEach(canvas => observer.observe(canvas)); Per-Pixel Voronoi Grid The renderer is still too slow. Let's see if rectFill is slow and pixel rendering is faster. Utility Functions...+ function putPixel(imageData, x, y, colorRGBA) { const pixelStart = (x + y * imageData.width) * 4; for(let i = 0; i < 4; i++) { imageData.data[pixelStart + i] = colorRGBA[i]; } //imageData.data[ + 0] = (colorRGBA & 0xFF0000) >> 16; //imageData.data[(x + y * imageData.width) * 4 + 1] = (colorRGBA & 0x00FF00) >> 8; //imageData.data[(x + y * imageData.width) * 4 + 2] = colorRGBA & 0x0000FF; //imageData.data[(x + y * imageData.width) * 4 + 3] = 255;; } Utility Functions...+ function color_uint_from_string(colorString) { if(colorString.length == 4) { // "#FDA" // 17 = 16 + 1 :: F => FF , 8 => 88 (in hex) let r = parseInt(colorString.slice(1,2), 16) * 17 let g = parseInt(colorString.slice(2,3), 16) * 17 let b = parseInt(colorString.slice(3,4), 16) * 17 return [r, g, b, 255]; //return r << 16 | g << 8 | b; } else if(colorString.length == 7) { // "#FFDDAA" let r = parseInt(colorString.slice(1,3), 16) let g = parseInt(colorString.slice(3,5), 16) let b = parseInt(colorString.slice(5,7), 16) return [r, g, b, 255]; //return r << 16 | g << 8 | b ; } else { // ??? let r = 255 let g = 0 let b = 255 return [r, g, b, 255]; //return r << 16 | g << 8 | b ; } } Convert the points to RGBA uint values once. Future Point Initialization...+ point.colorUint = color_uint_from_string(point.color) Draw Voronoi Grid Per Pixel Start Performance Timing distance to point helper function const imageData = canvas.createImageData(canvas.canvas.width, canvas.canvas.height); //const imageData = canvas.getImageData(0, 0, canvas.canvas.width, canvas.canvas.height); for(let x = 0; x < CANVAS_WIDTH; x++) { for(let y = 0; y < CANVAS_HEIGHT; y ++) { let bestPoint = VoronoiPoints[0]; let bestDistSq = distance_to_point(x, y, bestPoint) for(let i in VoronoiPoints) { let point = VoronoiPoints[i]; let distSq = distance_to_point(x, y, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } putPixel(imageData, x, y, bestPoint.colorUint); } } canvas.putImageData(imageData, 0, 0); End Performance Timing Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Once...+ { canvas = document.getElementById("canvas6").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Grid Per Pixel } } O(Width * Height * NumControlPoints) = O(N^2*M) = 12,500,000 checks, which is 480ms on my PC. Let's experiment with the chunking again. Draw Voronoi Pixel Optimized Start Performance Timing const CELL_SIZE = 10; const SMALL_CELL_SIZE = 1; function distance_to_point(x, y, point) { return squared(point.x-x) + squared(point.y-y)// + squared(point.z-RENDER_Z) } const imageData = canvas.createImageData(canvas.canvas.width, canvas.canvas.height); for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { let cx = x + CELL_SIZE / 2; for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { let cy = y + CELL_SIZE / 2; let pointsWithPrecalculatedDistances = VoronoiPoints.map( point => [ distance_to_point(cx, cy, point), point ]) let sortedPoints = pointsWithPrecalculatedDistances.sort((a,b) => a[0] - b[0]); sortedPoints.length = Math.min(5, sortedPoints.length); for(let xx = 0; xx < CELL_SIZE; xx += SMALL_CELL_SIZE) { for(let yy = 0; yy < CELL_SIZE; yy += SMALL_CELL_SIZE) { let bestPoint = sortedPoints[0][1]; let bestDistSq = distance_to_point(x + xx, y + yy, bestPoint) for(let i in sortedPoints) { let point = sortedPoints[i][1]; let distSq = distance_to_point(x + xx, y + yy, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } for(let xxx = 0; xxx < SMALL_CELL_SIZE; xxx++) { for(let yyy = 0; yyy < SMALL_CELL_SIZE; yyy++) { putPixel(imageData, x + xx + xxx, y + yy + yyy, bestPoint.colorUint); } } } } } } canvas.putImageData(imageData, 0, 0); End Performance Timing Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Each Frame...+ { canvas = document.getElementById("canvas7").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Pixel Optimized } } This is comparable to the chunky version, but at 4 times the precision. A win, but not an incredible one. __main__ Useful Constants Utility Functions Basic drawing on canvas Point List Definition canvas = document.getElementById("canvas2").getContext("2d") Draw Points As Basic Circles Define Draw Points Function Set up a do_frame function to execute 30 times per second. Set up an Itersection Observer to check which canvas elements are visible Draw Points Once TODO I want to generate something different than HTML and D. I'm not happy with how the title of a code section is disconnected from the body. When I want to explain the code within the article, I have to repeat the text in both the description and the code. Extra newlines in code blocks. Long code lines can break their border and the screen. In javascript some keywords and some standard library functions aren't colored. I want to interlace descriptions and code. I named one section Draw Points Basic and another section Draw Points, and the append Draw Points...+ matched for both. Detect overly vague names. Index:Animated Points: (definition) Attempt to render a chunky pixel more quickly and more accurately.: (definition) 2Basic drawing on canvas: (definition) Calculate best control point per pixel: (definition) 2Calculate the distance from the current pixel cell to the control point and cache it in a pair: (definition) 2callback: 1canvas: 1Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations: (definition) 2Define Draw Points Function: (definition) distance to point helper function: (definition) 2 3do_frame: 1 2 3Draw Points As Basic Circles: 1 (definition) Draw Points Each Frame...+: (definition) (definition) (definition) Draw Points Each Frame: (definition) 2Draw Points Once...+: (definition) Draw the grid cell: (definition) 2Draw Voronoi Grid Optimized: 1 (definition) Draw Voronoi Grid Per Pixel: 1 (definition) Draw Voronoi Pixel Optimized: 1 (definition) End Performance Timing: 1 2 3 (definition) 5Fill the background of the canvas: 1 (definition) For a cell, calculate the list of nearish control points: 1 (definition) Future Point Initialization...+: (definition) (definition) (definition) Future Point Initialization: 1Getting Started: (definition) If the new control point is closer to our pixel than the previous control point, record the new best match.: (definition) 2Initialize the nearest control point: (definition) 2Inspiration: (definition) Javascript: 1Keep points on screen: (definition) 2Limit the control points to only the very nearest points.: 1 (definition) Loop over all the control points, calculating the new distance: (definition) 2Move points around: (definition) 2Optimized Voronoi Grid: (definition) Per-Pixel Voronoi Grid: (definition) Performance Improvements: (definition) Plot each point as a little filled circle: 1 (definition) Point List Definition: (definition) Points: (definition) Prologue: (definition) Set up a do_frame function to execute 30 times per second.: (definition) Set up an Itersection Observer to check which canvas elements are visible: (definition) setInterval: 1 2 3Sort the points based on the pre-calculated distances.: 1 (definition) Start Performance Timing: 1 2 3 (definition) 5Starting HTML: (definition) TODO: (definition) Useful Constants...+: (definition) Useful Constants: (definition) Utility Functions...+: (definition) (definition) (definition) Voronoi Grid Iteration: 1 (definition) Voronoi Grid: (definition) WEB: 1Within a single cell, calculate the actual best point per pixel.: 1 (definition) Prev:WEB 4 Top:WEB 0 Next:WEB 6
Now that we can see where our initial points are, let's generate a Voronoi grid from our points. A Voronoi grid is generated by coloring all the points on the screen with the color of the nearest control point.
At each pixel, find the control point that's the nearest.
Initialize the nearest control point
let bestPoint = VoronoiPoints[0]; let bestDistSq = squared(bestPoint.x-x) + squared(bestPoint.y-y) // + squared(bestPoint.z-RENDER_Z);
Loop over all the control points, calculating the new distance
for(let i in VoronoiPoints) { let point = VoronoiPoints[i]; let distSq = squared(point.x-x) + squared(point.y-y) //+ squared(point.z-RENDER_Z)
If the new control point is closer to our pixel than the previous control point, record the new best match.
if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } }
The control points are floating through 3D space, but we're rendering a slice at a specific Z level.
Useful Constants...+
const RENDER_Z = 200;
Iterate over every point in the canvas. I already know this process can be slow, so I'm skipping over a grid of 4x4 to simplify the calculations.
Voronoi Grid Iteration
Start Performance Timing const CELL_SIZE = 4; for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { Initialize the nearest control point Loop over all the control points, calculating the new distance If the new control point is closer to our pixel than the previous control point, record the new best match. Draw the grid cell } } End Performance Timing
Draw the grid cell
canvas.fillStyle=bestPoint.color; canvas.fillRect(x, y, CELL_SIZE, CELL_SIZE);
Add our Voronoi Grid draw code to the setInterval do_frame callback.
Draw Points Each Frame...+
{ canvas = document.getElementById("canvas4").getContext("2d") const z = 200; if(canvas.__is_visible != false) { Voronoi Grid Iteration } }
Utility Functions...+
function squared(value) { return value * value; }
Optimized Voronoi Grid This didn't work as well as I hoped. The intricacy of the Voronoi grid comes from the fine variation of possible control points against pixels. The outer loop of the optimized form doesn't really change from the previous form. Draw Voronoi Grid Optimized Start Performance Timing const CELL_SIZE = 20; const SMALL_CELL_SIZE = 2; distance to point helper function for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { let cx = x + CELL_SIZE / 2; for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { let cy = y + CELL_SIZE / 2; Attempt to render a chunky pixel more quickly and more accurately. } } End Performance Timing What I'm going to rely on for optimization is that for any chunky area, the list of closest control points doesn't change much, even if the exact closest control point does change pixel by pixel. Attempt to render a chunky pixel more quickly and more accurately. For a cell, calculate the list of nearish control points Within a single cell, calculate the actual best point per pixel. For a cell, calculate the list of nearish control points Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations Sort the points based on the pre-calculated distances. Limit the control points to only the very nearest points. The pythagorean therom for distance between two points is the square root of (The sum of (The squares of (the distance between points))). It's a game programming trick that when comparing distances, choosing to compare all the distances or all the distances squared doesn't matter because the relative distances still sort the same. So calculate the distance squared to skip the expensive and unneccessary square root function. distance to point helper function function distance_to_point(x, y, point) { return squared(point.x-x) + squared(point.y-y)// + squared(point.z-RENDER_Z) } Calculate the distance from the current pixel cell to the control point and cache it in a pair point => [ distance_to_point(cx, cy, point), point ] Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations let pointsWithPrecalculatedDistances = VoronoiPoints.map( Calculate the distance from the current pixel cell to the control point and cache it in a pair ) Sort the points based on the pre-calculated distances. let sortedPoints = pointsWithPrecalculatedDistances.sort((a,b) => a[0] - b[0]); Limit the control points to only the very nearest points. sortedPoints.length = Math.min(20, sortedPoints.length); Limiting the points too far didn't help. It made everything very chunky because in three dimensions, there are more active points than I think there are. Within a single cell, calculate the actual best point per pixel. for(let xx = 0; xx < CELL_SIZE; xx += SMALL_CELL_SIZE) { for(let yy = 0; yy < CELL_SIZE; yy += SMALL_CELL_SIZE) { Calculate best control point per pixel canvas.fillStyle = bestPoint.color; canvas.fillRect(x + xx, y + yy, SMALL_CELL_SIZE, SMALL_CELL_SIZE); } } This is the same code as for the unoptimized Voronoi grid, but now we're checking fewer adjacent control points. Calculate best control point per pixel let bestPoint = sortedPoints[0][1]; let bestDistSq = distance_to_point(x + xx, y + yy, bestPoint) for(let i in sortedPoints) { let point = sortedPoints[i][1]; let distSq = distance_to_point(x + xx, y + yy, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Each Frame...+ { canvas = document.getElementById("canvas5").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Grid Optimized } } Performance Improvements I'm still not happy with the performance, so I'm adding a performance tracker to the displays. Start Performance Timing const startTime = Date.now() End Performance Timing const elapsedTime = Date.now().valueOf() - startTime.valueOf(); const text = "" + elapsedTime + "ms"; canvas.strokeStyle="black"; canvas.fillStyle="white"; canvas.font = "24px arial"; canvas.fillText(text, 0,CANVAS_HEIGHT); canvas.strokeText(text, 0,CANVAS_HEIGHT); On my PC, the chunky naive renderer runs 30ms and the optimized more detailed version runs 60-70ms. My phone is running at the "optimized" version at 380ms. I'm going to start tracking which canvas is actually visible and stop processing the ones which aren't. Even if all this is expensive or inefficient, I don't need to process things that aren't visible. Set up an Itersection Observer to check which canvas elements are visible const observer = new IntersectionObserver(entries => { entries.forEach(canvas => { canvas.__is_visible = canvas.isVisible; }) } ); const allCanvasElements = document.querySelectorAll('canvas'); allCanvasElements.forEach(canvas => observer.observe(canvas)); Per-Pixel Voronoi Grid The renderer is still too slow. Let's see if rectFill is slow and pixel rendering is faster. Utility Functions...+ function putPixel(imageData, x, y, colorRGBA) { const pixelStart = (x + y * imageData.width) * 4; for(let i = 0; i < 4; i++) { imageData.data[pixelStart + i] = colorRGBA[i]; } //imageData.data[ + 0] = (colorRGBA & 0xFF0000) >> 16; //imageData.data[(x + y * imageData.width) * 4 + 1] = (colorRGBA & 0x00FF00) >> 8; //imageData.data[(x + y * imageData.width) * 4 + 2] = colorRGBA & 0x0000FF; //imageData.data[(x + y * imageData.width) * 4 + 3] = 255;; } Utility Functions...+ function color_uint_from_string(colorString) { if(colorString.length == 4) { // "#FDA" // 17 = 16 + 1 :: F => FF , 8 => 88 (in hex) let r = parseInt(colorString.slice(1,2), 16) * 17 let g = parseInt(colorString.slice(2,3), 16) * 17 let b = parseInt(colorString.slice(3,4), 16) * 17 return [r, g, b, 255]; //return r << 16 | g << 8 | b; } else if(colorString.length == 7) { // "#FFDDAA" let r = parseInt(colorString.slice(1,3), 16) let g = parseInt(colorString.slice(3,5), 16) let b = parseInt(colorString.slice(5,7), 16) return [r, g, b, 255]; //return r << 16 | g << 8 | b ; } else { // ??? let r = 255 let g = 0 let b = 255 return [r, g, b, 255]; //return r << 16 | g << 8 | b ; } } Convert the points to RGBA uint values once. Future Point Initialization...+ point.colorUint = color_uint_from_string(point.color) Draw Voronoi Grid Per Pixel Start Performance Timing distance to point helper function const imageData = canvas.createImageData(canvas.canvas.width, canvas.canvas.height); //const imageData = canvas.getImageData(0, 0, canvas.canvas.width, canvas.canvas.height); for(let x = 0; x < CANVAS_WIDTH; x++) { for(let y = 0; y < CANVAS_HEIGHT; y ++) { let bestPoint = VoronoiPoints[0]; let bestDistSq = distance_to_point(x, y, bestPoint) for(let i in VoronoiPoints) { let point = VoronoiPoints[i]; let distSq = distance_to_point(x, y, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } putPixel(imageData, x, y, bestPoint.colorUint); } } canvas.putImageData(imageData, 0, 0); End Performance Timing Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Once...+ { canvas = document.getElementById("canvas6").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Grid Per Pixel } } O(Width * Height * NumControlPoints) = O(N^2*M) = 12,500,000 checks, which is 480ms on my PC. Let's experiment with the chunking again. Draw Voronoi Pixel Optimized Start Performance Timing const CELL_SIZE = 10; const SMALL_CELL_SIZE = 1; function distance_to_point(x, y, point) { return squared(point.x-x) + squared(point.y-y)// + squared(point.z-RENDER_Z) } const imageData = canvas.createImageData(canvas.canvas.width, canvas.canvas.height); for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { let cx = x + CELL_SIZE / 2; for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { let cy = y + CELL_SIZE / 2; let pointsWithPrecalculatedDistances = VoronoiPoints.map( point => [ distance_to_point(cx, cy, point), point ]) let sortedPoints = pointsWithPrecalculatedDistances.sort((a,b) => a[0] - b[0]); sortedPoints.length = Math.min(5, sortedPoints.length); for(let xx = 0; xx < CELL_SIZE; xx += SMALL_CELL_SIZE) { for(let yy = 0; yy < CELL_SIZE; yy += SMALL_CELL_SIZE) { let bestPoint = sortedPoints[0][1]; let bestDistSq = distance_to_point(x + xx, y + yy, bestPoint) for(let i in sortedPoints) { let point = sortedPoints[i][1]; let distSq = distance_to_point(x + xx, y + yy, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } for(let xxx = 0; xxx < SMALL_CELL_SIZE; xxx++) { for(let yyy = 0; yyy < SMALL_CELL_SIZE; yyy++) { putPixel(imageData, x + xx + xxx, y + yy + yyy, bestPoint.colorUint); } } } } } } canvas.putImageData(imageData, 0, 0); End Performance Timing Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Each Frame...+ { canvas = document.getElementById("canvas7").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Pixel Optimized } } This is comparable to the chunky version, but at 4 times the precision. A win, but not an incredible one. __main__ Useful Constants Utility Functions Basic drawing on canvas Point List Definition canvas = document.getElementById("canvas2").getContext("2d") Draw Points As Basic Circles Define Draw Points Function Set up a do_frame function to execute 30 times per second. Set up an Itersection Observer to check which canvas elements are visible Draw Points Once TODO I want to generate something different than HTML and D. I'm not happy with how the title of a code section is disconnected from the body. When I want to explain the code within the article, I have to repeat the text in both the description and the code. Extra newlines in code blocks. Long code lines can break their border and the screen. In javascript some keywords and some standard library functions aren't colored. I want to interlace descriptions and code. I named one section Draw Points Basic and another section Draw Points, and the append Draw Points...+ matched for both. Detect overly vague names. Index:Animated Points: (definition) Attempt to render a chunky pixel more quickly and more accurately.: (definition) 2Basic drawing on canvas: (definition) Calculate best control point per pixel: (definition) 2Calculate the distance from the current pixel cell to the control point and cache it in a pair: (definition) 2callback: 1canvas: 1Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations: (definition) 2Define Draw Points Function: (definition) distance to point helper function: (definition) 2 3do_frame: 1 2 3Draw Points As Basic Circles: 1 (definition) Draw Points Each Frame...+: (definition) (definition) (definition) Draw Points Each Frame: (definition) 2Draw Points Once...+: (definition) Draw the grid cell: (definition) 2Draw Voronoi Grid Optimized: 1 (definition) Draw Voronoi Grid Per Pixel: 1 (definition) Draw Voronoi Pixel Optimized: 1 (definition) End Performance Timing: 1 2 3 (definition) 5Fill the background of the canvas: 1 (definition) For a cell, calculate the list of nearish control points: 1 (definition) Future Point Initialization...+: (definition) (definition) (definition) Future Point Initialization: 1Getting Started: (definition) If the new control point is closer to our pixel than the previous control point, record the new best match.: (definition) 2Initialize the nearest control point: (definition) 2Inspiration: (definition) Javascript: 1Keep points on screen: (definition) 2Limit the control points to only the very nearest points.: 1 (definition) Loop over all the control points, calculating the new distance: (definition) 2Move points around: (definition) 2Optimized Voronoi Grid: (definition) Per-Pixel Voronoi Grid: (definition) Performance Improvements: (definition) Plot each point as a little filled circle: 1 (definition) Point List Definition: (definition) Points: (definition) Prologue: (definition) Set up a do_frame function to execute 30 times per second.: (definition) Set up an Itersection Observer to check which canvas elements are visible: (definition) setInterval: 1 2 3Sort the points based on the pre-calculated distances.: 1 (definition) Start Performance Timing: 1 2 3 (definition) 5Starting HTML: (definition) TODO: (definition) Useful Constants...+: (definition) Useful Constants: (definition) Utility Functions...+: (definition) (definition) (definition) Voronoi Grid Iteration: 1 (definition) Voronoi Grid: (definition) WEB: 1Within a single cell, calculate the actual best point per pixel.: 1 (definition) Prev:WEB 4 Top:WEB 0 Next:WEB 6
This didn't work as well as I hoped. The intricacy of the Voronoi grid comes from the fine variation of possible control points against pixels.
The outer loop of the optimized form doesn't really change from the previous form.
Draw Voronoi Grid Optimized
Start Performance Timing const CELL_SIZE = 20; const SMALL_CELL_SIZE = 2; distance to point helper function for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { let cx = x + CELL_SIZE / 2; for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { let cy = y + CELL_SIZE / 2; Attempt to render a chunky pixel more quickly and more accurately. } } End Performance Timing
What I'm going to rely on for optimization is that for any chunky area, the list of closest control points doesn't change much, even if the exact closest control point does change pixel by pixel.
Attempt to render a chunky pixel more quickly and more accurately.
For a cell, calculate the list of nearish control points Within a single cell, calculate the actual best point per pixel.
For a cell, calculate the list of nearish control points
Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations Sort the points based on the pre-calculated distances. Limit the control points to only the very nearest points.
The pythagorean therom for distance between two points is the square root of (The sum of (The squares of (the distance between points))). It's a game programming trick that when comparing distances, choosing to compare all the distances or all the distances squared doesn't matter because the relative distances still sort the same. So calculate the distance squared to skip the expensive and unneccessary square root function.
distance to point helper function
function distance_to_point(x, y, point) { return squared(point.x-x) + squared(point.y-y)// + squared(point.z-RENDER_Z) }
Calculate the distance from the current pixel cell to the control point and cache it in a pair
point => [ distance_to_point(cx, cy, point), point ]
Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations
let pointsWithPrecalculatedDistances = VoronoiPoints.map( Calculate the distance from the current pixel cell to the control point and cache it in a pair )
Sort the points based on the pre-calculated distances.
let sortedPoints = pointsWithPrecalculatedDistances.sort((a,b) => a[0] - b[0]);
Limit the control points to only the very nearest points.
sortedPoints.length = Math.min(20, sortedPoints.length);
Limiting the points too far didn't help. It made everything very chunky because in three dimensions, there are more active points than I think there are.
Within a single cell, calculate the actual best point per pixel.
for(let xx = 0; xx < CELL_SIZE; xx += SMALL_CELL_SIZE) { for(let yy = 0; yy < CELL_SIZE; yy += SMALL_CELL_SIZE) { Calculate best control point per pixel canvas.fillStyle = bestPoint.color; canvas.fillRect(x + xx, y + yy, SMALL_CELL_SIZE, SMALL_CELL_SIZE); } }
This is the same code as for the unoptimized Voronoi grid, but now we're checking fewer adjacent control points.
Calculate best control point per pixel
let bestPoint = sortedPoints[0][1]; let bestDistSq = distance_to_point(x + xx, y + yy, bestPoint) for(let i in sortedPoints) { let point = sortedPoints[i][1]; let distSq = distance_to_point(x + xx, y + yy, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } }
Add our optimized Voronoi grid draw code to the setInterval do_frame callback.
{ canvas = document.getElementById("canvas5").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Grid Optimized } }
Performance Improvements I'm still not happy with the performance, so I'm adding a performance tracker to the displays. Start Performance Timing const startTime = Date.now() End Performance Timing const elapsedTime = Date.now().valueOf() - startTime.valueOf(); const text = "" + elapsedTime + "ms"; canvas.strokeStyle="black"; canvas.fillStyle="white"; canvas.font = "24px arial"; canvas.fillText(text, 0,CANVAS_HEIGHT); canvas.strokeText(text, 0,CANVAS_HEIGHT); On my PC, the chunky naive renderer runs 30ms and the optimized more detailed version runs 60-70ms. My phone is running at the "optimized" version at 380ms. I'm going to start tracking which canvas is actually visible and stop processing the ones which aren't. Even if all this is expensive or inefficient, I don't need to process things that aren't visible. Set up an Itersection Observer to check which canvas elements are visible const observer = new IntersectionObserver(entries => { entries.forEach(canvas => { canvas.__is_visible = canvas.isVisible; }) } ); const allCanvasElements = document.querySelectorAll('canvas'); allCanvasElements.forEach(canvas => observer.observe(canvas)); Per-Pixel Voronoi Grid The renderer is still too slow. Let's see if rectFill is slow and pixel rendering is faster. Utility Functions...+ function putPixel(imageData, x, y, colorRGBA) { const pixelStart = (x + y * imageData.width) * 4; for(let i = 0; i < 4; i++) { imageData.data[pixelStart + i] = colorRGBA[i]; } //imageData.data[ + 0] = (colorRGBA & 0xFF0000) >> 16; //imageData.data[(x + y * imageData.width) * 4 + 1] = (colorRGBA & 0x00FF00) >> 8; //imageData.data[(x + y * imageData.width) * 4 + 2] = colorRGBA & 0x0000FF; //imageData.data[(x + y * imageData.width) * 4 + 3] = 255;; } Utility Functions...+ function color_uint_from_string(colorString) { if(colorString.length == 4) { // "#FDA" // 17 = 16 + 1 :: F => FF , 8 => 88 (in hex) let r = parseInt(colorString.slice(1,2), 16) * 17 let g = parseInt(colorString.slice(2,3), 16) * 17 let b = parseInt(colorString.slice(3,4), 16) * 17 return [r, g, b, 255]; //return r << 16 | g << 8 | b; } else if(colorString.length == 7) { // "#FFDDAA" let r = parseInt(colorString.slice(1,3), 16) let g = parseInt(colorString.slice(3,5), 16) let b = parseInt(colorString.slice(5,7), 16) return [r, g, b, 255]; //return r << 16 | g << 8 | b ; } else { // ??? let r = 255 let g = 0 let b = 255 return [r, g, b, 255]; //return r << 16 | g << 8 | b ; } } Convert the points to RGBA uint values once. Future Point Initialization...+ point.colorUint = color_uint_from_string(point.color) Draw Voronoi Grid Per Pixel Start Performance Timing distance to point helper function const imageData = canvas.createImageData(canvas.canvas.width, canvas.canvas.height); //const imageData = canvas.getImageData(0, 0, canvas.canvas.width, canvas.canvas.height); for(let x = 0; x < CANVAS_WIDTH; x++) { for(let y = 0; y < CANVAS_HEIGHT; y ++) { let bestPoint = VoronoiPoints[0]; let bestDistSq = distance_to_point(x, y, bestPoint) for(let i in VoronoiPoints) { let point = VoronoiPoints[i]; let distSq = distance_to_point(x, y, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } putPixel(imageData, x, y, bestPoint.colorUint); } } canvas.putImageData(imageData, 0, 0); End Performance Timing Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Once...+ { canvas = document.getElementById("canvas6").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Grid Per Pixel } } O(Width * Height * NumControlPoints) = O(N^2*M) = 12,500,000 checks, which is 480ms on my PC. Let's experiment with the chunking again. Draw Voronoi Pixel Optimized Start Performance Timing const CELL_SIZE = 10; const SMALL_CELL_SIZE = 1; function distance_to_point(x, y, point) { return squared(point.x-x) + squared(point.y-y)// + squared(point.z-RENDER_Z) } const imageData = canvas.createImageData(canvas.canvas.width, canvas.canvas.height); for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { let cx = x + CELL_SIZE / 2; for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { let cy = y + CELL_SIZE / 2; let pointsWithPrecalculatedDistances = VoronoiPoints.map( point => [ distance_to_point(cx, cy, point), point ]) let sortedPoints = pointsWithPrecalculatedDistances.sort((a,b) => a[0] - b[0]); sortedPoints.length = Math.min(5, sortedPoints.length); for(let xx = 0; xx < CELL_SIZE; xx += SMALL_CELL_SIZE) { for(let yy = 0; yy < CELL_SIZE; yy += SMALL_CELL_SIZE) { let bestPoint = sortedPoints[0][1]; let bestDistSq = distance_to_point(x + xx, y + yy, bestPoint) for(let i in sortedPoints) { let point = sortedPoints[i][1]; let distSq = distance_to_point(x + xx, y + yy, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } for(let xxx = 0; xxx < SMALL_CELL_SIZE; xxx++) { for(let yyy = 0; yyy < SMALL_CELL_SIZE; yyy++) { putPixel(imageData, x + xx + xxx, y + yy + yyy, bestPoint.colorUint); } } } } } } canvas.putImageData(imageData, 0, 0); End Performance Timing Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Each Frame...+ { canvas = document.getElementById("canvas7").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Pixel Optimized } } This is comparable to the chunky version, but at 4 times the precision. A win, but not an incredible one. __main__ Useful Constants Utility Functions Basic drawing on canvas Point List Definition canvas = document.getElementById("canvas2").getContext("2d") Draw Points As Basic Circles Define Draw Points Function Set up a do_frame function to execute 30 times per second. Set up an Itersection Observer to check which canvas elements are visible Draw Points Once TODO I want to generate something different than HTML and D. I'm not happy with how the title of a code section is disconnected from the body. When I want to explain the code within the article, I have to repeat the text in both the description and the code. Extra newlines in code blocks. Long code lines can break their border and the screen. In javascript some keywords and some standard library functions aren't colored. I want to interlace descriptions and code. I named one section Draw Points Basic and another section Draw Points, and the append Draw Points...+ matched for both. Detect overly vague names. Index:Animated Points: (definition) Attempt to render a chunky pixel more quickly and more accurately.: (definition) 2Basic drawing on canvas: (definition) Calculate best control point per pixel: (definition) 2Calculate the distance from the current pixel cell to the control point and cache it in a pair: (definition) 2callback: 1canvas: 1Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations: (definition) 2Define Draw Points Function: (definition) distance to point helper function: (definition) 2 3do_frame: 1 2 3Draw Points As Basic Circles: 1 (definition) Draw Points Each Frame...+: (definition) (definition) (definition) Draw Points Each Frame: (definition) 2Draw Points Once...+: (definition) Draw the grid cell: (definition) 2Draw Voronoi Grid Optimized: 1 (definition) Draw Voronoi Grid Per Pixel: 1 (definition) Draw Voronoi Pixel Optimized: 1 (definition) End Performance Timing: 1 2 3 (definition) 5Fill the background of the canvas: 1 (definition) For a cell, calculate the list of nearish control points: 1 (definition) Future Point Initialization...+: (definition) (definition) (definition) Future Point Initialization: 1Getting Started: (definition) If the new control point is closer to our pixel than the previous control point, record the new best match.: (definition) 2Initialize the nearest control point: (definition) 2Inspiration: (definition) Javascript: 1Keep points on screen: (definition) 2Limit the control points to only the very nearest points.: 1 (definition) Loop over all the control points, calculating the new distance: (definition) 2Move points around: (definition) 2Optimized Voronoi Grid: (definition) Per-Pixel Voronoi Grid: (definition) Performance Improvements: (definition) Plot each point as a little filled circle: 1 (definition) Point List Definition: (definition) Points: (definition) Prologue: (definition) Set up a do_frame function to execute 30 times per second.: (definition) Set up an Itersection Observer to check which canvas elements are visible: (definition) setInterval: 1 2 3Sort the points based on the pre-calculated distances.: 1 (definition) Start Performance Timing: 1 2 3 (definition) 5Starting HTML: (definition) TODO: (definition) Useful Constants...+: (definition) Useful Constants: (definition) Utility Functions...+: (definition) (definition) (definition) Voronoi Grid Iteration: 1 (definition) Voronoi Grid: (definition) WEB: 1Within a single cell, calculate the actual best point per pixel.: 1 (definition) Prev:WEB 4 Top:WEB 0 Next:WEB 6
I'm still not happy with the performance, so I'm adding a performance tracker to the displays.
Start Performance Timing
const startTime = Date.now()
End Performance Timing
const elapsedTime = Date.now().valueOf() - startTime.valueOf(); const text = "" + elapsedTime + "ms"; canvas.strokeStyle="black"; canvas.fillStyle="white"; canvas.font = "24px arial"; canvas.fillText(text, 0,CANVAS_HEIGHT); canvas.strokeText(text, 0,CANVAS_HEIGHT);
On my PC, the chunky naive renderer runs 30ms and the optimized more detailed version runs 60-70ms. My phone is running at the "optimized" version at 380ms.
I'm going to start tracking which canvas is actually visible and stop processing the ones which aren't. Even if all this is expensive or inefficient, I don't need to process things that aren't visible.
Set up an Itersection Observer to check which canvas elements are visible
const observer = new IntersectionObserver(entries => { entries.forEach(canvas => { canvas.__is_visible = canvas.isVisible; }) } ); const allCanvasElements = document.querySelectorAll('canvas'); allCanvasElements.forEach(canvas => observer.observe(canvas));
The renderer is still too slow. Let's see if rectFill is slow and pixel rendering is faster.
function putPixel(imageData, x, y, colorRGBA) { const pixelStart = (x + y * imageData.width) * 4; for(let i = 0; i < 4; i++) { imageData.data[pixelStart + i] = colorRGBA[i]; } //imageData.data[ + 0] = (colorRGBA & 0xFF0000) >> 16; //imageData.data[(x + y * imageData.width) * 4 + 1] = (colorRGBA & 0x00FF00) >> 8; //imageData.data[(x + y * imageData.width) * 4 + 2] = colorRGBA & 0x0000FF; //imageData.data[(x + y * imageData.width) * 4 + 3] = 255;; }
function color_uint_from_string(colorString) { if(colorString.length == 4) { // "#FDA" // 17 = 16 + 1 :: F => FF , 8 => 88 (in hex) let r = parseInt(colorString.slice(1,2), 16) * 17 let g = parseInt(colorString.slice(2,3), 16) * 17 let b = parseInt(colorString.slice(3,4), 16) * 17 return [r, g, b, 255]; //return r << 16 | g << 8 | b; } else if(colorString.length == 7) { // "#FFDDAA" let r = parseInt(colorString.slice(1,3), 16) let g = parseInt(colorString.slice(3,5), 16) let b = parseInt(colorString.slice(5,7), 16) return [r, g, b, 255]; //return r << 16 | g << 8 | b ; } else { // ??? let r = 255 let g = 0 let b = 255 return [r, g, b, 255]; //return r << 16 | g << 8 | b ; } }
Convert the points to RGBA uint values once.
point.colorUint = color_uint_from_string(point.color)
Draw Voronoi Grid Per Pixel
Start Performance Timing distance to point helper function const imageData = canvas.createImageData(canvas.canvas.width, canvas.canvas.height); //const imageData = canvas.getImageData(0, 0, canvas.canvas.width, canvas.canvas.height); for(let x = 0; x < CANVAS_WIDTH; x++) { for(let y = 0; y < CANVAS_HEIGHT; y ++) { let bestPoint = VoronoiPoints[0]; let bestDistSq = distance_to_point(x, y, bestPoint) for(let i in VoronoiPoints) { let point = VoronoiPoints[i]; let distSq = distance_to_point(x, y, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } putPixel(imageData, x, y, bestPoint.colorUint); } } canvas.putImageData(imageData, 0, 0); End Performance Timing
Draw Points Once...+
{ canvas = document.getElementById("canvas6").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Grid Per Pixel } }
O(Width * Height * NumControlPoints) = O(N^2*M) = 12,500,000 checks, which is 480ms on my PC. Let's experiment with the chunking again.
Draw Voronoi Pixel Optimized Start Performance Timing const CELL_SIZE = 10; const SMALL_CELL_SIZE = 1; function distance_to_point(x, y, point) { return squared(point.x-x) + squared(point.y-y)// + squared(point.z-RENDER_Z) } const imageData = canvas.createImageData(canvas.canvas.width, canvas.canvas.height); for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { let cx = x + CELL_SIZE / 2; for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { let cy = y + CELL_SIZE / 2; let pointsWithPrecalculatedDistances = VoronoiPoints.map( point => [ distance_to_point(cx, cy, point), point ]) let sortedPoints = pointsWithPrecalculatedDistances.sort((a,b) => a[0] - b[0]); sortedPoints.length = Math.min(5, sortedPoints.length); for(let xx = 0; xx < CELL_SIZE; xx += SMALL_CELL_SIZE) { for(let yy = 0; yy < CELL_SIZE; yy += SMALL_CELL_SIZE) { let bestPoint = sortedPoints[0][1]; let bestDistSq = distance_to_point(x + xx, y + yy, bestPoint) for(let i in sortedPoints) { let point = sortedPoints[i][1]; let distSq = distance_to_point(x + xx, y + yy, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } for(let xxx = 0; xxx < SMALL_CELL_SIZE; xxx++) { for(let yyy = 0; yyy < SMALL_CELL_SIZE; yyy++) { putPixel(imageData, x + xx + xxx, y + yy + yyy, bestPoint.colorUint); } } } } } } canvas.putImageData(imageData, 0, 0); End Performance Timing Add our optimized Voronoi grid draw code to the setInterval do_frame callback. Draw Points Each Frame...+ { canvas = document.getElementById("canvas7").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Pixel Optimized } } This is comparable to the chunky version, but at 4 times the precision. A win, but not an incredible one. __main__ Useful Constants Utility Functions Basic drawing on canvas Point List Definition canvas = document.getElementById("canvas2").getContext("2d") Draw Points As Basic Circles Define Draw Points Function Set up a do_frame function to execute 30 times per second. Set up an Itersection Observer to check which canvas elements are visible Draw Points Once TODO I want to generate something different than HTML and D. I'm not happy with how the title of a code section is disconnected from the body. When I want to explain the code within the article, I have to repeat the text in both the description and the code. Extra newlines in code blocks. Long code lines can break their border and the screen. In javascript some keywords and some standard library functions aren't colored. I want to interlace descriptions and code. I named one section Draw Points Basic and another section Draw Points, and the append Draw Points...+ matched for both. Detect overly vague names. Index:Animated Points: (definition) Attempt to render a chunky pixel more quickly and more accurately.: (definition) 2Basic drawing on canvas: (definition) Calculate best control point per pixel: (definition) 2Calculate the distance from the current pixel cell to the control point and cache it in a pair: (definition) 2callback: 1canvas: 1Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations: (definition) 2Define Draw Points Function: (definition) distance to point helper function: (definition) 2 3do_frame: 1 2 3Draw Points As Basic Circles: 1 (definition) Draw Points Each Frame...+: (definition) (definition) (definition) Draw Points Each Frame: (definition) 2Draw Points Once...+: (definition) Draw the grid cell: (definition) 2Draw Voronoi Grid Optimized: 1 (definition) Draw Voronoi Grid Per Pixel: 1 (definition) Draw Voronoi Pixel Optimized: 1 (definition) End Performance Timing: 1 2 3 (definition) 5Fill the background of the canvas: 1 (definition) For a cell, calculate the list of nearish control points: 1 (definition) Future Point Initialization...+: (definition) (definition) (definition) Future Point Initialization: 1Getting Started: (definition) If the new control point is closer to our pixel than the previous control point, record the new best match.: (definition) 2Initialize the nearest control point: (definition) 2Inspiration: (definition) Javascript: 1Keep points on screen: (definition) 2Limit the control points to only the very nearest points.: 1 (definition) Loop over all the control points, calculating the new distance: (definition) 2Move points around: (definition) 2Optimized Voronoi Grid: (definition) Per-Pixel Voronoi Grid: (definition) Performance Improvements: (definition) Plot each point as a little filled circle: 1 (definition) Point List Definition: (definition) Points: (definition) Prologue: (definition) Set up a do_frame function to execute 30 times per second.: (definition) Set up an Itersection Observer to check which canvas elements are visible: (definition) setInterval: 1 2 3Sort the points based on the pre-calculated distances.: 1 (definition) Start Performance Timing: 1 2 3 (definition) 5Starting HTML: (definition) TODO: (definition) Useful Constants...+: (definition) Useful Constants: (definition) Utility Functions...+: (definition) (definition) (definition) Voronoi Grid Iteration: 1 (definition) Voronoi Grid: (definition) WEB: 1Within a single cell, calculate the actual best point per pixel.: 1 (definition) Prev:WEB 4 Top:WEB 0 Next:WEB 6
Draw Voronoi Pixel Optimized
Start Performance Timing const CELL_SIZE = 10; const SMALL_CELL_SIZE = 1; function distance_to_point(x, y, point) { return squared(point.x-x) + squared(point.y-y)// + squared(point.z-RENDER_Z) } const imageData = canvas.createImageData(canvas.canvas.width, canvas.canvas.height); for(let x = 0; x < CANVAS_WIDTH; x += CELL_SIZE) { let cx = x + CELL_SIZE / 2; for(let y = 0; y < CANVAS_HEIGHT; y += CELL_SIZE) { let cy = y + CELL_SIZE / 2; let pointsWithPrecalculatedDistances = VoronoiPoints.map( point => [ distance_to_point(cx, cy, point), point ]) let sortedPoints = pointsWithPrecalculatedDistances.sort((a,b) => a[0] - b[0]); sortedPoints.length = Math.min(5, sortedPoints.length); for(let xx = 0; xx < CELL_SIZE; xx += SMALL_CELL_SIZE) { for(let yy = 0; yy < CELL_SIZE; yy += SMALL_CELL_SIZE) { let bestPoint = sortedPoints[0][1]; let bestDistSq = distance_to_point(x + xx, y + yy, bestPoint) for(let i in sortedPoints) { let point = sortedPoints[i][1]; let distSq = distance_to_point(x + xx, y + yy, point) if(distSq < bestDistSq) { bestDistSq = distSq; bestPoint = point; } } for(let xxx = 0; xxx < SMALL_CELL_SIZE; xxx++) { for(let yyy = 0; yyy < SMALL_CELL_SIZE; yyy++) { putPixel(imageData, x + xx + xxx, y + yy + yyy, bestPoint.colorUint); } } } } } } canvas.putImageData(imageData, 0, 0); End Performance Timing
{ canvas = document.getElementById("canvas7").getContext("2d") if(canvas.__is_visible != false) { Draw Voronoi Pixel Optimized } }
This is comparable to the chunky version, but at 4 times the precision. A win, but not an incredible one. __main__ Useful Constants Utility Functions Basic drawing on canvas Point List Definition canvas = document.getElementById("canvas2").getContext("2d") Draw Points As Basic Circles Define Draw Points Function Set up a do_frame function to execute 30 times per second. Set up an Itersection Observer to check which canvas elements are visible Draw Points Once TODO I want to generate something different than HTML and D. I'm not happy with how the title of a code section is disconnected from the body. When I want to explain the code within the article, I have to repeat the text in both the description and the code. Extra newlines in code blocks. Long code lines can break their border and the screen. In javascript some keywords and some standard library functions aren't colored. I want to interlace descriptions and code. I named one section Draw Points Basic and another section Draw Points, and the append Draw Points...+ matched for both. Detect overly vague names. Index:Animated Points: (definition) Attempt to render a chunky pixel more quickly and more accurately.: (definition) 2Basic drawing on canvas: (definition) Calculate best control point per pixel: (definition) 2Calculate the distance from the current pixel cell to the control point and cache it in a pair: (definition) 2callback: 1canvas: 1Convert or map the raw points into pairs with the distances pre-calculated so we do not repeat the distance calculations: (definition) 2Define Draw Points Function: (definition) distance to point helper function: (definition) 2 3do_frame: 1 2 3Draw Points As Basic Circles: 1 (definition) Draw Points Each Frame...+: (definition) (definition) (definition) Draw Points Each Frame: (definition) 2Draw Points Once...+: (definition) Draw the grid cell: (definition) 2Draw Voronoi Grid Optimized: 1 (definition) Draw Voronoi Grid Per Pixel: 1 (definition) Draw Voronoi Pixel Optimized: 1 (definition) End Performance Timing: 1 2 3 (definition) 5Fill the background of the canvas: 1 (definition) For a cell, calculate the list of nearish control points: 1 (definition) Future Point Initialization...+: (definition) (definition) (definition) Future Point Initialization: 1Getting Started: (definition) If the new control point is closer to our pixel than the previous control point, record the new best match.: (definition) 2Initialize the nearest control point: (definition) 2Inspiration: (definition) Javascript: 1Keep points on screen: (definition) 2Limit the control points to only the very nearest points.: 1 (definition) Loop over all the control points, calculating the new distance: (definition) 2Move points around: (definition) 2Optimized Voronoi Grid: (definition) Per-Pixel Voronoi Grid: (definition) Performance Improvements: (definition) Plot each point as a little filled circle: 1 (definition) Point List Definition: (definition) Points: (definition) Prologue: (definition) Set up a do_frame function to execute 30 times per second.: (definition) Set up an Itersection Observer to check which canvas elements are visible: (definition) setInterval: 1 2 3Sort the points based on the pre-calculated distances.: 1 (definition) Start Performance Timing: 1 2 3 (definition) 5Starting HTML: (definition) TODO: (definition) Useful Constants...+: (definition) Useful Constants: (definition) Utility Functions...+: (definition) (definition) (definition) Voronoi Grid Iteration: 1 (definition) Voronoi Grid: (definition) WEB: 1Within a single cell, calculate the actual best point per pixel.: 1 (definition) Prev:WEB 4 Top:WEB 0 Next:WEB 6
This is comparable to the chunky version, but at 4 times the precision. A win, but not an incredible one.
__main__
Useful Constants Utility Functions Basic drawing on canvas Point List Definition canvas = document.getElementById("canvas2").getContext("2d") Draw Points As Basic Circles Define Draw Points Function Set up a do_frame function to execute 30 times per second. Set up an Itersection Observer to check which canvas elements are visible Draw Points Once