Skip to content

Instantly share code, notes, and snippets.

@automat
Last active August 29, 2015 14:05
Show Gist options
  • Save automat/653c7ba3489f1005c2b1 to your computer and use it in GitHub Desktop.
Save automat/653c7ba3489f1005c2b1 to your computer and use it in GitHub Desktop.
Canvas - Flush fill vs. direct fill

Canvas - Flush fill vs. direct fill

A quick test to see if filling group of shapes is more performant than filling each shape separately. FillStyle is wrapped to imitate the processing api, in 'normal' mode 'fill' just sets the current fillStyle, ellipse() constructs a new path via 'beginPath' and immediately closes it by applying the fillStyle with 'fill'. In 'flush' mode 'fill' already begins to construct a new path via 'beginPath' right after setting the fillStyle. All subsequent calls to ellipse only append new path segments to the same path. The current path gets closed by calling 'fill' again or automatically right after draw().

'Flush' filling shape segments increases the frame rate about 2-4 fps when drawing large amount of shapes, but introduces some fill order errors. Calling fill right after a single shape is finished is actually faster when dealing with smaller amounts of shapes (max.2500).

This needs some proper benchmarks.

function test(canvas){
// Setup
var width = canvas.width,
height = canvas.height;
var ctx = canvas.getContext('2d');
var timeStart = Date.now(),
secondsElapsed = 0;
// Draw internal
const s_kappa = .5522848;
var s_x,s_y,
s_sx,s_sy,
s_sx_05, s_sy_05;
var s_ox, s_oy;
var s_xe, s_ye, s_xm, s_ym;
var s_cp1x_0,s_cp1y_0,s_cp2x_0,s_cp2y_0,s_x_0,s_y_0,
s_cp1x_1,s_cp1y_1,s_cp2x_1,s_cp2y_1,s_x_1,s_y_1,
s_cp1x_2,s_cp1y_2,s_cp2x_2,s_cp2y_2,s_x_2,s_y_2,
s_cp1x_3,s_cp1y_3,s_cp2x_3,s_cp2y_3,s_x_3,s_y_3;
const scaleThreshold = 0.05;
var numEllipsesDrawn;
function ellipse_internal(x,y,sx,sy) {
if(sx < scaleThreshold || sy < scaleThreshold){
return;
} else if (!(x == s_x && y == s_y && sx == s_sx && sy == s_sy)) {
if (sx != s_sx) {
s_sx = sx;
s_sx_05 = s_sx * 0.5;
s_ox = s_sx_05 * s_kappa;
}
if (sy != s_sy) {
s_sy = sy;
s_sy_05 = s_sy * 0.5;
s_oy = s_sy_05 * s_kappa;
}
s_cp1x_0 = s_cp2x_3 = s_x_3 = s_x = x - s_sx_05;
s_cp2y_0 = s_y_0 = s_cp1y_1 = s_y = y - s_sy_05;
s_cp2x_1 = s_x_1 = s_cp1x_2 = s_xe = s_x + s_sx;
s_cp2y_2 = s_y_2 = s_cp1y_3 = s_ye = s_y + s_sy;
s_x_0 = s_x_2 = s_xm = s_x + s_sx_05;
s_y_1 = s_y_3 = s_ym = s_y + s_sy_05;
s_cp1y_0 = s_cp2y_1 = s_ym - s_oy;
s_cp2x_0 = s_cp1x_3 = s_xm - s_ox;
s_cp1y_2 = s_cp2y_3 = s_ym + s_oy;
s_cp1x_1 = s_cp2x_2 = s_xm + s_ox;
}
ctx.moveTo(s_x, s_ym);
ctx.bezierCurveTo(s_cp1x_0, s_cp1y_0, s_cp2x_0, s_cp2y_0, s_x_0, s_y_0);
ctx.bezierCurveTo(s_cp1x_1, s_cp1y_1, s_cp2x_1, s_cp2y_1, s_x_1, s_y_1);
ctx.bezierCurveTo(s_cp1x_2, s_cp1y_2, s_cp2x_2, s_cp2y_2, s_x_2, s_y_2);
ctx.bezierCurveTo(s_cp1x_3, s_cp1y_3, s_cp2x_3, s_cp2y_3, s_x_3, s_y_3);
numEllipsesDrawn++;
}
var prevR,prevG,prevB,prevA;
function fillFlush(r,g,b,a){
ctx.fill();
if(r == prevR && g == prevG && b == prevB && a == prevA){
return;
}
ctx.fillStyle = 'rgba(' + r + ',' + (g || 0) + ',' + (b || 0) + ',' + (a === undefined ? 1.0 : a) + ')';
ctx.beginPath();
prevR = r;
prevG = g;
prevB = b;
prevA = a;
}
function fillDirect(r,g,b,a){
if(r == prevR && g == prevG && b == prevB && a == prevA){
return;
}
ctx.fillStyle = 'rgba(' + r + ',' + (g || 0) + ',' + (b || 0) + ',' + (a === undefined ? 1.0 : a)+ ')';
prevR = r;
prevG = g;
prevB = b;
prevA = a;
}
function ellipseDirect(x,y,sx,sy){
ctx.beginPath();
ellipse_internal(x,y,sx,sy);
ctx.fill();
}
function ellipseFlush(x,y,sx,sy){
ellipse_internal(x,y,sx,sy);
}
const PI = Math.PI,
HALF_PI = PI * 0.5,
QUARTER_PI = PI * 0.25,
TWO_PI = PI * 2;
var fill,ellipse;
function draw(){
ctx.clearRect(0,0,width,height);
ctx.fillStyle = 'rgb(0,0,255)';
ctx.fillRect(0,0,width,height);
if(RENDER_MODE == 0){
fill = fillDirect;
ellipse = ellipseDirect;
} else {
fill = fillFlush;
ellipse = ellipseFlush;
}
var i,j;
var a, b, c, d, e, f;
var width_05 = width * 0.5;
i = -1;
a = 40;
b = 1 / (a-1);
while(++i < a){
c = i * b;
j = -1;
while(++j < a){
d = j * b;
e = (0.5 + Math.cos(c * PI * 10 + secondsElapsed) * Math.sin(d * PI * 4 + secondsElapsed) * 0.5) * 50;
fill(~~(c*255),0,~~(d*255));
ellipse(c * width_05,d * 175,e,e);
}
}
a = ~~(secondsElapsed * 2) % 2 == 0 ? 255 : 0;
fill(a,a,a);
ellipse(100,150,50,50);
ellipse(350,150,50,50);
a = 1/9;
i = -1;
while(++i < 10){
b = i * a * PI;
c = Math.sin(b + secondsElapsed * 4)* 40;
ellipse(225 + Math.cos(b) * 125,250 + Math.sin(b) * 125,c,c);
}
ctx.save();
ctx.translate(width_05,0);
a = 50;
b = 1 / (a-1);
i = -1;
while(++i < a){
c = i * b;
j = -1;
while(++j < a){
d = j * b;
e = (0.5 + Math.cos(c * PI * 10 + secondsElapsed) * Math.sin(d * PI * 2 + secondsElapsed) * 0.5) * 10;
ellipse(c * width_05,d * height,e,e);
}
}
ctx.translate(150,150);
a = 1/499;
i = -1;
while(++i < 500){
b = i * a;
c = QUARTER_PI * 1.25 + i * a * (PI + QUARTER_PI * 1.5);
d = (0.5 + Math.sin(b * PI * 6 + secondsElapsed * 8) * Math.sin(b * PI * 4 + secondsElapsed )) * 50;
e = 100 + Math.cos(c) * 150;
f = 100 + Math.sin(c) * 150;
fill(0,0,0);
ellipse(e,f,d,d);
fill(255,255,255);
ellipse(e-3,f+1,d,d);
fill(255,0,0);
ellipse(e-2,f-2,d,d);
}
ctx.restore();
}
function drawLoop(){
stats.begin();
ctx.save();
draw();
if(RENDER_MODE == 1){
ctx.fill();
}
ctx.restore();
numEllipsesDisplay.innerHTML = 'Num ellipses displayed: ' + numEllipsesDrawn;
numEllipsesDrawn = 0;
prevR = prevG = prevB = prevA = null;
secondsElapsed = (Date.now() - timeStart) / 1000;
stats.end();
requestAnimationFrame(drawLoop,null);
}
drawLoop();
}
window.addEventListener('load',function(){
test(document.getElementsByTagName('canvas')[0]);
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body{
padding: 0;
margin: 0;
}
canvas{
vertical-align: bottom;
}
#options {
position: absolute;
float: left;
left: 10px;
top: 10px;
color: #fff;
font-size: 10px;
width: 100%;
font-family: Helvetica, Arial, sans-serif;
font-weight: bolder;
}
</style>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/stats.js/r11/Stats.js"></script>
<script>
var stats = new Stats();
var statsElement = stats.domElement;
statsElement.style.position = 'absolute';
statsElement.style.left = '880px';
statsElement.style.top = '0px';
var numEllipsesDisplay;
var RENDER_MODE;
window.addEventListener('load', function () {
document.body.appendChild( statsElement );
var selectMode = document.getElementById('mode');
selectMode.addEventListener('change',function(){
RENDER_MODE = this.selectedIndex;
});
RENDER_MODE = selectMode.selectedIndex;
numEllipsesDisplay = document.getElementById('num-ellipses');
});
</script>
<script type="text/javascript" src="app.js"></script>
</head>
<body>
<canvas width="960" height="500"></canvas>
<div id="options">
<select id="mode">
<option>normal</option>
<option>flush</option>
</select>
<p id="num-ellipses"></p>
</div>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment