Calculating a concentric arc in canvas - javascript

I'm trying to calculate a 2 concentric arcs (cubic bezier) from a given arc (quadratic bezier). I figured I could calculate control points for the cubic at 1/3 and 2/3 but it doesn't quite match up.
var u = 1 / 3; // fraction of curve where Px1 and Py1 are
var v = 2 / 3; // fraction of curve where Px2 and Py2 are
//Calculate control points (Cx1, Cy1, Cx2, Cy2)
var a = 3 * (1 - u) * (1 - u) * u;
var b = 3 * (1 - u) * u * u;
var c = 3 * (1 - v) * (1 - v) * v;
var d = 3 * (1 - v) * v * v;
var det = a * d - b * c;
var Qx1 = Px1 - ((1 - u) * (1 - u) * (1 - u) * Px0 + u * u * u * Px3);
var Qy1 = Py1 - ((1 - u) * (1 - u) * (1 - u) * Py0 + u * u * u * Py3);
var Qx2 = Px2 - ((1 - v) * (1 - v) * (1 - v) * Px0 + v * v * v * Px3);
var Qy2 = Py2 - ((1 - v) * (1 - v) * (1 - v) * Py0 + v * v * v * Py3);
var Cx1 = (d * Qx1 - b * Qx2) / det;
var Cy1 = (d * Qy1 - b * Qy2) / det;
var Cx2 = ((-c) * Qx1 + a * Qx2) / det;
var Cy2 = ((-c) * Qy1 + a * Qy2) / det;
ctx.beginPath();
ctx.moveTo(Px0, Py0);
ctx.bezierCurveTo(Cx1, Cy1, Cx2, Cy2, Px3, Py3);
ctx.strokeStyle = "#0000FF";
ctx.stroke();
Are the control points also dependent on the radius of the arc or something entirely different? Is a cubic bezier even a good option for drawing a concentric arc? Quadratic bezier definitely does not work and cubic definitely got me closer to what I need.
Here is the link:
http://codepen.io/davidreed0/full/zGqPxQ/
Use the position slider to move the ellipse.

