javascript - how to make a fast NOT anti-aliasing HTML5canvas basic drawing function? -
i trying anti-aliasing drawing function in canvas. super answers on site canvas , aliasing.
here's demo: https://jsfiddle.net/garciavvv/eu34c8sy/12/
here's line js:
function linexy(mousex, mousey, mousexx, mouseyy){ var x0= mousex; var y0= mousey; var x1= mousexx; var y1= mouseyy; var coordinatesarray = []; // translate coordinates // define differences , error check var dx = math.abs(x1 - x0); var dy = math.abs(y1 - y0); var sx = (x0 < x1) ? 1 : -1; var sy = (y0 < y1) ? 1 : -1; var err = dx - dy; // set first coordinates coordinatesarray.push([x0,y0]); // main loop while (!((x0 == x1) && (y0 == y1))) { var e2 = err << 1; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } // set coordinates coordinatesarray.push([x0,y0]); // return result } for(var i=0;i<coordinatesarray.length;i++) { aliasedcircle(ctx, coordinatesarray[i][0], coordinatesarray[i][1], 100); } }
what make jerky while drawing fast large pen ? , how make sweet?
thanks
the main reason of course quite large number of paths generated, first circle , line reproduces circle paths x length per pixel.
there couple of things can improve this:
we can cache circle image , use bitmap brush. eliminates need regenerate lines in circle each point in line. brush needs updated when size or color changes.
we don't have draw each point of line, can find way calculate how many pixels can skip before need draw, better option is:
we can "cheat" drawing thick line between first , last point instead of drawing circle each point.
and finally, can register mouse on each frame instead of each event reduce load.
the first point simple enough: create offscreen canvas size of brush (diameter) , draw in. change color either regenerate brush (or use composite mode , draw on it):
// show brush document.body.appendchild(createbrush(150, "#09f")); function createbrush(radius, color) { var ctx = document.createelement("canvas").getcontext("2d"); ctx.canvas.width = ctx.canvas.height = radius<<1; ctx.fillstyle = color; aliasedcircle(ctx, radius, radius, radius); ctx.fill(); return ctx.canvas } function aliasedcircle(ctx, xc, yc, r) { // note: fill only! var x = r, y = 0, cd = 0; // middle line ctx.rect(xc - x, yc, r<<1, 1); while (x > y) { cd -= (--x) - (++y); if (cd < 0) cd += x++; ctx.rect(xc - y, yc - x, y<<1, 1); // upper 1/4 ctx.rect(xc - x, yc - y, x<<1, 1); // upper 2/4 ctx.rect(xc - x, yc + y, x<<1, 1); // lower 3/4 ctx.rect(xc - y, yc + x, y<<1, 1); // lower 4/4 } }
now have image/bitmap brush can @ how draw line. can use 2 approaches. since want aliased have compromise somehow.
using bresenham draw , fill line can slow in context we're working. drawing circle multiple times slow well.
an third option use context's own line , "hack" edges (of course, if improve filling bucket fill, ref. previous question, spend energy on improving bucket fill algorithm instead :) ).
so lets try third option. need both internal line mechanism bresenham. challenge make bresenham cover edge exactly.
var ctx = c.getcontext("2d"); drawline(ctx, 60, 60, 250, 210, 50); ctx.stroke(); function drawline(ctx, x1, y1, x2, y2, radius) { ctx.moveto(x1, y1); ctx.lineto(x2, y2); ctx.linewidth = radius<<1; ctx.linecap = "butt"; }
<canvas id=c height=300></canvas>
lets add bresenham, actually, lets use faster line algorithm: efla , try match edges - now, may not perfect in cases , offset (or rather line width of native draw op.) may have adjusted.
we needs calculate 90° offset angle both side. instead of adding , subtracting 90° can switch cos/sin instead.
var ctx = c.getcontext("2d"); var x1 = 60, y1 = 60, x2 = 250, y2 = 210, r = 50; ctx.globalalpha = 0.25; drawline(ctx, x1, y1, x2, y2, r); ctx.stroke(); ctx.beginpath(); ctx.globalalpha = 1; // calc angle var diffx = x2 - x1, diffy = y2 - y1, angle = math.atan2(diffy, diffx); // 2 edge lines offset per angle var lx1 = x1 - r * math.sin(angle), ly1 = y1 + r * math.cos(angle), lx2 = x2 - r * math.sin(angle), ly2 = y2 + r * math.cos(angle), rx1 = x1 + r * math.sin(angle), ry1 = y1 - r * math.cos(angle), rx2 = x2 + r * math.sin(angle), ry2 = y2 - r * math.cos(angle); fastline(ctx, lx1|0, ly1|0, lx2|0, ly2|0); fastline(ctx, rx1|0, ry1|0, rx2|0, ry2|0); ctx.fill(); function drawline(ctx, x1, y1, x2, y2, radius) { ctx.moveto(x1, y1); ctx.lineto(x2, y2); ctx.linewidth = radius<<1; ctx.linecap = "butt"; } function fastline(ctx, x1, y1, x2, y2) { var dlt, mul, sl = y2 - y1, ll = x2 - x1, yl = false, lls = ll >> 31, sls = sl >> 31, i; if ((sl ^ sls) - sls > (ll ^ lls) - lls) { sl ^= ll; ll ^= sl; sl ^= ll; yl = true } dlt = ll < 0 ? -1 : 1; mul = (ll === 0) ? sl : sl / ll; if (yl) { x1 += 0.5; (i = 0; !== ll; += dlt) ctx.rect((x1 + * mul)|0, y1 + i, 1, 1) } else { y1 += 0.5; (i = 0; !== ll; += dlt) ctx.rect(x1 + i, (y1 + * mul)|0, 1, 1) } }
<canvas id=c height=300></canvas>
and finally, if merge components , refactor little neat aliased line drawing mechanism utilizes these approaches:
var ctx = c.getcontext("2d"); var x1 = 0, y1 = 0, r = 90; var brush = createbrush(r, "#000"); document.queryselector("button").onclick = function() { ctx.beginpath(); ctx.clearrect(0,0,c.width,c.height); }; // mouse move handler using raf. c.onmousemove = function(e) { requestanimationframe(function() { var x2 = e.clientx|0, y2=e.clienty|0; aliasedline(ctx, x1, y1, x2, y2, r); x1 = x2; y1 = y2; }) }; function aliasedline(ctx, x1, y1, x2, y2, radius) { // calc angle var diffx = x2 - x1, diffy = y2 - y1, angle = math.atan2(diffy, diffx), // 2 edge lines offset per angle lx1 = x1 - radius * math.sin(angle), ly1 = y1 + radius * math.cos(angle), lx2 = x2 - radius * math.sin(angle), ly2 = y2 + radius * math.cos(angle), rx1 = x1 + radius * math.sin(angle), ry1 = y1 - radius * math.cos(angle), rx2 = x2 + radius * math.sin(angle), ry2 = y2 - radius * math.cos(angle); // main line ctx.beginpath(); drawline(ctx, x1, y1, x2, y2, radius); ctx.stroke(); // aliased edges ctx.beginpath(); fastline(ctx, lx1|0, ly1|0, lx2|0, ly2|0); fastline(ctx, rx1|0, ry1|0, rx2|0, ry2|0); ctx.fill(); // caps ctx.drawimage(brush, x1 - radius, y1 - radius) ctx.drawimage(brush, x2 - radius, y2 - radius) } function createbrush(radius, color) { var ctx = document.createelement("canvas").getcontext("2d"); ctx.canvas.width = ctx.canvas.height = 1 + radius<<1; ctx.fillstyle = color; aliasedcircle(ctx, radius, radius, radius); ctx.fill(); return ctx.canvas } function aliasedcircle(ctx, xc, yc, r) { // note: fill only! var x = r, y = 0, cd = 0; // middle line ctx.rect(xc - x, yc, r<<1, 1); while (x > y) { cd -= (--x) - (++y); if (cd < 0) cd += x++; ctx.rect(xc - y, yc - x, y<<1, 1); // upper 1/4 ctx.rect(xc - x, yc - y, x<<1, 1); // upper 2/4 ctx.rect(xc - x, yc + y, x<<1, 1); // lower 3/4 ctx.rect(xc - y, yc + x, y<<1, 1); // lower 4/4 } } function drawline(ctx, x1, y1, x2, y2, radius) { ctx.moveto(x1, y1); ctx.lineto(x2, y2); ctx.linewidth = radius<<1; } function fastline(ctx, x1, y1, x2, y2) { var dlt, mul, sl = y2 - y1, ll = x2 - x1, yl = false, lls = ll >> 31, sls = sl >> 31, i; if ((sl ^ sls) - sls > (ll ^ lls) - lls) { sl ^= ll; ll ^= sl; sl ^= ll; yl = true } dlt = ll < 0 ? -1 : 1; mul = (ll === 0) ? sl : sl / ll; if (yl) { x1 += 0.5; (i = 0; !== ll; += dlt) ctx.rect((x1 + * mul)|0, y1 + i, 1, 1) } else { y1 += 0.5; (i = 0; !== ll; += dlt) ctx.rect(x1 + i, (y1 + * mul)|0, 1, 1) } }
#c {background:#aaa}
<canvas id=c width=1200 height=800></canvas> <br><button>clear</button>
some final notes: aware of may not perfect, alias-wise, in particular in 0/90° lines. because due number of samples there can sit many points making fine gradual line efla line cannot cover single pixel point.
one alternative make polygon fill (like scanline) implementation. it's little more math , steps involved doable acceptable performance.
Comments
Post a Comment