I'm trying to draw a curve in canvas with a linear gradient stoke style along the curve, as in this image. On that page there is a linked svg file that gives instructions on how to accomplish the effect in svg. Maybe a similar method would be possible in canvas?
A Demo: http://jsfiddle.net/m1erickson/4fX5D/
It's fairly easy to create a gradient that changes along the path:
It's more difficult to create a gradient that changes across the path:
To create a gradient across the path you draw many gradient lines tangent to the path:
If you draw enough tangent lines then the eye sees the curve as a gradient across the path.
Note: Jaggies can occur on the outsides of the path-gradient. That's because the gradient is really made up of hundreds of tangent lines. But you can smooth out the jaggies by drawing a line on either side of the gradient using the appropriate colors (here the anti-jaggy lines are red on the top side and purple on the bottom side).
Here are the steps to creating a gradient across the path:
Plot hundreds of points along the path.
Calculate the angle of the path at those points.
At each point, create a linear gradient and draw a gradient stroked line across the tangent of that point. Yes, you will have to create a new gradient for each point because the linear gradient must match the angle of the line tangent to that point.
To reduce the jaggy effect caused by drawing many individual lines, you can draw a smooth path along the top and bottom side of the gradient path to overwrite the jaggies.
Here is annotated code:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: ivory; }
#canvas{border:1px solid red;}
</style>
<script>
$(function(){
// canvas related variables
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
// variables defining a cubic bezier curve
var PI2=Math.PI*2;
var s={x:20,y:30};
var c1={x:200,y:40};
var c2={x:40,y:200};
var e={x:270,y:220};
// an array of points plotted along the bezier curve
var points=[];
// we use PI often so put it in a variable
var PI=Math.PI;
// plot 400 points along the curve
// and also calculate the angle of the curve at that point
for(var t=0;t<=100;t+=0.25){
var T=t/100;
// plot a point on the curve
var pos=getCubicBezierXYatT(s,c1,c2,e,T);
// calculate the tangent angle of the curve at that point
var tx = bezierTangent(s.x,c1.x,c2.x,e.x,T);
var ty = bezierTangent(s.y,c1.y,c2.y,e.y,T);
var a = Math.atan2(ty, tx)-PI/2;
// save the x/y position of the point and the tangent angle
// in the points array
points.push({
x:pos.x,
y:pos.y,
angle:a
});
}
// Note: increase the lineWidth if
// the gradient has noticable gaps
ctx.lineWidth=2;
// draw a gradient-stroked line tangent to each point on the curve
for(var i=0;i<points.length;i++){
// calc the topside and bottomside points of the tangent line
var offX1=points[i].x+20*Math.cos(points[i].angle);
var offY1=points[i].y+20*Math.sin(points[i].angle);
var offX2=points[i].x+20*Math.cos(points[i].angle-PI);
var offY2=points[i].y+20*Math.sin(points[i].angle-PI);
// create a gradient stretching between
// the calculated top & bottom points
var gradient=ctx.createLinearGradient(offX1,offY1,offX2,offY2);
gradient.addColorStop(0.00, 'red');
gradient.addColorStop(1/6, 'orange');
gradient.addColorStop(2/6, 'yellow');
gradient.addColorStop(3/6, 'green')
gradient.addColorStop(4/6, 'aqua');
gradient.addColorStop(5/6, 'blue');
gradient.addColorStop(1.00, 'purple');
// draw the gradient-stroked line at this point
ctx.strokeStyle=gradient;
ctx.beginPath();
ctx.moveTo(offX1,offY1);
ctx.lineTo(offX2,offY2);
ctx.stroke();
}
// draw a top stroke to cover jaggies
// on the top of the gradient curve
var offX1=points[0].x+20*Math.cos(points[0].angle);
var offY1=points[0].y+20*Math.sin(points[0].angle);
ctx.strokeStyle="red";
// Note: increase the lineWidth if this outside of the
// gradient still has jaggies
ctx.lineWidth=1.5;
ctx.beginPath();
ctx.moveTo(offX1,offY1);
for(var i=1;i<points.length;i++){
var offX1=points[i].x+20*Math.cos(points[i].angle);
var offY1=points[i].y+20*Math.sin(points[i].angle);
ctx.lineTo(offX1,offY1);
}
ctx.stroke();
// draw a bottom stroke to cover jaggies
// on the bottom of the gradient
var offX2=points[0].x+20*Math.cos(points[0].angle+PI);
var offY2=points[0].y+20*Math.sin(points[0].angle+PI);
ctx.strokeStyle="purple";
// Note: increase the lineWidth if this outside of the
// gradient still has jaggies
ctx.lineWidth=1.5;
ctx.beginPath();
ctx.moveTo(offX2,offY2);
for(var i=0;i<points.length;i++){
var offX2=points[i].x+20*Math.cos(points[i].angle+PI);
var offY2=points[i].y+20*Math.sin(points[i].angle+PI);
ctx.lineTo(offX2,offY2);
}
ctx.stroke();
//////////////////////////////////////////
// helper functions
//////////////////////////////////////////
// calculate one XY point along Cubic Bezier at interval T
// (where T==0.00 at the start of the curve and T==1.00 at the end)
function getCubicBezierXYatT(startPt,controlPt1,controlPt2,endPt,T){
var x=CubicN(T,startPt.x,controlPt1.x,controlPt2.x,endPt.x);
var y=CubicN(T,startPt.y,controlPt1.y,controlPt2.y,endPt.y);
return({x:x,y:y});
}
// cubic helper formula at T distance
function CubicN(T, a,b,c,d) {
var t2 = T * T;
var t3 = t2 * T;
return a + (-a * 3 + T * (3 * a - a * T)) * T
+ (3 * b + T * (-6 * b + b * 3 * T)) * T
+ (c * 3 - c * 3 * T) * t2
+ d * t3;
}
// calculate the tangent angle at interval T on the curve
function bezierTangent(a, b, c, d, t) {
return (3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b));
};
}); // end $(function(){});
</script>
</head>
<body>
<canvas id="canvas" width=300 height=300></canvas>
</body>
</html>
I am working on doing something very similar, and I just wanted to add a couple things. markE's answer is great, but what he calls tangent lines to the curve, are actually lines normal or perpendicular to the curve. (Tangent lines are parallel, normal lines are perpendicular)
For my particular application, I am using a gradient across a line with transparency. In this case, it is important to get near pixel perfect gradient regions, as overlapping transparency will get drawn twice, changing the desired color. So instead of drawing a bunch of lines perpendicular to the curve, I divided the curve up into quadrilaterals and applied a linear gradient to each. Additionally, using these quadrilateral regions reduces the number of calls to draw you have to make, which can make it more efficient. You don't need a ton of regions to get a pretty smooth effect, and the fewer regions you use, the faster it will be able to render.
I adapted markE's code, so credit to him for that great answer. Here is the fiddle: https://jsfiddle.net/hvyt58dz/
Here is the adapted code I used:
// canvas related variables
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// variables defining a cubic bezier curve
var PI2 = Math.PI * 2;
var s = {
x: 20,
y: 30
};
var c1 = {
x: 200,
y: 40
};
var c2 = {
x: 40,
y: 200
};
var e = {
x: 270,
y: 220
};
// an array of points plotted along the bezier curve
var points = [];
// we use PI often so put it in a variable
var PI = Math.PI;
// plot 400 points along the curve
// and also calculate the angle of the curve at that point
var step_size = 100/18;
for (var t = 0; t <= 100 + 0.1; t += step_size) {
var T = t / 100;
// plot a point on the curve
var pos = getCubicBezierXYatT(s, c1, c2, e, T);
// calculate the tangent angle of the curve at that point
var tx = bezierTangent(s.x, c1.x, c2.x, e.x, T);
var ty = bezierTangent(s.y, c1.y, c2.y, e.y, T);
var a = Math.atan2(ty, tx) - PI / 2;
// save the x/y position of the point and the tangent angle
// in the points array
points.push({
x: pos.x,
y: pos.y,
angle: a
});
}
// Note: increase the lineWidth if
// the gradient has noticable gaps
ctx.lineWidth = 2;
var overlap = 0.2;
var outside_color = 'rgba(255,0,0,0.0)';
var inside_color = 'rgba(255,0,0,0.7)';
// draw a gradient-stroked line tangent to each point on the curve
var line_width = 40;
var half_width = line_width/2;
for (var i = 0; i < points.length - 1; i++) {
var x1 = points[i].x, y1 = points[i].y;
var x2 = points[i+1].x, y2 = points[i+1].y;
var angle1 = points[i].angle, angle2 = points[i+1].angle;
var midangle = (angle1 + angle2)/ 2;
// calc the topside and bottomside points of the tangent line
var gradientOffsetX1 = x1 + half_width * Math.cos(midangle);
var gradientOffsetY1 = y1 + half_width * Math.sin(midangle);
var gradientOffsetX2 = x1 + half_width * Math.cos(midangle - PI);
var gradientOffsetY2 = y1 + half_width * Math.sin(midangle - PI);
var offX1 = x1 + half_width * Math.cos(angle1);
var offY1 = y1 + half_width * Math.sin(angle1);
var offX2 = x1 + half_width * Math.cos(angle1 - PI);
var offY2 = y1 + half_width * Math.sin(angle1 - PI);
var offX3 = x2 + half_width * Math.cos(angle2)
- overlap * Math.cos(angle2-PI/2);
var offY3 = y2 + half_width * Math.sin(angle2)
- overlap * Math.sin(angle2-PI/2);
var offX4 = x2 + half_width * Math.cos(angle2 - PI)
+ overlap * Math.cos(angle2-3*PI/2);
var offY4 = y2 + half_width * Math.sin(angle2 - PI)
+ overlap * Math.sin(angle2-3*PI/2);
// create a gradient stretching between
// the calculated top & bottom points
var gradient = ctx.createLinearGradient(gradientOffsetX1, gradientOffsetY1, gradientOffsetX2, gradientOffsetY2);
gradient.addColorStop(0.0, outside_color);
gradient.addColorStop(0.25, inside_color);
gradient.addColorStop(0.75, inside_color);
gradient.addColorStop(1.0, outside_color);
//gradient.addColorStop(1 / 6, 'orange');
//gradient.addColorStop(2 / 6, 'yellow');
//gradient.addColorStop(3 / 6, 'green')
//gradient.addColorStop(4 / 6, 'aqua');
//gradient.addColorStop(5 / 6, 'blue');
//gradient.addColorStop(1.00, 'purple');
// line cap
if(i == 0){
var x = x1 - overlap * Math.cos(angle1-PI/2);
var y = y1 - overlap * Math.sin(angle1-PI/2);
var cap_gradient = ctx.createRadialGradient(x, y, 0, x, y, half_width);
ctx.beginPath();
ctx.arc(x, y, half_width, angle1 - PI, angle1);
cap_gradient.addColorStop(0.5, inside_color);
cap_gradient.addColorStop(1.0, outside_color);
ctx.fillStyle = cap_gradient;
ctx.fill();
}
if(i == points.length - 2){
var x = x2 + overlap * Math.cos(angle2-PI/2);
var y = y2 + overlap * Math.sin(angle2-PI/2);
var cap_gradient = ctx.createRadialGradient(x, y, 0, x, y, half_width);
ctx.beginPath();
ctx.arc(x, y, half_width, angle2, angle2 + PI);
cap_gradient.addColorStop(0.5, inside_color);
cap_gradient.addColorStop(1.0, outside_color);
ctx.fillStyle = cap_gradient;
ctx.fill();
console.log(x,y);
}
// draw the gradient-stroked line at this point
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.moveTo(offX1, offY1);
ctx.lineTo(offX2, offY2);
ctx.lineTo(offX4, offY4);
ctx.lineTo(offX3, offY3);
ctx.fill();
}
//////////////////////////////////////////
// helper functions
//////////////////////////////////////////
// calculate one XY point along Cubic Bezier at interval T
// (where T==0.00 at the start of the curve and T==1.00 at the end)
function getCubicBezierXYatT(startPt, controlPt1, controlPt2, endPt, T) {
var x = CubicN(T, startPt.x, controlPt1.x, controlPt2.x, endPt.x);
var y = CubicN(T, startPt.y, controlPt1.y, controlPt2.y, endPt.y);
return ({
x: x,
y: y
});
}
// cubic helper formula at T distance
function CubicN(T, a, b, c, d) {
var t2 = T * T;
var t3 = t2 * T;
return a + (-a * 3 + T * (3 * a - a * T)) * T + (3 * b + T * (-6 * b + b * 3 * T)) * T + (c * 3 - c * 3 * T) * t2 + d * t3;
}
// calculate the tangent angle at interval T on the curve
function bezierTangent(a, b, c, d, t) {
return (3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b));
};
Related
Question:
Why does CanvasRenderingContext2D.clip() closes an additional path when applying it to a collection of CanvasRenderingContext2D.arc() sampled along the path of a quadratic curve?
Background
I am trying to create a path of quadratic segments with a longitudinal color split. Based on a comment to the question "Square curve with lengthwise color division" I am trying to accomplish this goal by going through the following steps:
Draw the quadratic path
Sample point on the quadratic curve
Create a clipping region and draw a cycle at each sampled point
let region = new Path2D();
for (j = 0; j < pointsQBez.length; j++) {
region.arc(pointsQBez[j].x, pointsQBez[j].y, 4, 0, 2 * Math.PI );
}
ctx.clip(region)
Split the canvas into two segments based on the curve
Calculate the intersection of the start- and end-segment with the canvas border
Close the path (first clipping region)
Draw a rectangle over the whole canvas (second clipping region)
Fill in the two regions created in step four
Steps 3, 4, and 5 in pictures:
Issue
The pink part in the third image above should have the same thickness as the turquoise.
But for some strange reason, the whole inner part of the curve gets filled in.
Additional observations
This behaviour does not show when using CanvasRenderingContext2D.rect() instead of CanvasRenderingContext2D.arc():
When using CanvasRenderingContext2D.arc(), the inner part of the curve that is filled in is not consistent
Because rect does include a call to closePath() while arc doesn't.
Two ways of working around that:
You can call closePath() after each arc:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const pointsQBez = [];
const cx = 75;
const cy = 75;
const rad = 50;
for(let i = 0; i < 180; i++) {
const a = (Math.PI / 180) * i - Math.PI / 2;
const x = cx + Math.cos(a) * rad;
const y = cy + Math.sin(a) * rad;
pointsQBez.push({ x, y });
}
let region = new Path2D();
for (const {x, y} of pointsQBez) {
region.arc(x, y, 4, 0, 2 * Math.PI);
region.closePath();
}
ctx.clip(region);
ctx.fillStyle = "red";
ctx.fillRect(0, 0, canvas.width, canvas.height);
<canvas></canvas>
Or you can moveTo() the entry point of your arc:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const pointsQBez = [];
const cx = 75;
const cy = 75;
const rad = 50;
for(let i = 0; i < 180; i++) {
const a = (Math.PI / 180) * i - Math.PI / 2;
const x = cx + Math.cos(a) * rad;
const y = cy + Math.sin(a) * rad;
pointsQBez.push({ x, y });
}
let region = new Path2D();
for (const {x, y} of pointsQBez) {
region.moveTo(x + 4, y); // x + arc radius
region.arc(x, y, 4, 0, 2 * Math.PI);
}
ctx.clip(region);
ctx.fillStyle = "red";
ctx.fillRect(0, 0, canvas.width, canvas.height);
<canvas></canvas>
I am trying to build a near-perfect circle with quadratic curves in canvas. I have this function for setting up points around a circle and connecting them with quadratic curves:
function calcPointsCircle(cx, cy, radius, dashLength) {
var n = radius / dashLength,
alpha = Math.PI * 2 / n,
i = -1;
while (i < n) {
var theta = alpha * i,
theta2 = alpha * (i + 1);
points.push({
x : (Math.cos(theta) * radius) + cx,
y : (Math.sin(theta) * radius) + cy,
ex : (Math.cos(theta2) * radius) + cx,
ey : (Math.sin(theta2) * radius) + cy,
py : (Math.sin(theta) * radius) + cy
});
i+=2;
}
}
for (i = 0; i < points.length; i++) {
var p = points[i];
ctx.strokeStyle = '#fff';
ctx.quadraticCurveTo(p.x, p.py, p.x, p.y);
ctx.stroke();
}
It works, but the lines are currently straight (which is obvious, since I am using the points x and y coordinates for the control point):
I am looking for a way to automatically calculate the poisitions for the control points based on the circle radius and the number of points... All help is more then welcome
Here's how to calculate the controls points of a set of quadratic curves which approximate a circle circumscribing a regular polygon.
Given:
A centerpoint, radius & sidecount.
For each side of the polygon, calculate:
3 points on a circumscribing circumference and then calculate the quadratic curve control point that causes the curve to pass through those 3 points:
The 2 points of the polygon side are 2 of the 3 points
Calculate the sweep angle between the 2 points of a side (var sweep)
Bisect the sweep angle (sweep/2)
Use trigonometry to calculate the point on the circumference midway between the 2 points of the side.
Calculate the middle control point:
// calc middle control point
var cpX=2*x1-x0/2-x2/2;
var cpY=2*y1-y0/2-y2/2;
Example code and a Demo:
// change sideCount to the # of poly sides desired
//
var sideCount=5;
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
ctx.lineWidth=2;
ctx.fillStyle=randomColor();
// save PI*2
var PI2=Math.PI*2;
// functions to calc a point on circumference of circle
var xx=function(a){return(cx+radius*Math.cos(a));}
var yy=function(a){return(cy+radius*Math.sin(a));}
// general interpolation function
var lerp=function(a,b,x){ return(a+x*(b-a)); }
// define the regular polygon
var cx=150;
var cy=150;
var radius=100;
// calc qCurve controls points and put in sides[] array
var sides=[];
for(var i=0;i<sideCount;i++){
sides.push(makeSide(i,sideCount));
}
// drawing and animating stuff
var percent=0;
var percentDirection=0.50;
$("#toShape").click(function(){
percentDirection=-0.50;
})
$("#toCircle").click(function(){
percentDirection=0.50;
})
animate();
// functions
function animate(){
requestAnimationFrame(animate);
drawSides(percent);
percent+=percentDirection;
if(percent>100){percent=100;}
if(percent<0){percent=0;}
}
function drawSides(pct,color){
ctx.clearRect(0,0,canvas.width,canvas.height);
if(pct==100){
ctx.beginPath();
ctx.arc(cx,cy,radius,0,PI2);
ctx.closePath();
ctx.fill();
}else{
ctx.beginPath();
ctx.moveTo(sides[0].x0,sides[0].y0);
for(var i=0;i<sideCount;i++){
var side=sides[i];
var cpx=lerp(side.midX,side.cpX,pct/100);
var cpy=lerp(side.midY,side.cpY,pct/100);
ctx.quadraticCurveTo(cpx,cpy,side.x2,side.y2);
}
ctx.fill();
}
}
// given a side of a regular polygon,
// calc a qCurve that approximates a circle
function makeSide(n,sideCount){
// starting & ending angles vs centerpoint
var sweep=PI2/sideCount;
var sAngle=sweep*(n-1);
var eAngle=sweep*n;
// given start & end points,
// calc the point on circumference at middle of sweep angle
var x0=xx(sAngle);
var y0=yy(sAngle);
var x1=xx((eAngle+sAngle)/2);
var y1=yy((eAngle+sAngle)/2);
var x2=xx(eAngle);
var y2=yy(eAngle);
// calc the control points to pass a qCurve
// through the 3 points
var dx=x2-x1;
var dy=y2-y1;
var a=Math.atan2(dy,dx);
var midX=lerp(x0,x2,0.50);
var midY=lerp(y0,y2,0.50);
// calc middle control point
var cpX=2*x1-x0/2-x2/2;
var cpY=2*y1-y0/2-y2/2;
return({
x0:x0, y0:y0,
x2:x2, y2:y2,
midX:midX, midY:midY,
cpX:cpX, cpY:cpY,
color:randomColor()
});
}
function randomColor(){
return('#'+Math.floor(Math.random()*16777215).toString(16));
}
body{ background-color: ivory; }
canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<button id="toShape">Animate to Shape</button>
<button id="toCircle">Animate to Circle</button><br>
<canvas id="canvas" width=300 height=300></canvas>
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.strokeRect(50,50,200,200);
ctx.translate(100,100);
ctx.scale(0.751,0.751);
ctx.translate(-100,-100);
ctx.strokeRect(50,50,200,200);
in canvas am drawing a rectangle,scaled and translated rectangle's start position is different from the original.
is there any calcutions to find the difference between the starting position of original rectangle and scaled rectangle.how can i get the difference or start position of scaled rectangle
Translations (moves), scales and rotations are all transforms.
Once you get a few transforms deep, tracking original vs transformed positions with arithmetic gets pretty scary!
Behind the scenes, html canvas uses matrix math to track where your transformed rectangle will be drawn.
You can also use the power of a matrix to track the difference between the top-left of your original rectangle and the top-left of your transformed rectangle.
The matrix is actually an array of 6 numbers. This is the “identity matrix” which represents one point in original untransformed space.
var matrix=[1,0,0,1,0,0];
Assume that your original rectangle is positioned at X/Y (in your case 50/50).
var x=50;
var y=50;
Instead of just ctx.translate(100,100), use this wrapper function to both translate and matrix track.
function translate(translateByX,translateByY){
matrix[4] += matrix[0] * translateByX + matrix[2] * translateByY;
matrix[5] += matrix[1] * translateByX + matrix[3] * translateByY;
ctx.translate(translateByX,translateByY);
}
After you’re back in original space, you have tracked where your transformed rectangle is by using the matrix.
You can get the position of your transformed X/Y (but in original space) like this:
function getXY(){
newX = x * matrix[0] + y * matrix[2] + matrix[4];
newY = x * matrix[1] + y * matrix[3] + matrix[5];
return({x:newX,y:newY});
}
Here is code and a Fiddle: http://jsfiddle.net/m1erickson/yuaMs/
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: ivory; }
canvas{border:1px solid red;}
</style>
<script>
$(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
// test values
// objective: track this point into transformed space
var x=50;
var y=50;
// a transform matrix for point x/y
var matrix=[1,0,0,1,0,0];
// draw the first rectangle in blue
ctx.save();
ctx.beginPath();
ctx.strokeStyle="blue";
ctx.strokeRect(x,y,200,200);
// do transforms using the wrapped transform functions
// so the matrix is also tracked
translate(100,100);
scale(0.751,0.751);
translate(-100,-100);
// draw the second rectangle (now in transformed space)
ctx.beginPath();
ctx.strokeStyle="green";
ctx.strokeRect(x,y,200,200);
ctx.restore();
// Note: ctx.restore() has un-transformed our space
// get our original XY which has been transformed using the matrix
var transformed=getXY();
// draw a dot at the transformed x/y
ctx.beginPath();
ctx.arc(transformed.x,transformed.y,5,0,Math.PI*2,false);
ctx.closePath();
ctx.fillStyle="red";
ctx.fill();
// do the translate
// but also save the translate in the matrix
function translate(x,y){
matrix[4] += matrix[0] * x + matrix[2] * y;
matrix[5] += matrix[1] * x + matrix[3] * y;
ctx.translate(x,y);
}
// do the scale
// but also save the scale in the matrix
function scale(x,y){
matrix[0] *= x;
matrix[1] *= x;
matrix[2] *= y;
matrix[3] *= y;
ctx.scale(x,y);
}
// do the rotate
// but also save the rotate in the matrix
function rotate(radians){
var cos = Math.cos(radians);
var sin = Math.sin(radians);
var m11 = matrix[0] * cos + matrix[2] * sin;
var m12 = matrix[1] * cos + matrix[3] * sin;
var m21 = -matrix[0] * sin + matrix[2] * cos;
var m22 = -matrix[1] * sin + matrix[3] * cos;
matrix[0] = m11;
matrix[1] = m12;
matrix[2] = m21;
matrix[3] = m22;
ctx.rotate(radians);
}
// get the transformed point from the matrix
function getXY(vertex){
newX = x * matrix[0] + y * matrix[2] + matrix[4];
newY = x * matrix[1] + y * matrix[3] + matrix[5];
return({x:newX,y:newY});
}
}); // end $(function(){});
</script>
</head>
<body>
<p>Blue=drawn in original space</p>
<p>Green=drawn transformed space</p>
<p>Red=drawn in original space but tracked with matrix!</p>
<p id="newXY">Tracked x/y=</p>
<canvas id="canvas" width=300 height=300></canvas>
</body>
</html>
Once you've scaled your canvas, every operation that follows will be scaled, including the transforms.
So your second translate :
ctx.translate(-100,-100);
is in fact performing a translate by (-75.1, -75.1) in the original coordinate system.
how come this doesn't work? does rotate only work with images?
context.moveTo(60,60);
context.lineTo(200,60);
context.lineTo(200,200);
context.lineTo(60,200);
context.lineTo(60,60);
context.stroke();
context.rotate(45 * Math.PI / 180);
context.restore();
You are rotating the whole canvas when you use context.rotate, and since the pivot point is defaulted at the coordinates (0, 0), your square sometimes will be drawn out of bounds.
By moving the pivot point to the middle of the square, you can then rotate it successfully.
Note: Make sure you rotate the canvas before you draw the square.
// pivot point coordinates = the center of the square
var cx = 130; // (60+200)/2
var cy = 130; // (60+200)/2
// Note that the x and y values of the square
// are relative to the pivot point.
var x = -70; // cx + x = 130 - 70 = 60
var y = -70; // cy + y = 130 - 70 = 60
var w = 140; // (cx + x) + w = 60 + w = 200
var h = 140; // (cy + y) + h = 60 + h = 200
var deg = 45;
context.save();
context.translate(cx, cy);
context.rotate(deg * Math.PI/180);
context.fillRect(x, y, w, h);
context.restore();
Explanation:
context.save(); saves the current state of the coordinate system.
context.translate(cx, cy); moves the pivot point.
context.rotate(deg * Math.PI/180); rotates the square to deg degrees (Note that the parameter is in radians, not degrees)
context.fillRect( x, y, w, h ); draws the square
context.restore(); restores the last state of the coordinate system.
Here is a JS Fiddle example.
Here is another JS Fiddle example that features a HTML5 slider.
I have a convex polygon P1 of N points. This polygon could be any shape or proportion (as long as it is still convex).
I need to compute another polygon P2 using the original polygons geometry, but "expanded" by a given number of units. What might the algorithm be for expanding a convex polygon?
To expand a convex polygon, draw a line parallel to each edge and the given number of units away. Then use the intersection points of the new lines as the vertices of the expanded polygon. The javascript/canvas at the end follows this functional breakdown:
Step 1: Figure out which side is "out"
The order of the vertices (points) matters. In a convex polygon, they can be listed in a clockwise (CW), or a counter-clockwise (CCW) order. In a CW polygon, turn one of the edges 90 degrees CCW to obtain an outward-facing normal. On a CCW polygon, turn it CW instead.
If the turn direction of the vertices is not known in advance, examine how the second edge turns from the first. In a convex polygon, the remaining edges will keep turning in the same direction:
Find the CW normal of the first edge. We don't know yet whether it's facing inward or outward.
Compute the dot product of the second edge with the normal we computed. If the second edge turns CW, the dot product will be positive. It will be negative otherwise.
Math:
// in vector terms:
v01 = p1 - p0 // first edge, as a vector
v12 = p2 - p1 // second edge, as a vector
n01 = (v01.y, -v01.x) // CW normal of first edge
d = v12 * n01 // dot product
// and in x,y terms:
v01 = (p1.x-p0.x, p1.y-p0.y) // first edge, as a vector
v12 = (p2.x-p1.x, p2.y-p1.y) // second edge, as a vector
n01 = (v01.y, -v01.x) // CW normal of first edge
d = v12.x * n01.x + v12.y * n01.y; // dot product: v12 * n01
if (d > 0) {
// the polygon is CW
} else {
// the polygon is CCW
}
// and what if d==0 ?
// -- that means the second edge continues in the same
// direction as a first. keep looking for an edge that
// actually turns either CW or CCW.
Code:
function vecDot(v1, v2) {
return v1.x * v2.x + v1.y * v2.y;
}
function vecRot90CW(v) {
return { x: v.y, y: -v.x };
}
function vecRot90CCW(v) {
return { x: -v.y, y: v.x };
}
function polyIsCw(p) {
return vecDot(
vecRot90CW({ x: p[1].x - p[0].x, y: p[1].y - p[0].y }),
{ x: p[2].x - p[1].x, y: p[2].y - p[1].y }) >= 0;
}
var rot = polyIsCw(p) ? vecRot90CCW : vecRot90CW;
Step 2: Find lines parallel to the polygon edges
Now that we know which side is out, we can compute lines parallel to each polygon edge, at exactly the required distance. Here's our strategy:
For each edge, compute its outward-facing normal
Normalize the normal, such that its length becomes one unit
Multiply the normal by the distance we want the expanded polygon to be from the original
Add the multiplied normal to both ends of the edge. That will give us two points on the parallel line. Those two points are enough to define the parallel line.
Code:
// given two vertices pt0 and pt1, a desired distance, and a function rot()
// that turns a vector 90 degrees outward:
function vecUnit(v) {
var len = Math.sqrt(v.x * v.x + v.y * v.y);
return { x: v.x / len, y: v.y / len };
}
function vecMul(v, s) {
return { x: v.x * s, y: v.y * s };
}
var v01 = { x: pt1.x - pt0.x, y: pt1.y - pt0.y }; // edge vector
var d01 = vecMul(vecUnit(rot(v01)), distance); // multiplied unit normal
var ptx0 = { x: pt0.x + d01.x, y: pt0.y + d01.y }; // two points on the
var ptx1 = { x: pt1.x + d01.x, y: pt1.y + d01.y }; // parallel line
Step 3: Compute the intersections of the parallel lines
--these will be the vertices of the expanded polygon.
Math:
A line going through two points P1, P2 can be described as:
P = P1 + t * (P2 - P1)
Two lines can be described as
P = P1 + t * (P2 - P1)
P = P3 + u * (P4 - P3)
And their intersection has to be on both lines:
P = P1 + t * (P2 - P1) = P3 + u * (P4 - P3)
This can be massaged to look like:
(P2 - P1) * t + (P3 - P4) * u = P3 - P1
Which in x,y terms is:
(P2.x - P1.x) * t + (P3.x - P4.x) * u = P3.x - P1.x
(P2.y - P1.y) * t + (P3.y - P4.y) * u = P3.y - P1.y
As the points P1, P2, P3 and P4 are known, so are the following values:
a1 = P2.x - P1.x a2 = P2.y - P1.y
b1 = P3.x - P4.x b2 = P3.y - P4.y
c1 = P3.x - P1.x c2 = P3.y - P1.y
This shortens our equations to:
a1*t + b1*u = c1
a2*t + b2*u = c2
Solving for t gets us:
t = (b1*c2 - b2*c1)/(a2*b1 - a1*b2)
Which lets us find the intersection at P = P1 + t * (P2 - P1).
Code:
function intersect(line1, line2) {
var a1 = line1[1].x - line1[0].x;
var b1 = line2[0].x - line2[1].x;
var c1 = line2[0].x - line1[0].x;
var a2 = line1[1].y - line1[0].y;
var b2 = line2[0].y - line2[1].y;
var c2 = line2[0].y - line1[0].y;
var t = (b1*c2 - b2*c1) / (a2*b1 - a1*b2);
return {
x: line1[0].x + t * (line1[1].x - line1[0].x),
y: line1[0].y + t * (line1[1].y - line1[0].y)
};
}
Step 4: Deal with special cases
There is a number of special cases that merit attention. Left as an exercise to the reader...
When there's a very sharp angle between two edges, the expanded vertex can be very far from the original one. You might want to consider clipping the expanded edge if it goes beyond some threshold. At the extreme case, the angle is zero, which suggests that the expanded vertex is at infinity, causing division by zero in the arithmetic. Watch out.
When the first two edges are on the same line, you can't tell if it's a CW or a CCW polygon by looking just at them. Look at more edges.
Non convex polygons are much more interesting... and are not tackled here.
Full sample code
Drop this in a canvas-capable browser. I used Chrome 6 on Windows. The triangle and its expanded version should animate.
canvas { border: 1px solid #ccc; }
$(function() {
var canvas = document.getElementById('canvas');
if (canvas.getContext) {
var context = canvas.getContext('2d');
// math for expanding a polygon
function vecUnit(v) {
var len = Math.sqrt(v.x * v.x + v.y * v.y);
return { x: v.x / len, y: v.y / len };
}
function vecMul(v, s) {
return { x: v.x * s, y: v.y * s };
}
function vecDot(v1, v2) {
return v1.x * v2.x + v1.y * v2.y;
}
function vecRot90CW(v) {
return { x: v.y, y: -v.x };
}
function vecRot90CCW(v) {
return { x: -v.y, y: v.x };
}
function intersect(line1, line2) {
var a1 = line1[1].x - line1[0].x;
var b1 = line2[0].x - line2[1].x;
var c1 = line2[0].x - line1[0].x;
var a2 = line1[1].y - line1[0].y;
var b2 = line2[0].y - line2[1].y;
var c2 = line2[0].y - line1[0].y;
var t = (b1*c2 - b2*c1) / (a2*b1 - a1*b2);
return {
x: line1[0].x + t * (line1[1].x - line1[0].x),
y: line1[0].y + t * (line1[1].y - line1[0].y)
};
}
function polyIsCw(p) {
return vecDot(
vecRot90CW({ x: p[1].x - p[0].x, y: p[1].y - p[0].y }),
{ x: p[2].x - p[1].x, y: p[2].y - p[1].y }) >= 0;
}
function expandPoly(p, distance) {
var expanded = [];
var rot = polyIsCw(p) ? vecRot90CCW : vecRot90CW;
for (var i = 0; i < p.length; ++i) {
// get this point (pt1), the point before it
// (pt0) and the point that follows it (pt2)
var pt0 = p[(i > 0) ? i - 1 : p.length - 1];
var pt1 = p[i];
var pt2 = p[(i < p.length - 1) ? i + 1 : 0];
// find the line vectors of the lines going
// into the current point
var v01 = { x: pt1.x - pt0.x, y: pt1.y - pt0.y };
var v12 = { x: pt2.x - pt1.x, y: pt2.y - pt1.y };
// find the normals of the two lines, multiplied
// to the distance that polygon should inflate
var d01 = vecMul(vecUnit(rot(v01)), distance);
var d12 = vecMul(vecUnit(rot(v12)), distance);
// use the normals to find two points on the
// lines parallel to the polygon lines
var ptx0 = { x: pt0.x + d01.x, y: pt0.y + d01.y };
var ptx10 = { x: pt1.x + d01.x, y: pt1.y + d01.y };
var ptx12 = { x: pt1.x + d12.x, y: pt1.y + d12.y };
var ptx2 = { x: pt2.x + d12.x, y: pt2.y + d12.y };
// find the intersection of the two lines, and
// add it to the expanded polygon
expanded.push(intersect([ptx0, ptx10], [ptx12, ptx2]));
}
return expanded;
}
// drawing and animating a sample polygon on a canvas
function drawPoly(p) {
context.beginPath();
context.moveTo(p[0].x, p[0].y);
for (var i = 0; i < p.length; ++i) {
context.lineTo(p[i].x, p[i].y);
}
context.closePath();
context.fill();
context.stroke();
}
function drawPolyWithMargin(p, margin) {
context.fillStyle = "rgb(255,255,255)";
context.strokeStyle = "rgb(200,150,150)";
drawPoly(expandPoly(p, margin));
context.fillStyle = "rgb(150,100,100)";
context.strokeStyle = "rgb(200,150,150)";
drawPoly(p);
}
var p = [{ x: 100, y: 100 }, { x: 200, y: 120 }, { x: 80, y: 200 }];
setInterval(function() {
for (var i in p) {
var pt = p[i];
if (pt.vx === undefined) {
pt.vx = 5 * (Math.random() - 0.5);
pt.vy = 5 * (Math.random() - 0.5);
}
pt.x += pt.vx;
pt.y += pt.vy;
if (pt.x < 0 || pt.x > 400) { pt.vx = -pt.vx; }
if (pt.y < 0 || pt.y > 400) { pt.vy = -pt.vy; }
}
context.clearRect(0, 0, 800, 400);
drawPolyWithMargin(p, 10);
}, 50);
}
});
<html>
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
</head>
<body>
<canvas id="canvas" width="400" height="400"></canvas>
</body>
</html>
sample code disclaimers:
the sample sacrifices some efficiency for the sake of clarity. In your code, you may want to compute each edge's expanded parallel just once, and not twice as in here
the canvas's y coordinate grows downward, which inverts the CW/CCW logic. Things keep on working though as we just need to turn the outward normals in a direction opposite to the polygon's -- and both get flipped.
For each line segment of the original, find the midpoint m and (unit length) outward normal u of the segment. The corresponding segment of the expanded polygon will then lie on the line through m+n*u (where you want to expand the original by n) with normal u. To find the vertices of the expanded polygon you then need to find the intersection of pairs of successive lines.
If the polygon is centered on the origin simply multiply each of the points by a common scaling factor.
If the polygon is not centered on the origin then first translate so the center is on the origin, scale, and then translate it back to where it was.
After your comment
It seems you want all points to be moved the same distance away from the origin.
You can do this for each point by getting the normalised vector to this point. multiplying this by your 'expand constant' and adding the resulting vector back onto the original point.
n.b. You will still have to translate-modify-translate if the center is not also the origin for this solution.
Let the points of the polygon be A1, B1, C1... Now you have lines from A1 to B1, then from B1 to C1... We want to compute points A2, B2, C2 of the polygon P2.
If you bisect angle, for example A1 B1 C1, you will have a line which goes in the direction you want. Now you can find a point B2 on it which is the appropriate distance from B1 on bisector line.
Repeat this for all points of the polygon P1.
Look at straight skeletons. As has been implied here there are a number of tricky issues with non convex polygons that you have been mecifully spared!