Skip to content

Instantly share code, notes, and snippets.

@yurydelendik
Created November 16, 2012 14:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yurydelendik/4087906 to your computer and use it in GitHub Desktop.
Save yurydelendik/4087906 to your computer and use it in GitHub Desktop.
stroke-to-path stuff
<!DOCTYPE html>
<html>
<head>
<script>
function strokeToPath(cmds, options) {
var CURVE_APPROX_POINTS = 10;
var CAP_APPROX_POINTS = 10;
function pushCurveApprox(points, args) {
var x0 = points[points.length - 1].x;
var y0 = points[points.length - 1].y;
var x1 = args[0], y1 = args[1], x2 = args[2], y2 = args[3];
for (var i = 0; i < CURVE_APPROX_POINTS; i++) {
var p2 = (i + 1) / CURVE_APPROX_POINTS, p1 = 1 - p2;
var x01 = x0 * p1 + x1 * p2, y01 = y0 * p1 + y1 * p2;
var x12 = x1 * p1 + x2 * p2, y12 = y1 * p1 + y2 * p2;
var x = x01 * p1 + x12 * p2, y = y01 * p1 + y12 * p2;
points.push({x: x, y: y, type: 2});
}
points[points.length - 1].type = 1;
}
function pushBezierCurveApprox(points, args) {
var x0 = points[points.length - 1].x;
var y0 = points[points.length - 1].y;
var x1 = args[0], y1 = args[1], x2 = args[2], y2 = args[3];
var x3 = args[4], y3 = args[5];
for (var i = 0; i < CURVE_APPROX_POINTS; i++) {
var p2 = (i + 1) / CURVE_APPROX_POINTS, p1 = 1 - p2;
var x01 = x0 * p1 + x1 * p2, y01 = y0 * p1 + y1 * p2;
var x12 = x1 * p1 + x2 * p2, y12 = y1 * p1 + y2 * p2;
var x23 = x2 * p1 + x3 * p2, y23 = y2 * p1 + y3 * p2;
var x012 = x01 * p1 + x12 * p2, y012 = y01 * p1 + y12 * p2;
var x123 = x12 * p1 + x23 * p2, y123 = y12 * p1 + y23 * p2;
var x = x012 * p1 + x123 * p2, y = y012 * p1 + y123 * p2;
points.push({x: x, y: y, type: 2});
}
points[points.length - 1].type = 1;
}
function buildCap(lines, capStyle, line1, line2) {
line1.type = 3;
switch (capStyle) {
case 'round':
var cx = (line1.x2 + line2.x1) / 2;
var cy = (line1.y2 + line2.y1) / 2;
var dx = (line1.x2 - cx), dy = (line1.y2 - cy);
var cos = Math.cos(Math.PI / CAP_APPROX_POINTS);
var sin = Math.sin(Math.PI / CAP_APPROX_POINTS);
for (var i = 0; i < CAP_APPROX_POINTS; i++) {
var dx1 = dx * cos - dy * sin;
var dy1 = dx * sin + dy * cos;
lines.push({
x1: cx + dx, y1: cy + dy,
x2: cx + dx1, y2: cy + dy1,
type: 3});
dx = dx1; dy = dy1;
}
break;
case 'square':
var capHeight = options.strokeWidth;
var dx = line1.x2 - line1.x1, dy = line1.y2 - line1.y1;
var d = Math.sqrt(dx * dx + dy * dy);
line1.x2 += dx * capHeight / d;
line1.y2 += dy * capHeight / d;
line2.x1 += dx * capHeight / d;
line2.y1 += dy * capHeight / d;
// fall throw
case 'none':
default:
lines.push({
x1: line1.x2, y1: line1.y2,
x2: line2.x1, y2: line2.y1,
type: 3});
break;
}
}
function joinLines(cmds, line1, line2, type) {
// (x - x1) * (y2 - y1) - (y - y1) * (x2 - x1) = 0, a*x + b*y = c
var a1 = (line1.y2 - line1.y1), b1 = -(line1.x2 - line1.x1), c1 = line1.x1 * line1.y2 - line1.x2 * line1.y1;
var a2 = (line2.y2 - line2.y1), b2 = -(line2.x2 - line2.x1), c2 = line2.x1 * line2.y2 - line2.x2 * line2.y1;
var d = a1 * b2 - b1 * a2;
if (d == 0) {
// parellel lines doing bevel
cmds.push({type: 'lineTo', args: [line1.x2, line1.y2]});
cmds.push({type: 'lineTo', args: [line2.x1, line2.y1]});
return;
}
var x = (c1 * b2 - b1 * c2) / d;
var y = (a1 * c2 - c1 * a2) / d;
var onLine1 = !(
(x < line1.x1 && x < line1.x2) || (x > line1.x1 && x > line1.x2) ||
(y < line1.y1 && y < line1.y2) || (y > line1.y1 && y > line1.y2));
var onLine2 = !(
(x < line2.x1 && x < line2.x2) || (x > line2.x1 && x > line2.x2) ||
(y < line2.y1 && y < line2.y2) || (y > line2.y1 && y > line2.y2));
if (!onLine1 && !onLine2) {
switch (type) {
default:
case 'bevel':
cmds.push({type: 'lineTo', args: [line1.x2, line1.y2]});
cmds.push({type: 'lineTo', args: [line2.x1, line2.y1]});
break;
case 'round':
cmds.push({type: 'lineTo', args: [line1.x2, line1.y2]});
cmds.push({type: 'quadraticCurveTo', args: [x, y, line2.x1, line2.y1]});
break;
case 'miter':
cmds.push({type: 'lineTo', args: [line1.x2, line1.y2]});
var a = -(line1.y2 - line2.y1), b = line1.x2 - line2.x1;
var d = Math.sqrt(a * a + b * b);
var miterLength = (a * (x - line2.x1) + b * (y - line2.y1)) / d;
if (miterLength > options.miterLimit) {
var p2 = options.miterLimit / miterLength, p1 = 1 - p2;
cmds.push({type: 'lineTo', args: [line1.x2 * p1 + x * p2, line1.y2 * p1 + y * p2]});
cmds.push({type: 'lineTo', args: [line2.x1 * p1 + x * p2, line2.y1 * p1 + y * p2]});
} else {
cmds.push({type: 'lineTo', args: [x, y]});
}
cmds.push({type: 'lineTo', args: [line2.x1, line2.y1]});
break;
}
} else if (!onLine1 || !onLine2) {
cmds.push({type: 'lineTo', args: onLine1 ? [x, y] : [line1.x2, line1.y2]});
cmds.push({type: 'lineTo', args: onLine2 ? [x, y] : [line2.x1, line2.y1]});
} else {
cmds.push({type: 'lineTo', args: [x, y]});
}
}
function buildPath(cmds, lines) {
moveCmd = {type: 'moveTo', args: null};
cmds.push(moveCmd);
var joinType = options.join;
for (var j = 0; j < lines.length; j++) {
var type = lines[j].type;
switch (type) {
case 3: // simple line
cmds.push({type: 'lineTo', args: [lines[j].x2, lines[j].y2]});
break;
case 2:
joinLines(cmds, lines[j], lines[(j + 1) % lines.length], 'bevel');
break;
case 1:
joinLines(cmds, lines[j], lines[(j + 1) % lines.length], joinType);
break;
}
}
moveCmd.args = cmds[cmds.length - 1].args.slice(-2);
cmds.push({type: 'closePath'});
}
var i = 0;
var shape = [];
do {
while (i < cmds.length && cmds[i].type != 'moveTo') i++;
var points = [{x: cmds[i].args[0], y: cmds[i].args[1], type: 1}];
i++;
var stopHere = false, pathClosed = false;
while (i < cmds.length && !stopHere) {
switch (cmds[i].type) {
case "lineTo":
points.push({x: cmds[i].args[0], y: cmds[i].args[1], type: 1});
i++;
break;
case "quadraticCurveTo":
pushCurveApprox(points, cmds[i].args);
i++;
break;
case "bezierCurveTo":
pushBezierCurveApprox(points, cmds[i].args);
i++;
break;
case "closePath":
points.push({x: points[0].x, y: points[0].y, type: 1});
pathClosed = true;
i++;
stopHere = true;
break;
default:
stopHere = true;
break;
}
}
// building paths
var forward = [], backward = [], q = 0;
var strokeWidth = options.strokeWidth;
for (var j = 1; j < points.length; j++) {
var dx = points[j].x - points[q].x;
var dy = points[j].y - points[q].y;
if (dx == 0 && dy == 0) continue;
var k = strokeWidth / Math.sqrt(dx * dx + dy * dy);
dx *= k; dy *= k;
forward.push({
x1: points[q].x + dy, y1: points[q].y - dx,
x2: points[j].x + dy, y2: points[j].y - dx,
type: points[j].type
});
backward.push({
x1: points[j].x - dy, y1: points[j].y + dx,
x2: points[q].x - dy, y2: points[q].y + dx,
type: points[q].type
});
q = j;
}
backward.reverse();
if (!pathClosed) {
buildCap(forward, options.endCap, forward[forward.length - 1], backward[0]);
buildCap(backward, options.startCap, backward[backward.length - 1], forward[0]);
forward = forward.concat(backward);
buildPath(shape, forward);
} else {
buildPath(shape, forward);
buildPath(shape, backward);
}
} while (i < cmds.length);
return shape;
}
</script>
</head>
<body>
<canvas id="c" width="700" height="500"></canvas>
<div id="strokeTools">
<label for="strokeWidth">Width:</label> <input type="number" id="strokeWidth" value="10">
<label for="startCap">Start:</label> <select id="startCap"><option>none</option><option>square</option><option>round</option></select>
<label for="endCap">End:</label> <select id="endCap"><option>none</option><option>square</option><option>round</option></select>
<label for="join">Join:</label> <select id="join"><option>bevel</option><option>miter</option><option>round</option></select>
<label for="miterLimit">Miter Limit:</label> <input type="number" id="miterLimit" value="20">
</div>
<p>
Hints: click on the canvas to move the point; next click will add a point to polygon;
'b' - will continue as bezier, 'q' - as quadratic; c - closes the polygon;
ENTER - will stroke and convert stroke into polygon.
</p>
<script>
var state = "move";
var cmds = [];
function updateCanvas(ctx) {
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// draw points
var lastX, lastY;
for (var i = 0; i < cmds.length; i++) {
var cmd = cmds[i];
if (!cmd.args) continue;
if (cmd.type == 'lineTo' || cmd.type == 'moveTo') {
ctx.beginPath();
ctx.strokeStyle = 'blue';
ctx.rect(cmd.args[0] - 5, cmd.args[1] - 5, 10, 10);
ctx.stroke();
lastX = cmd.args[0]; lastY = cmd.args[1];
}
if (cmd.type == 'bezierCurveTo' || cmd.type == 'quadraticCurveTo') {
ctx.beginPath();
ctx.strokeStyle = 'cyan';
ctx.rect(cmd.args[0] - 5, cmd.args[1] - 5, 10, 10);
ctx.moveTo(lastX, lastY);
ctx.lineTo(lastX = cmd.args[0], lastY = cmd.args[1]);
ctx.stroke();
}
if (cmd.type == 'quadraticCurveTo' &&
cmd.args.length > 2) {
ctx.beginPath();
ctx.strokeStyle = 'blue';
ctx.rect(cmd.args[2] - 5, cmd.args[3] - 5, 10, 10);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = 'cyan';
ctx.moveTo(cmd.args[0], cmd.args[1]);
ctx.lineTo(lastX = cmd.args[2], lastY = cmd.args[3]);
ctx.stroke();
}
if (cmd.type == 'bezierCurveTo' &&
cmd.args.length > 2) {
ctx.beginPath();
ctx.strokeStyle = 'cyan';
ctx.rect(cmd.args[2] - 5, cmd.args[3] - 5, 10, 10);
ctx.stroke();
}
if (cmd.type == 'bezierCurveTo' &&
cmd.args.length > 4) {
ctx.beginPath();
ctx.strokeStyle = 'blue';
ctx.rect(cmd.args[4] - 5, cmd.args[5] - 5, 10, 10);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = 'cyan';
ctx.moveTo(cmd.args[2], cmd.args[3]);
ctx.lineTo(lastX = cmd.args[4], lastY = cmd.args[5]);
ctx.stroke();
}
}
ctx.strokeStyle = 'white';
ctx.beginPath();
for (var i = 0; i < cmds.length; i++) {
var cmd = cmds[i];
if (cmd.skip) continue;
ctx[cmd.type].apply(ctx, cmd.args);
}
ctx.stroke();
}
var c = document.getElementById('c');
var ctx = c.getContext('2d');
updateCanvas(ctx);
c.addEventListener('mousedown', function (e) {
var x = e.clientX - c.offsetLeft, y = e.clientY - c.offsetTop;
switch (state) {
case "move":
cmds.push({type: "moveTo", args: [x, y]});
state = "line";
break;
case "line":
cmds.push({type: "lineTo", args: [x, y]});
break;
case "bez":
cmds.push({type: "bezierCurveTo", args: [x, y], skip: true});
state = "bez1";
break;
case "bez1":
cmds[cmds.length - 1].args.push(x, y);
state = "bez2";
break;
case "bez2":
cmds[cmds.length - 1].args.push(x, y);
delete cmds[cmds.length - 1].skip;
state = "bez";
break;
case "quad":
cmds.push({type: "quadraticCurveTo", args: [x, y], skip: true});
state = "quad1";
break;
case "quad1":
cmds[cmds.length - 1].args.push(x, y);
delete cmds[cmds.length - 1].skip;
state = "quad";
break;
}
updateCanvas(ctx);
});
window.addEventListener('keydown', function (e) {
switch (e.keyCode) {
case 77: // m
state = "move";
break;
case 76: // l
state = "line";
break;
case 66: // b
state = "bez";
break;
case 67: // c
cmds.push({type: "closePath"});
state = "move";
updateCanvas(ctx);
break;
case 81: // q
state = "quad";
break;
case 83: // s
prompt('save', uneval(cmds));
state = "move";
break;
case 79: // o
cmds = eval(prompt('open')) || [];
updateCanvas(ctx);
state = "move";
break;
case 13: // Enter
cmds = strokeToPath(cmds, {
strokeWidth: +document.getElementById('strokeWidth').value,
startCap: document.getElementById('startCap').value,
endCap: document.getElementById('endCap').value,
join: document.getElementById('join').value,
miterLimit: +document.getElementById('miterLimit').value
});
updateCanvas(ctx);
state = "move";
break;
default:
//console.log('key: ' + e.keyCode);
break;
}
});
</script>
</body></html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment