Created
November 16, 2012 14:52
-
-
Save yurydelendik/4087906 to your computer and use it in GitHub Desktop.
stroke-to-path stuff
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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