Related
I am using following javascript algorithm to draw dashed line on canvas. this algo draws horizontal line correctly but unable to draw vertical line:
g.dashedLine = function (x, y, x2, y2, dashArray) {
this.beginPath();
this.lineWidth = "2";
if (!dashArray)
dashArray = [10, 5];
if (dashLength == 0)
dashLength = 0.001; // Hack for Safari
var dashCount = dashArray.length;
this.moveTo(x, y);
var dx = (x2 - x);
var dy = (y2 - y);
var slope = dy / dx;
var distRemaining = Math.sqrt(dx * dx + dy * dy);
var dashIndex = 0;
var draw = true;
while (distRemaining >= 0.1) {
var dashLength = dashArray[(dashIndex++) % dashCount];
if (dashLength > distRemaining)
dashLength = distRemaining;
var xStep = Math.sqrt((dashLength * dashLength) / (1 + slope * slope));
if (x < x2) {
x += xStep;
y += slope * xStep;
}
else {
x -= xStep;
y -= slope * xStep;
}
if (draw) {
this.lineTo(x, y);
}
else {
this.moveTo(x, y);
}
distRemaining -= dashLength;
draw = !draw;
}
this.stroke();
this.closePath();
};
following Points used for testing vertical line draw:
g.dashedLine(157, 117, 157,153, [10, 5]);
following Points used for testing horizontal line draw:
g.dashedLine(157, 117, 160,157, [10, 5]);
When the line is vertical, dx = 0 which leads to slope = Infinity. If you write your own dash logic then you need to handle the special case where dx = 0 (or very near 0). In this special case you would have to work with the inverse slope (i.e. dx / dy) and yStep (instead of xStep).
A bigger question is why are you writing your own dash logic. Canvas has built-in support for dashed lines. Call setLineDash() function to set the dash pattern. Restore the previous dash pattern when done. For example...
g.dashedLine = function (x, y, x2, y2, dashArray) {
this.beginPath();
this.lineWidth = "2";
if (!dashArray)
dashArray = [10, 5];
var prevDashArray = this.getLineDash();
this.setLineDash(dashArray);
this.moveTo(x, y);
this.lineTo(x2, y2);
this.stroke();
this.closePath();
this.setLineDash(prevDashArray);
};
I've been trying to understand arc svg since it seems I need them in plotly -- my goal is to plot circle intersections.
My original idea was something like this:
for every intersection, to find the start and end coordinates as well as the height - but I am not very sure of where to go from here. It seems I am lacking the rotation and Large Arc Flag / Sweep parameters, and I am not sure how I would go about retrieving them. If anyone could point me into the right direction here, that would be great!
Circles and intercepting points
Don't know much about SVG arcTo. MDN gives "A rx,ry xAxisRotate LargeArcFlag,SweepFlag x,y". as an arc in the path element. What rx and ry are??? I would guess radius for x,y.
I am guessing you would use it as
// x,y start position
// rx,ry radius x and y
// x1,y1 end position
<path d="M x,y A rx, ry, 0 1 1 x1, y1"/>
Below is the problem solved as javascript. I have Commented the part you need for the SVG. The two end points (intercepts)
There is a lot of redundancy but its not clear what you want so the code provides how to find other parts of two intersecting circles.
Law of Cosines
The math to solve the problem is called the law of cosines that is used to solve triangles.
In this case the triangle is created from 3 lengths. One each of the circle radius and one is the distance between circle centers. The image gives more details
With the angle c you can find the lengths GE, DE, and EF. If you want the angle for the other side at point f just swap B and C.
Example
Move mouse to check intercept.
const ctx = canvas.getContext("2d");
const m = {
x: 0,
y: 0
};
document.addEventListener("mousemove", e => {
var b = canvas.getBoundingClientRect();
m.x = e.pageX - b.left - scrollX;
m.y = e.pageY - b.top - scrollY;
});
const PI = Math.PI;
const PI2 = Math.PI * 2;
const circles = [];
function circle(x, y, r, col, f = 0, t = PI2, w = 2) {
var c;
circles.push(c = { x, y,r, col, f, t, w});
return c;
};
function drawCircle(A) {
ctx.strokeStyle = A.col;
ctx.lineWidth = A.w;
ctx.beginPath();
ctx.arc(A.x, A.y, A.r, A.f, A.t);
ctx.stroke();
}
function mark(x, y, r, c) {
ctx.strokeStyle = c;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(x, y, r, 0, PI2);
ctx.stroke();
}
function line(A, B, c) {
ctx.strokeStyle = c;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.lineTo(A.x, A.y);
ctx.lineTo(B.x, B.y);
ctx.stroke();
}
// note I am sharing calc results between function
function circleIntercept(A, B) {
var vx, vy, dist, c, d, x, y, x1, y1, x2, y2, dir, a1, a2;
// Vec from A to B
vx = B.x - A.x;
vy = B.y - A.y;
// Distance between
dist = Math.sqrt(vx * vx + vy * vy);
// Are the intercepting
if (dist < A.r + B.r && dist > B.r - A.r) {
c = (B.r * B.r - (dist * dist + A.r * A.r)) / (-2 * dist);
// Find mid point on cord
x = A.x + vx * (c / dist);
y = A.y + vy * (c / dist);
mark(x, y, 5, "blue");
// Find circumference intercepts
//#################################################################
//=================================================================
// SVG path
// Use x1,y1 and x2,y2 as the start and end angles of the ArcTo SVG
d = Math.sqrt(A.r * A.r - c * c);
x1 = x - vy * (d / dist);
y1 = y + vx * (d / dist);
x2 = x + vy * (d / dist);
y2 = y - vx * (d / dist);
// SVG path from above coords
// d = `M ${x1}, ${y1} A ${A.r}, ${A,r1} 0, 1, 1, ${x2}, ${y2}`;
//=================================================================
// draw the chord
line({x: x1,y: y1}, {x: x2,y: y2}, "red");
// mark the intercepts
mark(x1, y1, 5, "Green");
mark(x2, y2, 5, "Orange");
// Get direction from A to B
dir = Math.atan2(vy, vx);
// Get half inside sweep
a1 = Math.acos(c / A.r);
// Draw arc for A
A.col = "black";
A.w = 4;
A.f = dir - a1;
A.t = dir + a1;
drawCircle(A);
A.col = "#aaa";
A.w = 2;
A.f = 0;
A.t = PI2;
// inside sweep for B
a2 = Math.asin(d / B.r);
// Draw arc for B
B.col = "black";
B.w = 4;
if (dist < c) {
B.t = dir - a2;
B.f = dir + a2;
} else {
B.f = dir + PI - a2;
B.t = dir + PI + a2;
}
drawCircle(B);
B.col = "#aaa";
B.w = 2;
B.f = 0;
B.t = PI2;
}
}
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var C1 = circle(cw, ch, ch * 0.5, "#aaa");
var C2 = circle(cw, ch, ch * 0.8, "#aaa");
function update(timer) {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.globalAlpha = 1;
if (w !== innerWidth || h !== innerHeight) {
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
C1.x = cw;
C1.y = ch;
C1.r = ch * 0.5;
ctx.lineCap = "round";
}
C2.x = m.x;
C2.y = m.y;
ctx.clearRect(0, 0, w, h);
drawCircle(C1);
drawCircle(C2);
circleIntercept(C1, C2);
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
Let's start with some terminology to clear up what is a clockwise direction (remember the y axis of SVG goes down): the first circle has radius r1, the second r2.
If the center of the first circle is lower than that of the second (cy1 > cy2), then name the intersection point with the smaller x coordinate (x1, y1), and the other (x2, y2).
If the center of the first circle is higher than that of the second (cy1 < cy2), then name the intersection point with the greater x coordinate (x1, y1), and the other (x2, y2).
Else, name the intersection point with the smaller y coordinate (x1, y1), and the other (x2, y2).
Now we will draw an arc from the first to the second intersection point with the radius of the first circle. The first two arc parameters are the horizontal and vertical radius. Since we are drawing a circle, both are identical. For the same reason, rotating the radii does not make sense and the third parameter is 0.
The intersection of two circles always uses the small arc (the large arc would be used for the union), therefore the large arc flag is 0. We are drawing the arc clockwise, therefore the sweep flag is 1.
Still unclear? The spec uses this picture for explaining the flags:
The second arc is going from the second to the first intersection point with the radius of the second circle. The flags remain the same.
The result looks like this:
M x1, y1 A r1 r1 0 0 1 x2, y2 A r2 r2 0 0 1 x1, y1 Z
I am trying to extend a line (from to points(X,Y)) to the end of the drawing area.
so far i found a couple of instructions on how to calculate the extension end point.
however i don't really get it done it works in one direction and breaks as soon as you reach over the middle point.
see attached code sample (the real product i am working on is in swift, but as it is not a programming language related issue, i ported it to javascript)
on the right side it seems to work, black line is the one the user selects, red one is the extension to the edge of canvas, going to the left side produces garbage.
var canvas = document.getElementById("myCanvas");
var endPoint = {
x: 200,
y: 200
};
function draw() {
//Demo only in final product user also can select the startpoint
startPoint = {
x: 150,
y: 150
}
screenMax = {
x: canvas.height,
y: canvas.width
}
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.moveTo(startPoint.x, startPoint.y);
ctx.lineTo(endPoint.x, endPoint.y);
ctx.strokeStyle = "#000000";
ctx.stroke();
//Extend line to end of canvas according to slope
var slope = 1.0
var extendedPoint = {
x: 0,
y: 0
}
if (endPoint.x != startPoint.x) {
slope = (endPoint.y - startPoint.y) / (endPoint.x - startPoint.x);
extendedPoint = {
x: screenMax.x,
y: slope * (screenMax.x - endPoint.x) + endPoint.y
}
} else {
slope = 0
extendedPoint.x = endPoint.x;
extendedPoint.y = screenMax.y;
}
console.log(endPoint);
//Draw the Extension
ctx.beginPath();
ctx.moveTo(endPoint.x, endPoint.y);
ctx.lineTo(extendedPoint.x, extendedPoint.y);
ctx.strokeStyle = "#FF0000";
ctx.stroke();
}
//initial draw
draw();
//handle Mouse dOwn
canvas.onmousedown = function(e) {
handleMouseDown(e);
}
// handle the mousedown event
//Set new endpoint
function handleMouseDown(e) {
mouseX = parseInt(e.clientX);
mouseY = parseInt(e.clientY);
endPoint = {
x: mouseX,
y: mouseY
}
draw();
}
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="300" height="300" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>
</body>
</html>
This function may help, takes the line x1,y1 to x2,y2 and extends it to the border defined by left,top,right,bottom returning the intercept point as {x:?,y:?}
function toBorder(x1, y1, x2, y2, left, top, right, bottom){
var dx, dy, py, vx, vy;
vx = x2 - x1;
vy = y2 - y1;
dx = vx < 0 ? left : right;
dy = py = vy < 0 ? top : bottom;
if(vx === 0){
dx = x1;
}else if(vy === 0){
dy = y1;
}else{
dy = y1 + (vy / vx) * (dx - x1);
if(dy < top || dy > bottom){
dx = x1 + (vx / vy) * (py - y1);
dy = py;
}
}
return {x : dx, y : dy}
}
Slope approach is not universal - it cannot work with vertical lines (x0=x1).
I'd use parametric representation of ray (line)
x0 = startPoint.x
x1 = endPoint.x
y0 = startPoint.y
y1 = endPoint.y
dx = x1 - x0
dy = y1 - y0
x = x0 + dx * t
y = y0 + dy * t
Now check what border will be intersected first (with smaller t value)
//prerequisites: potential border positions
if dx > 0 then
bx = width
else
bx = 0
if dy > 0 then
by = height
else
bx = 0
//first check for horizontal/vertical lines
if dx = 0 then
return ix = x0, iy = by
if dy = 0 then
return iy = y0, ix = bx
//in general case find parameters of intersection with horizontal and vertical edge
tx = (bx - x0) / dx
ty = (by - y0) / dy
//and get intersection for smaller parameter value
if tx <= ty then
ix = bx
iy = y0 + tx * dy
else
iy = by
ix = x0 + ty * dx
return ix, iy
I'm new to HTML5 Canvas and I'm trying to draw a triangle with rounded corners.
I have tried
ctx.lineJoin = "round";
ctx.lineWidth = 20;
but none of them are working.
Here's my code:
var ctx = document.querySelector("canvas").getContext('2d');
ctx.scale(5, 5);
var x = 18 / 2;
var y = 0;
var triangleWidth = 18;
var triangleHeight = 8;
// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>
Could you help me?
Rounding corners
An invaluable function I use a lot is rounded polygon. It takes a set of 2D points that describe a polygon's vertices and adds arcs to round the corners.
The problem with rounding corners and keeping within the constraint of the polygons area is that you can not always fit a round corner that has a particular radius.
In these cases you can either ignore the corner and leave it as pointy or, you can reduce the rounding radius to fit the corner as best possible.
The following function will resize the corner rounding radius to fit the corner if the corner is too sharp and the lines from the corner not long enough to get the desired radius in.
Note the code has comments that refer to the Maths section below if you want to know what is going on.
roundedPoly(ctx, points, radius)
// ctx is the context to add the path to
// points is a array of points [{x :?, y: ?},...
// radius is the max rounding radius
// this creates a closed polygon.
// To draw you must call between
// ctx.beginPath();
// roundedPoly(ctx, points, radius);
// ctx.stroke();
// ctx.fill();
// as it only adds a path and does not render.
function roundedPoly(ctx, points, radiusAll) {
var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut,radius;
// convert 2 points into vector form, polar form, and normalised
var asVec = function(p, pp, v) {
v.x = pp.x - p.x;
v.y = pp.y - p.y;
v.len = Math.sqrt(v.x * v.x + v.y * v.y);
v.nx = v.x / v.len;
v.ny = v.y / v.len;
v.ang = Math.atan2(v.ny, v.nx);
}
radius = radiusAll;
v1 = {};
v2 = {};
len = points.length;
p1 = points[len - 1];
// for each point
for (i = 0; i < len; i++) {
p2 = points[(i) % len];
p3 = points[(i + 1) % len];
//-----------------------------------------
// Part 1
asVec(p2, p1, v1);
asVec(p2, p3, v2);
sinA = v1.nx * v2.ny - v1.ny * v2.nx;
sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
angle = Math.asin(sinA < -1 ? -1 : sinA > 1 ? 1 : sinA);
//-----------------------------------------
radDirection = 1;
drawDirection = false;
if (sinA90 < 0) {
if (angle < 0) {
angle = Math.PI + angle;
} else {
angle = Math.PI - angle;
radDirection = -1;
drawDirection = true;
}
} else {
if (angle > 0) {
radDirection = -1;
drawDirection = true;
}
}
if(p2.radius !== undefined){
radius = p2.radius;
}else{
radius = radiusAll;
}
//-----------------------------------------
// Part 2
halfAngle = angle / 2;
//-----------------------------------------
//-----------------------------------------
// Part 3
lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
//-----------------------------------------
//-----------------------------------------
// Special part A
if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
lenOut = Math.min(v1.len / 2, v2.len / 2);
cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
} else {
cRadius = radius;
}
//-----------------------------------------
// Part 4
x = p2.x + v2.nx * lenOut;
y = p2.y + v2.ny * lenOut;
//-----------------------------------------
// Part 5
x += -v2.ny * cRadius * radDirection;
y += v2.nx * cRadius * radDirection;
//-----------------------------------------
// Part 6
ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
//-----------------------------------------
p1 = p2;
p2 = p3;
}
ctx.closePath();
}
You may wish to add to each point a radius eg {x :10,y:10,radius:20} this will set the max radius for that point. A radius of zero will be no rounding.
The maths
The following illistration shows one of two possibilities, the angle to fit is less than 90deg, the other case (greater than 90) just has a few minor calculation differences (see code).
The corner is defined by the three points in red A, B, and C. The circle radius is r and we need to find the green points F the circle center and D and E which will define the start and end angles of the arc.
First we find the angle between the lines from B,A and B,C this is done by normalising the vectors for both lines and getting the cross product. (Commented as Part 1) We also find the angle of line BC to the line at 90deg to BA as this will help determine which side of the line to put the circle.
Now we have the angle between the lines, we know that half that angle defines the line that the center of the circle will sit F but we do not know how far that point is from B (Commented as Part 2)
There are two right triangles BDF and BEF which are identical. We have the angle at B and we know that the side DF and EF are equal to the radius of the circle r thus we can solve the triangle to get the distance to F from B
For convenience rather than calculate to F is solve for BD (Commented as Part 3) as I will move along the line BC by that distance (Commented as Part 4) then turn 90deg and move up to F (Commented as Part 5) This in the process gives the point D and moving along the line BA to E
We use points D and E and the circle center F (in their abstract form) to calculate the start and end angles of the arc. (done in the arc function part 6)
The rest of the code is concerned with the directions to move along and away from lines and which direction to sweep the arc.
The code section (special part A) uses the lengths of both lines BA and BC and compares them to the distance from BD if that distance is greater than half the line length we know the arc can not fit. I then solve the triangles to find the radius DF if the line BD is half the length of shortest line of BA and BC
Example use.
The snippet is a simple example of the above function in use. Click to add points to the canvas (needs a min of 3 points to create a polygon). You can drag points and see how the corner radius adapts to sharp corners or short lines. More info when snippet is running. To restart rerun the snippet. (there is a lot of extra code that can be ignored)
The corner radius is set to 30.
const ctx = canvas.getContext("2d");
const mouse = {
x: 0,
y: 0,
button: false,
drag: false,
dragStart: false,
dragEnd: false,
dragStartX: 0,
dragStartY: 0
}
function mouseEvents(e) {
mouse.x = e.pageX;
mouse.y = e.pageY;
const lb = mouse.button;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
if (lb !== mouse.button) {
if (mouse.button) {
mouse.drag = true;
mouse.dragStart = true;
mouse.dragStartX = mouse.x;
mouse.dragStartY = mouse.y;
} else {
mouse.drag = false;
mouse.dragEnd = true;
}
}
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
const pointOnLine = {x:0,y:0};
function distFromLines(x,y,minDist){
var index = -1;
const v1 = {};
const v2 = {};
const v3 = {};
const point = P2(x,y);
eachOf(polygon,(p,i)=>{
const p1 = polygon[(i + 1) % polygon.length];
v1.x = p1.x - p.x;
v1.y = p1.y - p.y;
v2.x = point.x - p.x;
v2.y = point.y - p.y;
const u = (v2.x * v1.x + v2.y * v1.y)/(v1.y * v1.y + v1.x * v1.x);
if(u >= 0 && u <= 1){
v3.x = p.x + v1.x * u;
v3.y = p.y + v1.y * u;
dist = Math.hypot(v3.y - point.y, v3.x - point.x);
if(dist < minDist){
minDist = dist;
index = i;
pointOnLine.x = v3.x;
pointOnLine.y = v3.y;
}
}
})
return index;
}
function roundedPoly(ctx, points, radius) {
var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut;
var asVec = function(p, pp, v) {
v.x = pp.x - p.x;
v.y = pp.y - p.y;
v.len = Math.sqrt(v.x * v.x + v.y * v.y);
v.nx = v.x / v.len;
v.ny = v.y / v.len;
v.ang = Math.atan2(v.ny, v.nx);
}
v1 = {};
v2 = {};
len = points.length;
p1 = points[len - 1];
for (i = 0; i < len; i++) {
p2 = points[(i) % len];
p3 = points[(i + 1) % len];
asVec(p2, p1, v1);
asVec(p2, p3, v2);
sinA = v1.nx * v2.ny - v1.ny * v2.nx;
sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
angle = Math.asin(sinA); // warning you should guard by clampling
// to -1 to 1. See function roundedPoly in answer or
// Math.asin(Math.max(-1, Math.min(1, sinA)))
radDirection = 1;
drawDirection = false;
if (sinA90 < 0) {
if (angle < 0) {
angle = Math.PI + angle;
} else {
angle = Math.PI - angle;
radDirection = -1;
drawDirection = true;
}
} else {
if (angle > 0) {
radDirection = -1;
drawDirection = true;
}
}
halfAngle = angle / 2;
lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
lenOut = Math.min(v1.len / 2, v2.len / 2);
cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
} else {
cRadius = radius;
}
x = p2.x + v2.nx * lenOut;
y = p2.y + v2.ny * lenOut;
x += -v2.ny * cRadius * radDirection;
y += v2.nx * cRadius * radDirection;
ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
p1 = p2;
p2 = p3;
}
ctx.closePath();
}
const eachOf = (array, callback) => { var i = 0; while (i < array.length && callback(array[i], i++) !== true); };
const P2 = (x = 0, y = 0) => ({x, y});
const polygon = [];
function findClosestPointIndex(x, y, minDist) {
var index = -1;
eachOf(polygon, (p, i) => {
const dist = Math.hypot(x - p.x, y - p.y);
if (dist < minDist) {
minDist = dist;
index = i;
}
});
return index;
}
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var dragPoint;
var globalTime;
var closestIndex = -1;
var closestLineIndex = -1;
var cursor = "default";
const lineDist = 10;
const pointDist = 20;
var toolTip = "";
// main update function
function update(timer) {
globalTime = timer;
cursor = "crosshair";
toolTip = "";
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if (w !== innerWidth - 4 || h !== innerHeight - 4) {
cw = (w = canvas.width = innerWidth - 4) / 2;
ch = (h = canvas.height = innerHeight - 4) / 2;
} else {
ctx.clearRect(0, 0, w, h);
}
if (mouse.drag) {
if (mouse.dragStart) {
mouse.dragStart = false;
closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
if(closestIndex === -1){
closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
if(closestLineIndex === -1){
polygon.push(dragPoint = P2(mouse.x, mouse.y));
}else{
polygon.splice(closestLineIndex+1,0,dragPoint = P2(mouse.x, mouse.y));
}
}else{
dragPoint = polygon[closestIndex];
}
}
dragPoint.x = mouse.x;
dragPoint.y = mouse.y
cursor = "none";
}else{
closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
if(closestIndex === -1){
closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
if(closestLineIndex > -1){
toolTip = "Click to cut line and/or drag to move.";
}
}else{
toolTip = "Click drag to move point.";
closestLineIndex = -1;
}
}
ctx.lineWidth = 4;
ctx.fillStyle = "#09F";
ctx.strokeStyle = "#000";
ctx.beginPath();
roundedPoly(ctx, polygon, 30);
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.lineWidth = 0.5;
eachOf(polygon, p => ctx.lineTo(p.x,p.y) );
ctx.closePath();
ctx.stroke();
ctx.strokeStyle = "orange";
ctx.lineWidth = 1;
eachOf(polygon, p => ctx.strokeRect(p.x-2,p.y-2,4,4) );
if(closestIndex > -1){
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
dragPoint = polygon[closestIndex];
ctx.strokeRect(dragPoint.x-4,dragPoint.y-4,8,8);
cursor = "move";
}else if(closestLineIndex > -1){
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
var p = polygon[closestLineIndex];
var p1 = polygon[(closestLineIndex + 1) % polygon.length];
ctx.beginPath();
ctx.lineTo(p.x,p.y);
ctx.lineTo(p1.x,p1.y);
ctx.stroke();
ctx.strokeRect(pointOnLine.x-4,pointOnLine.y-4,8,8);
cursor = "pointer";
}
if(toolTip === "" && polygon.length < 3){
toolTip = "Click to add a corners of a polygon.";
}
canvas.title = toolTip;
canvas.style.cursor = cursor;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas {
border: 2px solid black;
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
I started by using #Blindman67 's answer, which works pretty well for basic static shapes.
I ran into the problem that when using the arc approach, having two points right next to each other is very different than having just one point. With two points next to each other, it won't be rounded, even if that is what your eye would expect. This is extra jarring if you are animating the polygon points.
I fixed this by using Bezier curves instead. IMO this is conceptually a little cleaner as well. I just make each corner with a quadratic curve where the control point is where the original corner was. This way, having two points in the same spot is virtually the same as only having one point.
I haven't compared performance but seems like canvas is pretty good at drawing Beziers.
As with #Blindman67 's answer, this doesn't actually draw anything so you will need to call ctx.beginPath() before and ctx.stroke() after.
/**
* Draws a polygon with rounded corners
* #param {CanvasRenderingContext2D} ctx The canvas context
* #param {Array} points A list of `{x, y}` points
* #radius {number} how much to round the corners
*/
function myRoundPolly(ctx, points, radius) {
const distance = (p1, p2) => Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
const lerp = (a, b, x) => a + (b - a) * x
const lerp2D = (p1, p2, t) => ({
x: lerp(p1.x, p2.x, t),
y: lerp(p1.y, p2.y, t)
})
const numPoints = points.length
let corners = []
for (let i = 0; i < numPoints; i++) {
let lastPoint = points[i]
let thisPoint = points[(i + 1) % numPoints]
let nextPoint = points[(i + 2) % numPoints]
let lastEdgeLength = distance(lastPoint, thisPoint)
let lastOffsetDistance = Math.min(lastEdgeLength / 2, radius)
let start = lerp2D(
thisPoint,
lastPoint,
lastOffsetDistance / lastEdgeLength
)
let nextEdgeLength = distance(nextPoint, thisPoint)
let nextOffsetDistance = Math.min(nextEdgeLength / 2, radius)
let end = lerp2D(
thisPoint,
nextPoint,
nextOffsetDistance / nextEdgeLength
)
corners.push([start, thisPoint, end])
}
ctx.moveTo(corners[0][0].x, corners[0][0].y)
for (let [start, ctrl, end] of corners) {
ctx.lineTo(start.x, start.y)
ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y)
}
ctx.closePath()
}
Styles for joining of lines such as ctx.lineJoin="round" apply to the stroke operation on paths - which is when their width, color, pattern, dash/dotted and similar line style attributes are taken into account.
Line styles do not apply to filling the interior of a path.
So to affect line styles a stroke operation is needed. In the following adaptation of posted code, I've translated canvas output to see the result without cropping, and stroked the triangle's path but not the rectangles below it:
var ctx = document.querySelector("canvas").getContext('2d');
ctx.scale(5, 5);
ctx.translate( 18, 12);
var x = 18 / 2;
var y = 0;
var triangleWidth = 48;
var triangleHeight = 8;
// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
// stroke the triangle path.
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.strokeStyle = "orange";
ctx.stroke();
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>
I am making a simple HTML5 Canvas drawing app where a circle is placed at a x and y position each time the mouse moves. The (quite common but unsolved) problem is: when the mouse is moved very fast (as in faster than the mouse move events are triggered), you end up with space in between the circles.
I have used Bresenham's line algorithm to somewhat successfully draw circles between the gaps. However, I have encountered another problem: when the color is one of translucency I get an unintentional fade-to-darker effect.
Here's an example:
I don't understand why this is happening. How would you properly interpolate between two points using Bresenham's line algorithm? Or some other algorithm?
Here's my code: http://jsfiddle.net/E5NBs/
var x = null;
var y = null;
var prevX = null;
var prevY = null;
var spacing = 3;
var drawing = false;
var size = 5;
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
function createFlow(x1, y1, x2, y2, callback) {
var dx = x2 - x1;
var sx = 1;
var dy = y2 - y1;
var sy = 1;
var space = 0;
if (dx < 0) {
sx = -1;
dx = -dx;
}
if (dy < 0) {
sy = -1;
dy = -dy;
}
dx = dx << 1;
dy = dy << 1;
if (dy < dx) {
var fraction = dy - (dx >> 1);
while (x1 != x2) {
if (fraction >= 0) {
y1 += sy;
fraction -= dx;
}
fraction += dy;
x1 += sx;
if (space == spacing) {
callback(x1, y1);
space = 0;
} else {
space += 1;
}
}
} else {
var fraction = dx - (dy >> 1);
while (y1 != y2) {
if (fraction >= 0) {
x1 += sx;
fraction -= dy;
}
fraction += dx;
y1 += sy;
if (space == spacing) {
callback(x1, y1);
space = 0;
} else {
space += 1;
}
}
}
callback(x1, y1);
}
context.fillStyle = '#FFFFFF';
context.fillRect(0, 0, 500, 400);
canvas.onmousemove = function(event) {
x = parseInt(this.offsetLeft);
y = parseInt(this.offsetTop);
if (this.offsetParent != null) {
x += parseInt(this.offsetParent.offsetLeft);
y += parseInt(this.offsetParent.offsetTop);
}
if (navigator.appVersion.indexOf('MSIE') != -1) {
x = (event.clientX + document.body.scrollLeft) - x;
y = (event.clientY + document.body.scrollTop) - y;
} else {
x = event.pageX - x;
y = event.pageY - y;
}
context.beginPath();
if (drawing == true) {
if (((x - prevX) >= spacing || (y - prevY) >= spacing) || (prevX - x) >= spacing || (prevY - y) >= spacing) {
createFlow(x, y, prevX, prevY, function(x, y) {
context.fillStyle = 'rgba(0, 0, 0, 0.1)';
context.arc(x, y, size, 0, 2 * Math.PI, false);
context.fill();
});
prevX = x, prevY = y;
}
} else {
prevX = x, prevY = y;
}
};
canvas.onmousedown = function() {
drawing = true;
};
canvas.onmouseup = function() {
drawing = false;
};
HTML Canvas supports fractional / floating point coordinates, so using an algorithm for integer coordinate based pixel canvas is not necessary and could be considered even counter-productive.
A simple, generic solution would be something along these lines:
when mouse_down:
x = mouse_x
y = mouse_y
draw_circle(x, y)
while mouse_down:
when mouse_moved:
xp = mouse_x
yp = mouse_y
if (x != xp or y != yp):
dir = atan2(yp - y, xp - x)
dist = sqrt(pow(xp - x, 2) + pow(yp - y, 2))
while (dist > 0):
x = x + cos(dir)
y = y + sin(dir)
draw_circle(x, y)
dist = dist - 1
That is, whenever the mouse is moved to a location different from the location of the last circle drawn, walk towards the new location with steps having distance one.
If I understand well, you want to have rgba(0, 0, 0, 0.1) on every point. If so, then you can clear the point before drawing new one.
// this is bad way to clear the point, just I don't know canvas so well
context.fillStyle = 'rgba(255, 255, 255, 1)';
context.arc(x, y, size, 0, 2 * Math.PI, false);
context.fill();
context.fillStyle = 'rgba(0, 0, 0, 0.1)';
context.arc(x, y, size, 0, 2 * Math.PI, false);
context.fill();
I've figured it out.
"context.beginPath();" needed to be in the createFlow callback function like so:
createFlow(x, y, prevX, prevY, function(x, y) {
context.beginPath();
context.fillStyle = 'rgba(0, 0, 0, 0.1)';
context.arc(x, y, size, 0, 2 * Math.PI, false);
context.fill();
});