The question as it stands is a bit unclear about requirements. Here is in any case an approach that does not require much calculations, but takes advantage of draw operations to visualize about the same as shown in the codepen.
The main steps are:
At an off-screen canvas:
Define a line thickness with radius set to the green area
Define round caps for the line
Draw the Bezier line with solid color
Draw the result into main canvas with various offsets relative to the thickness of the blue line.
Clear the center and you will have the blue outline
Implement a manual Bezier so you can draw the green arc/ellipse at any point within that shape
Radius/diameter can be expanded. If you need variable radius you can just use the Bezier formula to plot a series of blue arcs on top of each other instead.
Proof-of-concept
This will show the process step-by-step.
Step 1
On an off-screen canvas (shown on-screen here for demo, we'll switch in the next step):
var c = document.querySelector("canvas"),
ctx = c.getContext("2d"),
dia = 90; // diameter of graphics
ctx.strokeStyle = "blue"; // color
ctx.lineWidth = dia; // line-width = dia
ctx.lineCap = "round"; // round caps
// draw bezier (quadratic, one control point)
ctx.moveTo(dia, dia);
ctx.quadraticCurveTo(300, 230, c.width - dia, dia);
ctx.stroke();
<canvas width=600 height=300></canvas>
Done. We now have the main shape. Adjust points as needed.
Step 2
As we now have the main shape we will create the outline using this shape:
Draw this to main canvas offset it circle (f.ex. 8 position around main area)
Knock out the center using comp. mode "destination-out" to leave only the outline
var c = document.querySelector("canvas"),
co = document.createElement("canvas"),
ctx = c.getContext("2d"),
ctxo = co.getContext("2d"),
dia = 90;
co.width = c.width;
co.height = c.height;
ctxo.strokeStyle = "blue";
ctxo.lineWidth = dia;
ctxo.lineCap = "round";
// draw bezier (quadratic here, one control point)
ctxo.moveTo(dia, dia);
ctxo.quadraticCurveTo(300, 230, c.width - dia, dia);
ctxo.stroke();
// draw multiple times to main canvas
var thickness = 1, angle = 0, step = Math.PI * 0.25;
for(; angle < Math.PI * 2; angle += step) {
var x = thickness * Math.cos(angle),
y = thickness * Math.sin(angle);
ctx.drawImage(co, x, y);
}
// punch out center
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(co, 0, 0);
<canvas width=600 height=300></canvas>
Step 3
Plot the green circle using a custom implementation of the canvas. First we back up a copy of the resulting blue outline so we can redraw it on top of the free circle. We can reuse out off-line canvas for that, just clear it and draw back the result (reset transforms):
The only calculation we need from here is for the quadratic Bezier where we supply t in the range [0, 1] to get a point:
function getQuadraticPoint(z0x, z0y, cx, cy, z1x, z1y, t) {
var t1 = (1 - t), // (1 - t)
t12 = t1 * t1, // (1 - t) ^ 2
t2 = t * t, // t ^ 2
t21tt = 2 * t1 * t; // 2(1-t)t
return {
x: t12 * z0x + t21tt * cx + t2 * z1x,
y: t12 * z0y + t21tt * cy + t2 * z1y
}
}
The result will be (using values closer to original codepen):
var c = document.querySelector("canvas"),
co = document.createElement("canvas"),
ctx = c.getContext("2d"),
ctxo = co.getContext("2d"),
radius = 150,
dia = radius * 2;
co.width = c.width;
co.height = c.height;
ctxo.translate(2,2); // to avoid clipping of edges in this demo
ctxo.strokeStyle = "blue";
ctxo.lineWidth = dia;
ctxo.lineCap = "round";
// draw bezier (quadratic here, one control point)
ctxo.moveTo(radius, radius);
ctxo.quadraticCurveTo(300, 230, c.width - radius - 6, radius);
ctxo.stroke();
// draw multiple times to main canvas
var thickness = 1, angle = 0, step = Math.PI * 0.25;
for(; angle < Math.PI * 2; angle += step) {
var x = thickness * Math.cos(angle),
y = thickness * Math.sin(angle);
ctx.drawImage(co, x, y);
}
// punch out center
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(co, 0, 0);
// back-up result by reusing off-screen canvas
ctxo.clearRect(0, 0, co.width, co.height);
ctxo.drawImage(c, 0, 0);
// Step 3: draw the green circle at any point
ctx.globalCompositeOperation = "source-over"; // normal comp. mode
ctx.fillStyle = "#9f9";
ctx.strokeStyle = "#090";
var t = 0, dlt = 0.01;
(function loop(){
ctx.clearRect(0, 0, c.width, c.height);
t += dlt;
// calc position based on t [0, 1] and the same points as for the blue
var pos = getQuadraticPoint(radius, radius, 300, 230, c.width - radius, radius, t);
// draw the arc
ctx.beginPath();
ctx.arc(pos.x + 2, pos.y + 2, radius, 0, 2*Math.PI);
ctx.fill();
// draw center line
ctx.beginPath();
ctx.moveTo(radius, radius);
ctx.quadraticCurveTo(300, 230, c.width - radius - 6, radius);
ctx.stroke();
// draw blue outline on top
ctx.drawImage(co, 0, 0);
if (t <0 || t >= 1) dlt = -dlt; // ping-pong for demo
requestAnimationFrame(loop);
})();
// formula for quadr. curve is: B(t) = (1-t)^2 * Z0 + 2(1-t)t * C + t^2 * Z1
function getQuadraticPoint(z0x, z0y, cx, cy, z1x, z1y, t) {
var t1 = (1 - t), // (1 - t)
t12 = t1 * t1, // (1 - t) ^ 2
t2 = t * t, // t ^ 2
t21tt = 2 * t1 * t; // 2(1-t)t
return {
x: t12 * z0x + t21tt * cx + t2 * z1x,
y: t12 * z0y + t21tt * cy + t2 * z1y
}
}
<canvas width=600 height=600></canvas>
Example using non 1:1 axis by scale:
var c = document.querySelector("canvas"),
co = document.createElement("canvas"),
ctx = c.getContext("2d"),
ctxo = co.getContext("2d"),
radius = 150,
dia = radius * 2;
co.width = c.width;
co.height = c.height;
ctxo.translate(2,2); // to avoid clipping of edges in this demo
ctxo.strokeStyle = "blue";
ctxo.lineWidth = dia;
ctxo.lineCap = "round";
// draw bezier (quadratic here, one control point)
ctxo.moveTo(radius, radius);
ctxo.quadraticCurveTo(300, 230, c.width - radius - 6, radius);
ctxo.stroke();
// draw multiple times to main canvas
var thickness = 2, angle = 0, step = Math.PI * 0.25;
for(; angle < Math.PI * 2; angle += step) {
var x = thickness * Math.cos(angle),
y = thickness * Math.sin(angle);
ctx.drawImage(co, x, y);
}
// punch out center
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(co, 0, 0);
// back-up result by reusing off-screen canvas
ctxo.setTransform(1,0,0,1,0,0); // remove scale
ctxo.clearRect(0, 0, co.width, co.height);
ctxo.drawImage(c, 0, 0);
// Step 3: draw the green circle at any point
ctx.globalCompositeOperation = "source-over"; // normal comp. mode
ctx.fillStyle = "#9f9";
ctx.strokeStyle = "#090";
ctx.scale(1, 0.4); // create ellipse
var t = 0, dlt = 0.01;
(function loop(){
ctx.clearRect(0, 0, c.width, c.height * 1 / 0.4);
t += dlt;
// calc position based on t [0, 1] and the same points as for the blue
var pos = getQuadraticPoint(radius, radius, 300, 230, c.width - radius, radius, t);
// draw the arc
ctx.beginPath();
ctx.arc(pos.x + 2, pos.y + 2, radius, 0, 2*Math.PI);
ctx.fill();
// draw center line
ctx.beginPath();
ctx.moveTo(radius, radius);
ctx.quadraticCurveTo(300, 230, c.width - radius - 6, radius);
ctx.stroke();
// draw blue outline on top
ctx.drawImage(co, 0, 0);
if (t <0 || t >= 1) dlt = -dlt; // ping-pong for demo
requestAnimationFrame(loop);
})();
// formula for quadr. curve is: B(t) = (1-t)^2 * Z0 + 2(1-t)t * C + t^2 * Z1
function getQuadraticPoint(z0x, z0y, cx, cy, z1x, z1y, t) {
var t1 = (1 - t), // (1 - t)
t12 = t1 * t1, // (1 - t) ^ 2
t2 = t * t, // t ^ 2
t21tt = 2 * t1 * t; // 2(1-t)t
return {
x: t12 * z0x + t21tt * cx + t2 * z1x,
y: t12 * z0y + t21tt * cy + t2 * z1y
}
}
<canvas width=600 height=600></canvas>

Related

HTML5 Canvas rotate gradient around centre with best fit

I want to make a gradient that covers the whole canvas whatever the angle of it.
So I used a method found on a Stack Overflow post which is finally incorrect. The solution is almost right but, in fact, the canvas is not totally covered by the gradient.
It is this answer: https://stackoverflow.com/a/45628098/5594331
(You have to look at the last point named "Example of best fit.")
In my code example below, the yellow part should not be visible because it should be covered by the black and white gradient. This is mostly the code written in Blindman67's answer with some adjustments to highlight the problem.
I have drawn in green the control points of the gradient. With the right calculations, these should be stretched to the edges of the canvas at any angle.
var ctx = canvas.getContext("2d");
var w = canvas.width;
var h = canvas.height;
function bestFitGradient(angle){
var dist = Math.sqrt(w * w + h * h) / 2; // get the diagonal length
var diagAngle = Math.asin((h / 2) / dist); // get the diagonal angle
// Do the symmetry on the angle (move to first quad
var a1 = ((angle % (Math.PI *2))+ Math.PI*4) % (Math.PI * 2);
if(a1 > Math.PI){ a1 -= Math.PI }
if(a1 > Math.PI / 2 && a1 <= Math.PI){ a1 = (Math.PI / 2) - (a1 - (Math.PI / 2)) }
// get angles from center to edges for along and right of gradient
var ang1 = Math.PI/2 - diagAngle - Math.abs(a1);
var ang2 = Math.abs(diagAngle - Math.abs(a1));
// get distance from center to horizontal and vertical edges
var dist1 = Math.cos(ang1) * h;
var dist2 = Math.cos(ang2) * w;
// get the max distance
var scale = Math.max(dist2, dist1) / 2;
// get the vector to the start and end of gradient
var dx = Math.cos(angle) * scale;
var dy = Math.sin(angle) * scale;
var x0 = w / 2 + dx;
var y0 = h / 2 + dy;
var x1 = w / 2 - dx;
var y1 = h / 2 - dy;
// create the gradient
const g = ctx.createLinearGradient(x0, y0, x1, y1);
// add colours
g.addColorStop(0, "yellow");
g.addColorStop(0, "white");
g.addColorStop(.5, "black");
g.addColorStop(1, "white");
g.addColorStop(1, "yellow");
return {
g: g,
x0: x0,
y0: y0,
x1: x1,
y1: y1
};
}
function update(timer){
var r = bestFitGradient(timer / 1000);
// draw gradient
ctx.fillStyle = r.g;
ctx.fillRect(0,0,w,h);
// draw points
ctx.lineWidth = 3;
ctx.fillStyle = '#00FF00';
ctx.strokeStyle = '#FF0000';
ctx.beginPath();
ctx.arc(r.x0, r.y0, 5, 0, 2 * Math.PI, false);
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.arc(r.x1, r.y1, 5, 0, 2 * Math.PI, false);
ctx.stroke();
ctx.fill();
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas {
border : 2px solid red;
}
<canvas id="canvas" width="300" height="200"></canvas>
In this fiddle there is a function that calculates the distance between a rotated line and a point:
function distanceToPoint(px, py, angle) {
const cx = width / 2;
const cy = height / 2;
return Math.abs((Math.cos(angle) * (px - cx)) - (Math.sin(angle) * (py - cy)));
}
Which is then used to find the maximum distance between the line and the corner points (only two points are considered, because the distances to the other two points are mirrored):
const dist = Math.max(
distanceToPoint(0, 0, angle),
distanceToPoint(0, height, angle)
);
Which can be used to calculate offset points for the end of the gradient:
const ox = Math.cos(angle) * dist;
const oy = Math.sin(angle) * dist;
const gradient = context.createLinearGradient(
width / 2 + ox,
height / 2 + oy,
width / 2 - ox,
height / 2 - oy
)

Rotate a line in 3rd dimension on HTML5 canvas

I can easily draw/rotate a line of given length around z-axis, y-axis or x-axis.
const ctx = document.getElementById("drawing").getContext("2d");
ctx.scale(1, -1); ctx.translate(0, -ctx.canvas.height); // flip canvas
const length = 50;
let t = 0;
//x = r sin(q) cos(f)
//y = r sin(q) sin(f)
//z = r cos(q)
function rotate_around_zaxis() {
const x1=50; const y1=50;
const line_angle = 20 * Math.PI/180;
const angle = 0;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x1 + length * Math.sin(line_angle) * Math.cos(angle + t),
y1 + length * Math.sin(line_angle) * Math.sin(angle + t));
ctx.stroke();
}
function rotate_around_yaxis() {
const x1=150; const y1=50;
const line_angle = 20 * Math.PI/180;
const angle = 0;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x1 + length * Math.sin(line_angle) * Math.cos(angle + t),
y1 + length /*Math.sin(angle + t)*/ * Math.cos(line_angle) );
ctx.stroke();
}
function rotate_around_xaxis() {
const x1=250; const y1=50;
const line_angle = 20 * Math.PI/180;
const angle = 0;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x1 + length /**Math.sin(angle + t)*/ * Math.cos(line_angle),
y1 + length * Math.sin(line_angle) * Math.sin(angle + t));
ctx.stroke();
}
function line(x1, y1, x2, y2) {
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
}
function animate() {
ctx.clearRect(0,0,300,100);
line(0, 50, 100, 50);line(50, 0, 50, 100);rotate_around_zaxis();
line(105, 50, 200, 50);line(150, 0, 150, 100);rotate_around_yaxis();
line(205, 50, 300, 50);line(250, 0, 250, 100);rotate_around_xaxis();
t+=Math.PI/180;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
<canvas id="drawing" width=300 height=100></canvas>
However I can only do this around straight up/down y-axis degree or straight x-axis. I can not figure out rotation around an arbitrary line in space. In other words I don't know how to move it to any point in 3d space between x,y and z/.
I couldn't grasp rotation matrices. Rotation calculation on many places is given like this.
x' = x * cos(angle) - y * sin(angle);
y' = x * sin(angle) + y * cos(angle);
I don't understand where this equation fits in what I am trying to do.
I want to be able to rotate the line in cone like shape around any axis. How do I achieve this?
Well, theoretically, if the first example works, then to get the second one, you can apply the first one, and then just translate everything -60°
let newX2 = Math.cos(-60 / Math.PI/180) * x2
let newY2 = Math.sin(-60 / Math.PI/180) * y2
same for x1 and y1

How to draw triangle pointers inside of circle

I realize this is a simple Trigonometry question, but my high school is failing me right now.
Given an angle, that I have converted into radians to get the first point. How do I figure the next two points of the triangle to draw on the canvas, so as to make a small triangle always point outwards to the circle. So lets say Ive drawn a circle of a given radius already. Now I want a function to plot a triangle that sits on the edge of the circle inside of it, that points outwards no matter the angle. (follows the edge, so to speak)
function drawPointerTriangle(ctx, angle){
var radians = angle * (Math.PI/180)
var startX = this.radius + this.radius/1.34 * Math.cos(radians)
var startY = this.radius - this.radius/1.34 * Math.sin(radians)
// This gives me my starting point on the outer edge of the circle, plotted at the angle I need
ctx.moveTo(startX, startY);
// HOW DO I THEN CALCULATE x1,y1 and x2, y2. So that no matter what angle I enter into this function, the arrow/triangle always points outwards to the circle.
ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
}
Example
You don't say what type of triangle you want to draw so I suppose that it is an equilateral triangle.
Take a look at this image (credit here)
I will call 3 points p1, p2, p3 from top right to bottom right, counterclockwise.
You can easily calculate the coordinate of three points of the triangle in the coordinate system with the origin is coincident with the triangle's centroid.
Given a point belongs to the edge of the circle and the point p1 that we just calculated, we can calculate parameters of the translation from our main coordinate system to the triangle's coordinate system. Then, we just have to translate the coordinate of two other points back to our main coordinate system. That is (x1,y1) and (x2,y2).
You can take a look at the demo below that is based on your code.
const w = 300;
const h = 300;
function calculateTrianglePoints(angle, width) {
let r = width / Math.sqrt(3);
let firstPoint = [
r * Math.cos(angle),
r * Math.sin(angle),
]
let secondPoint = [
r * Math.cos(angle + 2 * Math.PI / 3),
r * Math.sin(angle + 2 * Math.PI / 3),
]
let thirdPoint = [
r * Math.cos(angle + 4 * Math.PI / 3),
r * Math.sin(angle + 4 * Math.PI / 3),
]
return [firstPoint, secondPoint, thirdPoint]
}
const radius = 100
const triangleWidth = 20;
function drawPointerTriangle(ctx, angle) {
var radians = angle * (Math.PI / 180)
var startX = radius * Math.cos(radians)
var startY = radius * Math.sin(radians)
var [pt0, pt1, pt2] = calculateTrianglePoints(radians, triangleWidth);
var delta = [
startX - pt0[0],
startY - pt0[1],
]
pt1[0] = pt1[0] + delta[0]
pt1[1] = pt1[1] + delta[1]
pt2[0] = pt2[0] + delta[0]
pt2[1] = pt2[1] + delta[1]
ctx.beginPath();
// This gives me my starting point on the outer edge of the circle, plotted at the angle I need
ctx.moveTo(startX, startY);
[x1, y1] = pt1;
[x2, y2] = pt2;
// HOW DO I THEN CALCULATE x1,y1 and x2, y2. So that no matter what angle I enter into this function, the arrow/triangle always points outwards to the circle.
ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.closePath();
ctx.fillStyle = '#FF0000';
ctx.fill();
}
function drawCircle(ctx, radius) {
ctx.beginPath();
ctx.arc(0, 0, radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.fillStyle = '#000';
ctx.fill();
}
function clear(ctx) {
ctx.fillStyle = '#fff';
ctx.fillRect(-w / 2, -h / 2, w, h);
}
function normalizeAngle(pointCoordinate, angle) {
const [x, y] = pointCoordinate;
if (x > 0 && y > 0) return angle;
else if (x > 0 && y < 0) return 360 + angle;
else if (x < 0 && y < 0) return 180 - angle;
else if (x < 0 && y > 0) return 180 - angle;
}
function getAngleFromPoint(point) {
const [x, y] = point;
if (x == 0 && y == 0) return 0;
else if (x == 0) return 90 * (y > 0 ? 1 : -1);
else if (y == 0) return 180 * (x >= 0 ? 0: 1);
const radians = Math.asin(y / Math.sqrt(
x ** 2 + y ** 2
))
return normalizeAngle(point, radians / (Math.PI / 180))
}
document.addEventListener('DOMContentLoaded', function() {
const canvas = document.querySelector('canvas');
const angleText = document.querySelector('.angle');
const ctx = canvas.getContext('2d');
ctx.translate(w / 2, h / 2);
drawCircle(ctx, radius);
drawPointerTriangle(ctx, 0);
canvas.addEventListener('mousemove', _.throttle(function(ev) {
let mouseCoordinate = [
ev.clientX - w / 2,
ev.clientY - h / 2
]
let degAngle = getAngleFromPoint(mouseCoordinate)
clear(ctx);
drawCircle(ctx, radius);
drawPointerTriangle(ctx, degAngle)
angleText.innerText = Math.floor((360 - degAngle)*100)/100;
}, 15))
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>
<canvas width=300 height=300></canvas>
<div class="angle">0</div>
reduce the radius, change the angle and call again cos/sin:
function drawPointerTriangle(ctx, angle)
{
var radians = angle * (Math.PI/180);
var radius = this.radius/1.34;
var startX = this.center.x + radius * Math.cos(radians);
var startY = this.center.y + radius * Math.sin(radians);
ctx.moveTo(startX, startY);
radius *= 0.9;
radians += 0.1;
var x1 = this.center.x + radius * Math.cos(radians);
var y1 = this.center.y + radius * Math.sin(radians);
radians -= 0.2;
var x1 = this.center.x + radius * Math.cos(radians);
var y1 = this.center.y + radius * Math.sin(radians);
ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(startX, startY);
}
the resulting triangle's size is proportional to the size of the circle.
in case you need an equilateral, fixed size triangle, use this:
//get h by pythagoras
h = sqrt( a^2 - (a/2)^2 );)
//get phi using arcustangens:
phi = atan( a/2, radius-h );
//reduced radius h by pythagoras:
radius = sqrt( (radius-h)^2 + (a/2)^2 );
radians += phi;
...
radians -= 2*phi;
...

JS move point by an angle on circle

I have program where I click three times, each click creating a point on canvas. I then calculate angle between those three points like this:
function find_angle(A, B, C) {
var AB = Math.sqrt(Math.pow(B.x - A.x, 2) + Math.pow(B.y - A.y, 2));
var BC = Math.sqrt(Math.pow(B.x - C.x, 2) + Math.pow(B.y - C.y, 2));
var AC = Math.sqrt(Math.pow(C.x - A.x, 2) + Math.pow(C.y - A.y, 2));
return Math.acos((BC * BC + AB * AB - AC * AC) / (2 * BC * AB));
}
In example picture above, the calculated angle is 93°. I need to move point 3 by -3° so the points make exactly 90°. I have this function for it:
var angleToCorrect = alpha * (Math.PI / 180) - 90 * (Math.PI / 180);
correct_angle(point2, point3, angleToCorrect)
...
function correct_angle(p2, p3, angle) {
var x = p2.x - p3.x;
var y = p2.y - p3.y;
var r = Math.sqrt(x * x + y * y); //circle radius. origin of the circle is point 2
return {
X: p2.x + (Math.cos(angle) * r),
Y: p2.y + (Math.sin(angle) * r)
};
}
Now, this function should return new x and y for point 3 with corrected angle to 90°. Yet the coordinates don't agree with what I expect. Can someone point out what I'm doing wrong?
To calculate the new position it isn't enough to provide just two of the points since the angle is measured between the three.
So inside this function you have to figure out what the current angle of the vector between point 1 and point 2 is. Javascript offers a nifty built-in function for this Math.atan2()
Now that we know the angle (in radians) we need to add the new angle to it. This makes sure we can place point 3 correctly.
function correct_angle(p1, p2, p3, angle)
{
var currentAngle=Math.atan2(p1.y-p2.y, p1.x-p2.x);
currentAngle+=angle;
var x = p2.x - p3.x;
var y = p2.y - p3.y;
var r = Math.sqrt(x * x + y * y);
return {
X: p2.x + (Math.cos(currentAngle) * r),
Y: p2.y + (Math.sin(currentAngle) * r)
};
}
The angle parameter of the function should be the target angle in radians (90 or 1.5707963267949 in your case)
Here's an interactive example:
Point = function(x, y) {
this.x = x;
this.y = y;
}
var pointA = new Point(162, 39);
var pointB = new Point(105, 161);
var pointC = new Point(211, 242);
var context = document.getElementById("canvas").getContext("2d");
function correct() {
var newPoint = correct_angle(pointA, pointB, pointC, 1.5707963267949);
pointC.x = newPoint.X;
pointC.y = newPoint.Y;
draw();
}
function correct_angle(p1, p2, p3, angle) {
var currentAngle = Math.atan2(p1.y - p2.y, p1.x - p2.x);
currentAngle += angle;
var x = p2.x - p3.x;
var y = p2.y - p3.y;
var r = Math.sqrt(x * x + y * y);
return {
X: p2.x + (Math.cos(currentAngle) * r),
Y: p2.y + (Math.sin(currentAngle) * r)
};
}
function draw() {
context.clearRect(0, 0, 400, 300);
context.fillStyle = "red";
context.beginPath();
context.arc(pointA.x, pointA.y, 10, 0, 2 * Math.PI);
context.fill();
context.beginPath();
context.arc(pointB.x, pointB.y, 10, 0, 2 * Math.PI);
context.fill();
context.beginPath();
context.arc(pointC.x, pointC.y, 10, 0, 2 * Math.PI);
context.fill();
}
draw();
<canvas id="canvas" width="400" height="300" style="background-color:#dddddd;"></canvas>
<button onclick="correct()" style="float:left">
correct me
</button>

How to draw parallel edges (arrows) between vertices with canvas?

I'm working on a flow-network visualization with Javascript.
Vertices are represented as circles and edges are represented as arrows.
Here is my Edge class:
function Edge(u, v) {
this.u = u; // start vertex
this.v = v; // end vertex
this.draw = function() {
var x1 = u.x;
var y1 = u.y;
var x2 = v.x;
var y2 = v.y;
context.beginPath();
context.moveTo(x1, y1);
context.lineTo(x2, y2);
context.stroke();
var dx = x1 - x2;
var dy = y1 - y2;
var length = Math.sqrt(dx * dx + dy * dy);
x1 = x1 - Math.round(dx / ((length / (radius))));
y1 = y1 - Math.round(dy / ((length / (radius))));
x2 = x2 + Math.round(dx / ((length / (radius))));
y2 = y2 + Math.round(dy / ((length / (radius))));
// calculate the angle of the edge
var deg = (Math.atan(dy / dx)) * 180.0 / Math.PI;
if (dx < 0) {
deg += 180.0;
}
if (deg < 0) {
deg += 360.0;
}
// calculate the angle for the two triangle points
var deg1 = ((deg + 25 + 90) % 360) * Math.PI * 2 / 360.0;
var deg2 = ((deg + 335 + 90) % 360) * Math.PI * 2 / 360.0;
// calculate the triangle points
var arrowx = [];
var arrowy = [];
arrowx[0] = x2;
arrowy[0] = y2;
arrowx[1] = Math.round(x2 + 12 * Math.sin(deg1));
arrowy[1] = Math.round(y2 - 12 * Math.cos(deg1));
arrowx[2] = Math.round(x2 + 12 * Math.sin(deg2));
arrowy[2] = Math.round(y2 - 12 * Math.cos(deg2));
context.beginPath();
context.moveTo(arrowx[0], arrowy[0]);
context.lineTo(arrowx[1], arrowy[1]);
context.lineTo(arrowx[2], arrowy[2]);
context.closePath();
context.stroke();
context.fillStyle = "black";
context.fill();
};
}
Given the code
var canvas = document.getElementById('canvas'); // canvas element
var context = canvas.getContext("2d");
context.lineWidth = 1;
context.strokeStyle = "black";
var radius = 20; // vertex radius
var u = {
x: 50,
y: 80
};
var v = {
x: 150,
y: 200
};
var e = new Edge(u, v);
e.draw();
The draw() function will draw an edge between two vertices like this:
If we add the code
var k = new Edge(v, u);
k.draw();
We will get:
but I want to draw edges both directions as following:
(sorry for my bad paint skills)
Of course the vertices and the edge directions are not fixed.
A working example (with drawing vertex fucntion) on JSFiddle:
https://jsfiddle.net/Romansko/0fu01oec/18/
Aligning axis to a line.
It can make everything a little easier if you rotate the rendering to align with the line. Once you do that it is then easy to draw above or below the line as that is just in the y direction and along the line is the x direction.
Thus if you have a line
const line = {
p1 : { x : ? , y : ? },
p2 : { x : ? , y : ? },
};
Convert it to a vector and normalise that vector
// as vector from p1 to p2
var nx = line.p2.x - line.p1.x;
var ny = line.p2.y - line.p1.y;
// then get length
const len = Math.sqrt(nx * nx + ny * ny);
// use the length to normalise the vector
nx /= len;
ny /= len;
The normalised vector represents the new x axis we want to render along, and the y axis is at 90 deg to that. We can use setTransform to set both axis and the origin (0,0) point at the start of the line.
ctx.setTransform(
nx, ny, // the x axis
-ny, nx, // the y axis at 90 deg to the x axis
line.p1.x, line.p1.y // the origin (0,0)
)
Now rendering the line and arrow heads is easy as they are axis aligned
ctx.beginPath();
ctx.lineTo(0,0); // start of line
ctx.lineTo(len,0); // end of line
ctx.stroke();
// add the arrow head
ctx.beginPath();
ctx.lineTo(len,0); // tip of arrow
ctx.lineTo(len - 10, 10);
ctx.lineTo(len - 10, -10);
ctx.fill();
To render two lines offset from the center
var offset = 10;
ctx.beginPath();
ctx.lineTo(0,offset); // start of line
ctx.lineTo(len,offset); // end of line
ctx.moveTo(0,-offset); // start of second line
ctx.lineTo(len,-offset); // end of second line
ctx.stroke();
// add the arrow head
ctx.beginPath();
ctx.lineTo(len,offset); // tip of arrow
ctx.lineTo(len - 10, offset+10);
ctx.lineTo(len - 10, offset-10);
ctx.fill();
offset = -10;
// add second arrow head
ctx.beginPath();
ctx.lineTo(0,offset); // tip of arrow
ctx.lineTo(10, offset+10);
ctx.lineTo(10, offset-10);
ctx.fill();
And you can reset the transform with
ctx.setTransform(1,0,0,1,0,0); // restore default transform

Categories