Mouse Path Smoothing for Jack Lumber

posted by harrison on March 23, 2013

Some months ago, we were working on a mouse path smoothing solution for a desktop version of Jack Lumber. There are two related problems here: one is that the game is noticeably harder with a mouse than with a greasy finger, and the other is that the trail left behind a mouse can be jagged and ugly. I’m going to talk about the latter here.

The problem is most noticeable when moving the mouse slowly. Go ahead, slowly draw a curved line here (drag to draw):

If you’re using a mouse, you might see that your line looks only slightly better than the same curve on an etch-a-sketch. It gets even worse if you have a wide, stylized line.

The first solution i attempted was to use an exponential moving average instead of the current mouse position. So, the point added to the line could be described as:

    \[ p_n = p_{n-1} (1-\alpha) + p_{mouse} \alpha \]

The larger \alpha is (between 0 and 1), the more sensitive the value is to quick changes. This is basically the same algorithm used to implement a low-pass filter on audio signals, so it makes sense that it filters out the unwanted high-frequency changes in the path.

The problem with this approach is that with a high \alpha, it doesn’t filter out enough of the noise, and if \alpha is too low, the end of the line lags behind the mouse cursor, as you can see here:

You can see the lag when you stop moving the mouse, too. To fix this, we attempted just drawing a line segment from the end of the line to the mouse, but it looks a little odd.

The solution we settled on was to drag each of the last N points toward the next point in the line. This allows us to smooth the line more without leaving a gap at the end of the line. Basically, the last point is unsmoothed, and each previous point is smoothed more, up to the Nth point from the end. If N is to large, the end of the line will chase the mouse around in a distracting way, but if N is kept small enough, it seems to work well. You can check out the result here:

You can see this technique, or a very similar one, used in Flight Control on the PC as well. As you adjust the path, the end of the line pulls toward the mouse, and you don’t get jagged lines.

Here’s the code for the examples above:

 
var unsmoothedCanvas = document.getElementById("unsmoothed"); 
var smoothedCanvas = document.getElementById("smoothed"); 
var expsmoothedCanvas = document.getElementById("expsmoothed"); 
var smoothLength = 4; 
 function drawLine(canvas, points) { 
  var ctx = canvas.getContext("2d"); 
  var p0 = points[0]; 
  ctx.fillStyle = "black" 
  ctx.beginPath(); 
  ctx.moveTo(p0.x, p0.y); 
  for(var i = 1; i < points.length; ++i) { 
    var p = points[i]; 
    ctx.lineTo(p.x, p.y); 
  } 
  ctx.stroke(); 
} 
 function clear(canvas) { 
  var ctx = canvas.getContext("2d"); 
  ctx.fillStyle = "white"; 
  ctx.fillRect(0, 0, canvas.width, canvas.height); 
} 
 function setupCanvas(canvas, smoothingFn, minDist) { 
  var getPos = function(e) { return {x: e.pageX -canvas.offsetLeft, y: e.pageY -canvas.offsetTop}; } 
  var dist = function(a,b) { var x = a.x-b.x; var y = a.y-b.y; return x*x+y*y; } 
  clear(canvas); 
  canvas.onmousedown = function (edown) { 
    canvas.points = []; 
    for(var i = 0; i < smoothLength+1; ++i) canvas.points.push(getPos(edown)); 
    canvas.onmousemove = function(e) { 
      var p = getPos(e) 
      var last = canvas.points[canvas.points.length-1]; 
      if(dist(p,last) > minDist) { 
        canvas.points.push(p); 
        smoothingFn(canvas.points); 
        clear(canvas); 
        drawLine(canvas, canvas.points); 
      } 
    } 
  } 
  canvas.onmouseup = function(e) { 
    canvas.onmousemove = null; 
  } 
} 
 setupCanvas(unsmoothedCanvas, function(ps) { }, 8); 
setupCanvas(expsmoothedCanvas, function(ps) { 
  var a = 0.2; 
  var p = ps[ps.length-1] 
  var p1 = ps[ps.length-2]; 
  ps[ps.length-1] = {x:p.x * a + p1.x * (1-a), y:p.y * a + p1.y * (1-a) } 
}, 8); 
 setupCanvas(smoothedCanvas, function(ps) { 
  for(var i = 0; i < smoothLength; ++i) { 
    var j = ps.length-i-2; 
    var p0 = ps[j] 
    var p1 = ps[j+1] 
    var a = 0.2; 
    var p = {x:p0.x * (1-a) + p1.x * a, y:p0.y * (1-a) + p1.y * a }; 
    ps[j] = p; 
  } 
}, 8); 

---

Discuss on Reddit, Hacker News


Twisted Oak Studios offers consulting and development on high-tech interactive projects. Check out our portfolio, or Give us a shout if you have anything you think some really rad engineers should help you with.

Archive

More interesting posts (3 of 8 articles)

Or check out our Portfolio.