Skip to content

Instantly share code, notes, and snippets.

@upsuper
Created May 8, 2012 07:51
Show Gist options
  • Save upsuper/2633348 to your computer and use it in GitHub Desktop.
Save upsuper/2633348 to your computer and use it in GitHub Desktop.
HTML5 Canvas demo with lines smoothed using Bézier curve
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title></title>
<style type="text/css">
canvas { position: absolute; top: 0; left: 0; }
#c { background: #ccc; }
#ps { position: absolute; top: 0; right: 0; }
#cl { position: absolute; bottom: 0; right: 0; }
</style>
</head>
<body>
<canvas id="c" width="300" height="300"></canvas>
<canvas id="p" width="300" height="300"></canvas>
<div id="ps"></div>
<button id="cl">Clear</button>
<script type="text/javascript">
var $c = document.getElementById('c');
var $p = document.getElementById('p');
var $ps = document.getElementById('ps');
var $cl = document.getElementById('cl');
$cl.addEventListener('click', function () {
var ctx = $c.getContext('2d');
ctx.clearRect(0, 0, 800, 600);
}, false);
var path = [];
var painting = false;
function redrawPath() {
var ctx = $p.getContext('2d');
ctx.clearRect(0, 0, 800, 600);
if (!path.length)
return;
ctx.beginPath();
ctx.moveTo(path[0].x, path[0].y);
for (var i = 1; i < path.length; ++i)
ctx.lineTo(path[i].x, path[i].y);
ctx.stroke();
}
function centrosymmetry(p0, p) {
return {x: p0.x * 2 - p.x, y: p0.y * 2 - p.y};
}
function averagePoint(p1, p2) {
return {x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2};
}
function getp(e) {
if (e.touches)
return {x: e.touches[0].clientX, y: e.touches[0].clientY};
else
return {x: e.clientX, y: e.clientY};
}
function startDrawing(e) {
e.preventDefault();
var curp = getp(e);
//var ctx = $c.getContext('2d');
//ctx.clearRect(0, 0, 800, 600);
painting = true;
path.push(curp);
redrawPath();
}
function drawing(e) {
e.preventDefault();
if (!painting)
return;
var curp = getp(e);
//$ps.innerHTML += curp.x + ',' + curp.y + ' ';
path.push(curp);
redrawPath();
}
function smooth1() {
if (path.length <= 2)
return;
var ctx = $c.getContext('2d');
ctx.strokeStyle = "blue";
ctx.beginPath();
ctx.moveTo(path[0].x, path[0].y);
var a = path[0],
o = path[1],
b = centrosymmetry(o, path[2]),
c = averagePoint(averagePoint(averagePoint(a, b), o), o);
ctx.quadraticCurveTo(c.x, c.y, o.x, o.y);
for (var i = 1; i + 2 < path.length; ++i) {
d = centrosymmetry(o, c);
a = o;
o = path[i + 1];
b = centrosymmetry(o, path[i + 2]);
c = averagePoint(averagePoint(averagePoint(a, b), o), o);
ctx.bezierCurveTo(d.x, d.y, c.x, c.y, o.x, o.y);
}
b = path[path.length - 1];
o = path[path.length - 2];
c = centrosymmetry(o, c);
ctx.quadraticCurveTo(c.x, c.y, b.x, b.y);
ctx.stroke();
}
function Vector(p0, p1) {
this.x = p0.x;
this.y = p0.y;
var x_ = this.x_ = p1.x - p0.x;
var y_ = this.y_ = p1.y - p0.y;
this.length = Math.sqrt(x_ * x_ + y_ * y_);
}
Vector.prototype.mul = function (k) {
this.x_ *= k;
this.y_ *= k;
this.length *= k;
return this;
};
Vector.prototype.end = function () {
return {x: this.x + this.x_, y: this.y + this.y_};
};
Vector.prototype.copy = function () {
return new Vector(this, this.end());
};
function smoothPoint(a, b, o) {
var va = new Vector(o, a),
vb = new Vector(o, b);
var minLen = Math.min(va.length, vb.length);
va.mul(minLen / va.length);
vb.mul(minLen / vb.length);
return averagePoint(va.end(), vb.end());
}
function smooth2() {
if (path.length <= 2)
return;
var ctx = $c.getContext('2d');
ctx.beginPath();
ctx.moveTo(path[0].x, path[0].y);
var a = path[0],
o = path[1],
b = centrosymmetry(o, path[2]),
c = smoothPoint(a, b, o);
ctx.quadraticCurveTo(c.x, c.y, o.x, o.y);
for (var i = 1; i + 2 < path.length; ++i) {
d = centrosymmetry(o, c);
a = o;
o = path[i + 1];
b = centrosymmetry(o, path[i + 2]);
c = smoothPoint(a, b, o);
ctx.bezierCurveTo(d.x, d.y, c.x, c.y, o.x, o.y);
}
b = path[path.length - 1];
o = path[path.length - 2];
c = centrosymmetry(o, c);
ctx.quadraticCurveTo(c.x, c.y, b.x, b.y);
ctx.stroke();
}
function smooth3() {
if (path.length <= 1)
return;
var ctx = $c.getContext('2d');
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.moveTo(path[0].x, path[0].y);
if (path.length == 2) {
ctx.lineTo(path[1].x, path[1].y);
}
else {
var a = path[0],
o = path[1],
b = centrosymmetry(o, path[2]),
v = new Vector(o, averagePoint(a, b));
var la = (new Vector(o, a)).length,
lb = (new Vector(o, b)).length,
ls = la + lb;
var c = v.copy().mul(la / ls).end();
var cs = [], ds = [];
cs.push(c);
ctx.quadraticCurveTo(c.x, c.y, o.x, o.y);
for (var i = 1; i + 2 < path.length; ++i) {
v = new Vector(o, centrosymmetry(o, averagePoint(a, b)));
d = v.copy().mul(lb / ls).end();
a = o;
o = path[i + 1];
b = centrosymmetry(o, path[i + 2]);
v = new Vector(o, averagePoint(a, b));
la = lb;
lb = (new Vector(o, b)).length;
ls = la + lb;
c = v.copy().mul(la / ls).end();
ds.push(d); cs.push(c);
ctx.bezierCurveTo(d.x, d.y, c.x, c.y, o.x, o.y);
}
v = new Vector(o, centrosymmetry(o, averagePoint(a, b)));
b = path[path.length - 1];
o = path[path.length - 2];
c = v.copy().mul(lb / ls).end();
cs.push(c);
ctx.quadraticCurveTo(c.x, c.y, b.x, b.y);
}
ctx.stroke();
/*
ctx.save();
ctx.fillStyle = "red";
ctx.strokeStyle = "red";
for (var i = 0; i < path.length; ++i)
ctx.fillRect(path[i].x - 1, path[i].y - 1, 3, 3);
ctx.fillStyle = "yellow";
ctx.strokeStyle = "yellow";
for (var i = 1; i < cs.length - 1; ++i)
ctx.fillRect(cs[i].x - 1, cs[i].y - 1, 3, 3);
ctx.fillStyle = "orange";
ctx.strokeStyle = "orange";
for (var i = 0; i < ds.length; ++i)
ctx.fillRect(ds[i].x - 1, ds[i].y - 1, 3, 3);
ctx.restore();
*/
}
function endDrawing(e) {
e.preventDefault();
if (!painting)
return;
if (!e.touches)
path.push(getp(e));
if (!path.length)
return;
painting = false;
//$ps.innerHTML = '';
//for (var i = 0; i < path.length; ++i)
// $ps.innerHTML += path[i].x + ',' + path[i].y + '<br>';
//smooth1();
smooth3();
// cleaning
path = [];
redrawPath();
}
$p.addEventListener('mousedown', startDrawing);
$p.addEventListener('mousemove', drawing);
$p.addEventListener('mouseup', endDrawing);
$p.addEventListener('touchstart', startDrawing);
$p.addEventListener('touchmove', drawing);
$p.addEventListener('touchend', endDrawing);
//$p.addEventListener('touchleave', endDrawing);
//$p.addEventListener('touchcancel', endDrawing);
</script>
</body>
</html>
@upsuper
Copy link
Author

upsuper commented May 8, 2012

There are 3 smoothing algorithms, and the final curves of any of them always pass through all points sampled.
smooth1 is the basic algorithm, and smooth2 is worse than smooth1. smooth3 is the best one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment