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>
Related
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;
...
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
I'm using Konva library to draw some stuff on HTML5 canvas.
I have given 2 points from user interaction by mouse click:
var A={x:'',y:''};
var B={x:'',y:''};
1) How to draw line line this?
My question is:
1) How to get perpendicular lines on each interval?
2) How to get distance from A to B point?
3) How to get all points on line from A to B?
4) How to get red points?
You have not explained what your line is so I am assuming it is a sin wave (though the image looks like circles stuck together???)
As MBo has given the basics this is just applying it to the wavy line.
// normalize a vector
function normalize(vec){
var length = Math.sqrt(vec.x * vec.x + vec.y * vec.y);
vec.x /= length;
vec.y /= length;
return vec;
}
// creates a wavy line
function wavyLine(start, end, waves, amplitude){
return ({
start,
end,
waves,
amplitude,
update(){
if(this.vec === undefined){
this.vec = {};
this.norm = {};
}
this.vec.x = this.end.x - this.start.x;
this.vec.y = this.end.y - this.start.y;
this.length = Math.sqrt(this.vec.x * this.vec.x + this.vec.y * this.vec.y);
this.norm.x = this.vec.x / this.length;
this.norm.y = this.vec.y / this.length;
return this;
}
}).update();
}
// draws a wavy line
function drawWavyLine(line) {
var x, stepSize, i, y, phase, dist;
ctx.beginPath();
stepSize = ctx.lineWidth;
ctx.moveTo(line.start.x, line.start.y);
for (i = stepSize; i < line.length; i+= stepSize) {
x = line.start.x + line.norm.x * i; // get point i pixels from start
y = line.start.y + line.norm.y * i; // get point i pixels from start
phase = (i / (line.length / line.waves)) * Math.PI * 2; // get the wave phase at this point
dist = Math.sin(phase) * line.amplitude; // get the distance from the line to the point on the wavy curve
x -= line.norm.y * dist;
y += line.norm.x * dist;
ctx.lineTo(x, y);
}
phase = line.waves * Math.PI * 2; // get the wave phase at this point
dist = Math.sin(phase) * line.amplitude; // get the distance from the line to the point on the wavy curve
ctx.lineTo(line.end.x - line.norm.y * dist, line.end.y + line.norm.x * dist);
ctx.stroke();
}
// find the closest point on a wavy line to a point returns the pos on the wave, tangent and point on the linear line
function closestPointOnLine(point,line){
var x = point.x - line.start.x;
var y = point.y - line.start.y;
// get the amount the line vec needs to be scaled so tat point is perpendicular to the line
var l = (line.vec.x * x + line.vec.y * y) / (line.length * line.length);
x = line.vec.x * l; // scale the vec
y = line.vec.y * l;
return pointAtDistance(Math.sqrt(x * x + y * y), line);
}
// find the point at (linear) distance along wavy line and return coordinate, coordinate on wave, and tangent
function pointAtDistance(distance,line){
var lenScale = line.length / line.waves; // scales the length into radians
var phase = distance * Math.PI * 2 / lenScale; // get the wave phase at this point
var dist = Math.sin(phase) * line.amplitude; // get the distance from the line to the point on the wavy curve
var slope = Math.cos(phase ) * Math.PI * 2 * line.amplitude / lenScale; // derivitive of sin(a*x) is -a*cos(a*x)
// transform tangent (slope) into a vector along the line. This vector is not a unit vector so normalize it
var tangent = normalize({
x : line.norm.x - line.norm.y * slope,
y : line.norm.y + line.norm.x * slope
});
// move from the line start to the point on the linear line at distance
var linear = {
x : line.start.x + line.norm.x * distance,
y : line.start.y + line.norm.y * distance
}
// move out perpendicular to the wavy part
return {
x : linear.x - line.norm.y * dist,
y : linear.y + line.norm.x * dist,
tangent,linear
};
}
// create a wavy line
var wLine = wavyLine({x:10,y:100},{x:300,y:100},3,50);
// draw the wavy line and show some points on it
function display(timer){
globalTime = timer;
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
var radius = Math.max(ch,cw);
// set up the wavy line
wLine.waves = Math.sin(timer / 10000) * 6;
wLine.start.x = Math.cos(timer / 50000) * radius + cw;
wLine.start.y = Math.sin(timer / 50000) * radius + ch;
wLine.end.x = -Math.cos(timer / 50000) * radius + cw;
wLine.end.y = -Math.sin(timer / 50000) * radius + ch ;
wLine.update();
// draw the linear line
ctx.lineWidth = 0.5;
ctx.strokeStyle = "blue";
ctx.beginPath();
ctx.moveTo(wLine.start.x, wLine.start.y);
ctx.lineTo(wLine.end.x, wLine.end.y);
ctx.stroke();
// draw the wavy line
ctx.lineWidth = 2;
ctx.strokeStyle = "black";
drawWavyLine(wLine);
// find point nearest mouse
var p = closestPointOnLine(mouse,wLine);
ctx.lineWidth = 1;
ctx.strokeStyle = "red";
ctx.beginPath();
ctx.arc(p.x,p.y,5,0,Math.PI * 2);
ctx.moveTo(p.x + p.tangent.x * 20,p.y + p.tangent.y * 20);
ctx.lineTo(p.x - p.tangent.y * 10,p.y + p.tangent.x * 10);
ctx.lineTo(p.x + p.tangent.y * 10,p.y - p.tangent.x * 10);
ctx.closePath();
ctx.stroke();
// find points at equal distance along line
ctx.lineWidth = 1;
ctx.strokeStyle = "blue";
ctx.beginPath();
for(var i = 0; i < w; i += w / 10){
var p = pointAtDistance(i,wLine);
ctx.moveTo(p.x + 5,p.y);
ctx.arc(p.x,p.y,5,0,Math.PI * 2);
ctx.moveTo(p.x,p.y);
ctx.lineTo(p.linear.x,p.linear.y);
ctx.moveTo(p.x + p.tangent.x * 40, p.y + p.tangent.y * 40);
ctx.lineTo(p.x - p.tangent.x * 40, p.y - p.tangent.y * 40);
}
ctx.stroke();
}
/******************************************************************************
The code from here down is generic full page mouse and canvas boiler plate
code. As I do many examples which all require the same mouse and canvas
functionality I have created this code to keep a consistent interface. The
Code may or may not be part of the answer.
This code may or may not have ES6 only sections so will require a transpiler
such as babel.js to run on legacy browsers.
*****************************************************************************/
// V2.0 ES6 version for Stackoverflow and Groover QuickRun
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0;
// You can declare onResize (Note the capital R) as a callback that is also
// called once at start up. Warning on first call canvas may not be at full
// size.
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var resizeTimeoutHandle;
var firstRun = true;
function createCanvas () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
function resizeCanvas () {
if (canvas === undefined) { canvas = createCanvas() }
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals() }
if (typeof onResize === "function") {
clearTimeout(resizeTimeoutHandle);
if (firstRun) { onResize() }
else { resizeTimeoutHandle = setTimeout(onResize, RESIZE_DEBOUNCE_TIME) }
firstRun = false;
}
}
function setGlobals () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
var m; // alias for mouse
var mouse = {
x : 0, y : 0, // mouse position and wheel
buttonRaw : 0,
buttonOnMasks : [0b1, 0b10, 0b100], // mouse button on masks
buttonOffMasks : [0b110, 0b101, 0b011], // mouse button off masks
bounds : null,
eventNames : "mousemove,mousedown,mouseup".split(","),
event(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
if (t === "mousedown") { m.buttonRaw |= m.buttonOnMasks[e.which - 1] }
else if (t === "mouseup") { m.buttonRaw &= m.buttonOffMasks[e.which - 1] }
},
start(element) {
m.element = element === undefined ? document : element;
m.eventNames.forEach(name => document.addEventListener(name, mouse.event) );
},
}
m = mouse;
return mouse;
})();
function update(timer) { // Main update loop
globalTime = timer;
display(timer); // call demo code
requestAnimationFrame(update);
}
setTimeout(function(){
canvas = createCanvas();
mouse.start(canvas);
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();
We have points A and B. Difference vector
D.X = B.X - A.X
D.Y = B.Y - A.Y
Length = Sqrt(D.X * D.X + D.Y * D.Y)
normalized (unit) vector
uD.X = D.X / Length
uD.Y = D.Y / Length
perpendicular unit vector
P.X = - uD.Y
P.Y = uD.X
some red point:
R.X = A.X + uD.X * Dist + P.X * SideDist * SideSign
R.Y = A.Y + uD.Y * Dist + P.Y * SideDist * SideSign
where Dist is in range 0..Length
Dist = i / N * Length for N equidistant points
SideSign is +/- 1 for left and right side
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>
I'm trying to draw a smooth curved arc between two points in canvas, I have set up the points as sutch note these are dynamic and can change.
var p1 = {
x=100, y=100
}
var p2 = {
x=255, y=255
}
The curve would look something like this
Here my started code, I can't get my head around the math/logic of this function:
function curveA2B(a,b){
var mindpoint = {
x: (a.x+b.x)/2,
y: (a.y+b.y)/2,
d: Math.sqrt(Math.pow(b.x-a.x,2) + Math.pow(b.y-a.y,2))
};
context.beginPath();
context.arc(
a.x,
a.y,
mindpoint.d/2,
1.5*Math.PI,
0,
false
);
context.arc(
b.x,
b.y,
mindpoint.d/2,
1*Math.PI,
0.5*Math.PI,
true
);
context.context.stroke();
}
The dynamic examples is here: http://jsfiddle.net/CezarisLT/JDdjp/6/
I created a function that would be easily modifiable to many needs called plot_curve that gives you an idea of the breakdown of your problem.
A quick DEMO: http://jsfiddle.net/LVFat/
function plot_curve(x,y,xx,yy, target,color)
{
var startX=x;
var startY=y;
var endX=xx;
var endY=yy;
var diff_x = xx - x;
var diff_y = yy - y;
var bezierX=x; // x1
var bezierY=yy; // y2
console.log("bx:"+bezierX);
console.log("by:"+bezierY);
var cx,cy, t;
for(t=0.0; t<=1; t+=0.01)
{
cx = Math.round( (1-t)*(1-t)*startX + 2*(1-t) * t * bezierX + t*t*endX);
cy = Math.round( (1-t)*(1-t)*startY + 2*(1-t) * t * bezierY + t*t*endY);
// change this part to whatever you are trying to manipulate to the curve
plot_pixel( Math.round(cx), Math.round(cy), target, color);
}
}
example... (works with a divCanvas function I made.. check out jsfiddle link...)
plot_curve(25,25,5,5, ".divCanvas","blue");
if you just want the coords for the curve between the two points, try this:
function plot_curve(x,y,xx,yy)
{
// returns an array of x,y coordinates to graph a perfect curve between 2 points.
var startX=x;
var startY=y;
var endX=xx;
var endY=yy;
var diff_x = xx - x;
var diff_y = yy - y;
var xy = [];
var xy_count = -1;
var bezierX=x; // x1
var bezierY=yy; // y2
var t;
for(t=0.0; t<=1; t+=0.01)
{
xy_count++;
xy[xy_count] = {};
xy[xy_count].x = Math.round( (1-t)*(1-t)*startX + 2*(1-t) * t * bezierX + t*t*endX);
xy[xy_count].y = Math.round( (1-t)*(1-t)*startY + 2*(1-t) * t * bezierY + t*t*endY);
}
return xy; // returns array of coordinates
}
You can use the mid of the two points as two radius settings for the x and y axis.
The following example is simplified but it shows one approach to create smooth curves inside the boxes as in your example.
The boxes will always scale so that the curves goes through the mid point between the two points (alter the end point for example).
DEMO
/// set up some values
var ctx = demo.getContext('2d'),
p1 = {x:100, y:100}, /// point 1
p2 = {x:355, y:255}, /// point 2
mx = (p2.x - p1.x) * 0.5, /// mid-point between point 1 and 2
my = (p2.y - p1.y) * 0.5,
c1 = {x: p1.x, y: p1.y + my}, /// create center point objects
c2 = {x: p2.x, y: p2.y - my},
steps = 0.05; /// curve resolution
/// mark the points and the boxes which represent the center of those
ctx.fillStyle = '#ff6e6e';
ctx.fillRect(p1.x, p1.y, mx, my);
ctx.fillStyle = '#6e93ff';
ctx.fillRect(p1.x + mx, p1.y + my, mx, my);
Then we render the quarter ellipse for each "box":
/// render the smooth curves using 1/4 ellipses
ctx.beginPath();
for(var isFirst = true, /// first point is moveTo, rest lineTo
angle = 1.5 * Math.PI, /// start angle in radians
goal = 2 * Math.PI, /// goal angle
x, y; angle < goal; angle += steps) {
/// calculate x and y using cos/sin
x = c1.x + mx * Math.cos(angle);
y = c1.y + my * Math.sin(angle);
/// move or draw line
(isFirst) ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
isFirst = false;
}
/// second box
for(var isFirst = true,
angle = Math.PI,
goal = 0.5 * Math.PI,
x, y;angle > goal; angle -= steps) {
x = c2.x + mx * Math.cos(angle);
y = c2.y + my * Math.sin(angle);
(isFirst) ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
isFirst = false;
}
ctx.stroke();
I'll leave it to you to put this into re-usable functions. Hope this helps!
If this doesn't cut it I would recommend you to take a look at my cardinal spline implementation.