4

I have a simple Canvas drawing app. Sometimes the lineTo() command produces a line of less coordinates and the drawing has many edges:

enter image description here

I'm using the latest firefox, is it because the connection is bad or my computer is buisy? Is there a work around? Here is my code: JS FIDDLE

beginPath();
                moveTo(this.X, this.Y);
                lineTo(e.pageX , e.pageY );
                strokeStyle = "rgb(0,0,0)";
                ctx.lineWidth=3;
                stroke();
daisy
  • 615
  • 2
  • 8
  • 15
  • You could experiment with using the mousemove points (the vertices shown in the above screenshot) to draw curves rather than straight lines (not sure if that's practical though). With straight lines, you're at the mercy of however quickly the browser is able to re-render the canvas (plus occasional hiccups where the browser or OS is busy doing something else). – Matt Coughlin Jun 24 '13 at 18:21
  • Yes that's a good idea - you could use Bezier or Catmull-Rom splines to smooth it out, though that can produce some odd effects sometimes. – Pointy Jun 24 '13 at 18:29
  • yeah, but then the stroke wil not be similar to my mouse movement, it would be just a kind of interpolation. And to calculate the curves it will take extra time – daisy Jun 24 '13 at 18:41
  • Try scrubbing the mouse quickly in Photoshop while drawing and see what happens there. (Hint: it's about the same.) – Phrogz Jun 25 '13 at 04:42

2 Answers2

4

It's responding as fast as it can. Your browser will deliver events as fast as it can, but it's not in any way guaranteed to be able to track you moving the mouse. A lot has to do with the load on the client machine.

edithere is a modified fiddle demonstrating some ways you might make it a little better. That version keeps a separate "points" queue that draws new points every 50 milliseconds. That makes it so that the "mousemove" handler only has to log the point coordinates from the event, and the drawing code can do a bunch of points with one canvas update when the mouse is moving quickly. It's still not perfect.

 var canvas = document.getElementById('canvas');
        var ctx = canvas.getContext('2d');
        var width  = window.innerWidth;
        var height = window.innerHeight;
        canvas.height = height;
        canvas.width = width;
        canvas.addEventListener('mousedown', function(e) {
            this.down = true;
            points.setStart(e.pageX, e.pageY);
        }, 0);
        canvas.addEventListener('mouseup', function() {
            this.down = false;          
        }, 0);
        canvas.addEventListener('mousemove', function(e) {          
            if (this.down) {
                points.newPoint(e.pageX, e.pageY);
            }
        }, 0);

var points = function() {
    var queue = [], qi = 0;
    var ctx = canvas.getContext('2d');

    function clear() {
        queue = [];
        qi = 0;
    }

    function setStart(x, y) {
        clear();
        newPoint(x, y);
    }

    function newPoint(x, y) {
        queue.push([x, y]);
    }

    function tick() {

        var k = 20; // adjust to limit points drawn per cycle

        if (queue.length - qi > 1) {
            ctx.beginPath();
            if (qi === 0)
                ctx.moveTo(queue[0][0], queue[0][1]);
            else
                ctx.moveTo(queue[qi - 1][0], queue[qi - 1][1]);

            for (++qi; --k >= 0 && qi < queue.length; ++qi) {
                ctx.lineTo(queue[qi][0], queue[qi][1]);
            }

            ctx.strokeStyle = "rgb(0,0,0)";
            ctx.lineWidth = 3;
            ctx.stroke();
        }
    }

    setInterval(tick, 50); // adjust cycle time

    return {
        setStart: setStart,
        newPoint: newPoint
    };
}();
Pointy
  • 405,095
  • 59
  • 585
  • 614
  • +1 Inventive use of the points queue + timed draws! On the "k" throttle, have you though about drawing every queue.length/k element instead of just the first k elements. Nice answer! – markE Jun 24 '13 at 19:09
  • @markE well the idea of the limit was to try and avoid blocking mouse response, so it seemed like having a fixed maximum amount of drawing would be good. I suppose one could also compute the net line length, though that'd involve a little more math. – Pointy Jun 24 '13 at 19:09
3

You can use a cardinal spline to smooth out lines like this:

enter image description here

The cause is as @Pointy already explained due to how fast the browser is able to respond to the events (mousemove). There is an API called Pointer Lock API which might help solve this in the future as it is more low-level, but for now we need to use algorithms to smooth out lines appearing segmented due to this.

In addition to smoothing there is detail-smoothing, point reduction, taper and other things that can be applied to improve the result.

But in this particular case you can use the following function which I made as an extension to the canvas. Just call it:

ctx.curve(myPointArray, tension, segments);
ctx.stroke();

The array contains your x and y points ordered like [x1, y1, x2, y2, ... xn, yn.

A typical value for tension is 0.5. segments (default 16) is optional.

The more tension the more round the curve will appear. Segments are the resolution between each point in the array. For drawing application a value of 5 might work fine (less resulting points).

To make it work better you could register your points on a separate canvas where you draw the original line. At mouse up process the line with this function and draw it to the main canvas and then clear the drawing canvas.

This function is highly optimized - it also returns the processed points so you can store the result instead of re-processing every time.

/**
 *  curve() by Ken Fyrstenberg (c) 2013 Epistemex
 *  See Code Project for full source:
 *  http://www.codeproject.com/Tips/562175/Draw-Smooth-Lines-on-HTML5-Canvas
*/
CanvasRenderingContext2D.prototype.curve = function(pts, ts, nos) {

    nos = (typeof numOfSegments === 'undefined') ? 16 : nos;

    var _pts = [], res = [],        // clone array
        x, y,                       // our x,y coords
        t1x, t2x, t1y, t2y,         // tension vectors
        c1, c2, c3, c4,             // cardinal points
        st, st2, st3, st23, st32,   // steps
        t, i, l = pts.length,
        pt1, pt2, pt3, pt4;

    _pts.push(pts[0]);          //copy 1. point and insert at beginning
    _pts.push(pts[1]);

    _pts = _pts.concat(pts);

    _pts.push(pts[l - 2]);  //copy last point and append
    _pts.push(pts[l - 1]);

    this.moveTo(pts[0], pts[1])

    for (i = 2; i < l; i+=2) {

        pt1 = _pts[i];
        pt2 = _pts[i+1];
        pt3 = _pts[i+2];
        pt4 = _pts[i+3];

        // calc tension vectors
        t1x = (pt3 - _pts[i-2]) * ts;
        t2x = (_pts[i+4] - pt1) * ts;

        t1y = (pt4 - _pts[i-1]) * ts;
        t2y = (_pts[i+5] - pt2) * ts;

        for (t = 0; t <= nos; t++) {

            // pre-calc steps
            st = t / nos;
            st2 = st * st;
            st3 = st2 * st;
            st23 = st3 * 2;
            st32 = st2 * 3;

            // calc cardinals
            c1 = st23 - st32 + 1; 
            c2 = st32 - st23;
            c3 = st3 - 2 * st2 + st; 
            c4 = st3 - st2;

            res.push(c1 * pt1 + c2 * pt3 + c3 * t1x + c4 * t2x);
            res.push(c1 * pt2 + c2 * pt4 + c3 * t1y + c4 * t2y);

        } //for t
    } //for i

    l = res.length;
    for(i=0;i<l;i+=2) this.lineTo(res[i], res[i+1]);

    return res;

} //func ext

See this answer for an implementation of a cardinal spline.

  • and what will happen if I want draw something with hard edges? – daisy Jul 01 '13 at 16:34
  • @daisy usually this is handled by calculating a knee value - how much bend to tolerate before considering a "new " stroke. A knee value is the degree between the previous line and current line (before current is drawn). If above a threshold (f.ex. 50-60 degrees) then a new stroke is generated allowing you to take a hard turn without smoothing everything. –  Jul 01 '13 at 17:07
  • @daisy as you also record the points for the stroke you can also provide parameters to adjust these thresholds post stroke. –  Jul 01 '13 at 17:08