HTML5 Canvas rotate gradient around centre with best fit - javascript

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
)

Related

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;
...

Rotating Rectangles Around Circle Perimeter on Canvas

I'm trying to create a little circular "equalizer" effect using JavaScript and HTML canvas for a little project I'm working on, and it works great, except one little thing. It's just a series of rectangular bars moving in time to an mp3 - nothing overly fancy, but at the moment all the bars point in one direction (i.e. 0 radians, or 90 degrees).
I want each respective rectangle around the edge of the circle to point directly away from the center point, rather than to the right. I have 360 bars, so naturally, each one should be 1 degree more rotated than the previous.
I thought that doing angle = i*Math.PI/180 would fix that, but it doesn't seem to matter what I do with the rotate function - they always end up pointing in weird and wonderful directions, and being translated a million miles from where they were. And I can't see why. Can anyone see where I'm going wrong?
My frame code, for reference, is as follows:
function frames() {
// Clear the canvas and get the mp3 array
window.webkitRequestAnimationFrame(frames);
musicArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(musicArray);
ctx.clearRect(0, 0, canvas.width, canvas.height);
bars = 360;
for (var i = 0; i < bars; i++) {
// Find the rectangle's position on circle edge
distance = 100;
var angle = i * ((Math.PI * 2) / bars);
var x = Math.cos(angle) * distance + (canvas.width / 2);
var y = Math.sin(angle) * distance + (canvas.height / 2);
barWidth = 5;
barHeight = (musicArray[i] / 4);
// Fill with a blue-green gradient
var grd = ctx.createLinearGradient(x, 0, x + 40, 0);
grd.addColorStop(0, "#00CCFF");
grd.addColorStop(1, "#00FF7F");
ctx.fillStyle = grd;
// Rotate the rectangle according to position
// ctx.rotate(i*Math.PI/180); - DOESN'T WORK
// Draw the rectangle
ctx.fillRect(x, y, barHeight, barWidth);
}
For clarity I've removed part of your code. I'm using rotate as you intended. Also I'm using barHeight = (Math.random()* 50); instead your (musicArray[i]/4); because I wanted to have something to show.
Also I've changed your bars to 180. It's very probable that you won't have 360 bars but 32 or 64 or 128 or 256 . . . Now you can change the numbers of bare to one of these numbers to see the result.
I'm drawing everything around the origin of the canvas and translating the context in the center.
I hope it helps.
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
let cw = canvas.width = 400;
let ch = canvas.height = 400;
let bars = 180;
let r = 100;
ctx.translate(cw / 2, ch / 2)
for (var i = 0; i < 360; i += (360 / bars)) {
// Find the rectangle's position on circle edge
var angle = i * ((Math.PI * 2) / bars);
//var x = Math.cos(angle)*r+(canvas.width/2);
//var y = Math.sin(angle)*r+(canvas.height/2);
barWidth = 2 * Math.PI * r / bars;
barHeight = (Math.random() * 50);
ctx.fillStyle = "green";
// Rotate the rectangle according to position
// ctx.rotate(i*Math.PI/180); - DOESN'T WORK
// Draw the rectangle
ctx.save();
ctx.rotate(i * Math.PI / 180);
ctx.fillRect(r, -barWidth / 2, barHeight, barWidth);
//ctx.fillRect(r ,0, barHeight, barWidth);
ctx.restore();
}
canvas {
border: 1px solid
}
<canvas id="c"></canvas>
Here is another solution, I'm preserving your initial trigonometry approach.
But instead of rectangles I used lines, I don't think it makes a difference for you, if what you need is bars moving in time to an mp3 all you need to do is change the var v = Math.random() + 1; to a reading from the Amplitude, and those bars will be dancing.
const canvas = document.getElementById("c");
canvas.width = canvas.height = 170;
const ctx = canvas.getContext("2d");
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.lineWidth = 2;
let r = 40;
let bars = 180;
function draw() {
ctx.clearRect(-100, -100, 200, 200)
for (var i = 0; i < 360; i += (360 / bars)) {
var angle = i * ((Math.PI * 2) / bars);
var x = Math.cos(angle) * r;
var y = Math.sin(angle) * r;
ctx.beginPath();
var v = Math.random() + 1;
ctx.moveTo(x, y);
ctx.lineTo(x * v, y * v)
grd = ctx.createLinearGradient(x, y, x*2, y*2);
grd.addColorStop(0, "blue");
grd.addColorStop(1, "red");
ctx.strokeStyle = grd;
ctx.stroke();
}
}
setInterval(draw, 100)
<canvas id="c"></canvas>

JS calculting width and height for rotated rectangle

am trying to draw rectangles it different angles , initially i know rectangle position data it 0 degree , then sometime i get angle greater than 0 so i have to draw on that angle , for which i then have to rotate points. so currently i can rotate points but do not know to calculate width and height after rotation.
am doing it like ...
// Main rotation function
function rotate(originX, originY,pointX, pointY, angle) {
angle = angle * Math.PI / 180.0;
return {
x: Math.cos(angle) * (pointX-originX) - Math.sin(angle) * (pointY-originY) + originX,
y: Math.sin(angle) * (pointX-originX) + Math.cos(angle) * (pointY-originY) + originY
};
}
// initial Rectangle
ctx.fillStyle = "red";
var x = 200
var y = 200
var w = 80
var h = 20
var angle = 90 ;
ctx.fillRect(x, y, w, h);
// Calculate Center
var cx = (x + (w/2));
var cy = (y + (h/2));
// highlight Center
ctx.fillStyle = "black";
ctx.fillRect(cx,cy, 5, 5);
// Rotate starting x y at angle xxx
var r = rotate(cx,cy,x,y, angle - h );
// highlight roate points
ctx.fillStyle = "yellow";
ctx.fillRect(r.x, r.y, 5, 5);
// rotate Width and Height
var r2 = rotate(cx,cy,x+w,y+h, angle - h );
// highlight roate points
ctx.fillStyle = "green";
ctx.fillRect(r2.x, r2.y, 5, 5);
ctx.save();
so it end i rotated width and height which is ok for single line , but am interested in full rotation of width of height , like it 90 angle old with will become new height and old height will become new width. so any idea how to do it
Fiddle : https://jsfiddle.net/047txgox/
fixed it , hope will help someone
var x = 40
var y = 80
var w = 80
var h = 20
var angle = 120 ;
// Calculate Center
var cx = (x + (w/2));
var cy = (y + (h/2));
context.setTransform(1,0,0,1,0,0)
context.translate(cx, cy);
context.rotate(angle*Math.PI/180);
context.translate(-cx, -cy);
context.rect(x, y, w, h);
context.fillStyle = "#FF00FF";
context.fill();
// highlight Center
context.fillStyle = "black";
context.fillRect(cx,cy, 5, 5);
context.save();
Fiddle : http://jsfiddle.net/vorjcbz7/

Elliptical arc arrow edge d3 forced layout

I am using forced layout to create directed graph .
Its rendered on canvas . My sample example is at http://jsbin.com/vuyapibaqa/1/edit?html,output
Now I am inspired from
https://bl.ocks.org/mattkohl/146d301c0fc20d89d85880df537de7b0#index.html
Few Resources in d3 svg , something similar i am trying to get in canvas.
http://jsfiddle.net/zhanghuancs/a2QpA/
http://bl.ocks.org/mbostock/1153292 https://bl.ocks.org/ramtob/3658a11845a89c4742d62d32afce3160
http://bl.ocks.org/thomasdobber/9b78824119136778052f64a967c070e0
Drawing multiple edges between two nodes with d3.
Want to add elliptical arc connecting edge with arrow . How to achieve this in canvas.
My Code :
<!DOCTYPE html>
<html>
<head>
<title>Sample Graph Rendring Using Canvas</title>
<script src="https://rawgit.com/gka/randomgraph.js/master/randomgraph.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var graph = {}//randomgraph.WattsStrogatz.beta(15, 4, 0.06);
graph.nodes = [{"label":"x"} , {"label":"y"}];
graph.edges = [{source:0,target:1},{source:0,target:1},
{source:1,target:0}]
var canvas = null
var width = window.innerWidth,
height = window.innerHeight;
canvas = d3.select("body").append("canvas").attr("width",width).attr("height",height);
var context = canvas.node().getContext("2d");
force = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.index;
})).force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
force.nodes(graph.nodes);
force.force("link").links(graph.edges).distance(200);
var detachedContainer = document.createElement("custom");
dataContainer = d3.select(detachedContainer);
link = dataContainer.selectAll(".link").data(graph.edges)
.enter().append("line").attr("class", "link")
.style("stroke-width", 2)
node = dataContainer.selectAll(".node").data(graph.nodes)
.enter().append("g");
var circles = node.append("circle")
.classed("circle-class", true)
.attr("class", function (d){ return "node node_" + d.index;})
.attr("r", 5)
.attr("fill", 'red')
.attr("strokeStyle", 'black');
d3.timer(function(){
context.clearRect(0, 0, width, height);
// draw links
link.each(function(d) {
context.strokeStyle = "#ccc";
/***** Elliptical arcs *****/
context.stroke(new Path2D(linkArc(d)));
/***** Elliptical arcs *****/
});
context.lineWidth = 2;
node.each(function(d) {
context.beginPath();
context.moveTo(d.x, d.y);
var r = d3.select(this).select("circle").node().getAttribute('r');
d.x = Math.max(30, Math.min(width - 30, d.x));
d.y = Math.max(30, Math.min(height - 30, d.y));
context.closePath();
context.arc(d.x, d.y, r, 0, 2 * Math.PI);
context.fillStyle = d3.select(this).select("circle").node().getAttribute('fill');
context.strokeStyle = d3.select(this).select("circle").node().getAttribute('strokeStyle');
context.stroke();
context.fill();
context.beginPath();
context.arc(d.x + 15, d.y-20, 5, 0, 2 * Math.PI);
context.fillStyle = "orange";
context.strokeStyle = "orange";
var data = d3.select(this).data();
context.stroke();
context.fill();
context.font = "10px Arial";
context.fillStyle = "black";
context.strokeStyle = "black";
context.fillText(parseInt(data[0].index),d.x + 10, d.y-15);
});
});
circles.transition().duration(5000).attr('r', 20).attr('fill', 'orange');
canvas.node().addEventListener('click',function( event ){
console.log(event)
// Its COMING ANY TIME INSIDE ON CLICK OF CANVAS
});
/***** Elliptical arcs *****/
function linkArc(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
}
/***** Elliptical arcs *****/
</script>
</body>
</html>
Draw arc from circle to circle with arrow heads
The basic problem
The two points need to be random(from anywhere to anywhere) x1,y1 and x2,y2. You will want to control the amount of bending that is invariant to the distance between the points (ie the same amount of bending if the distance between points is 100 pixels or 10 pixels)
Thus inputs are
x1,y1 // as start
x2,y2 // as end
bend // as factor of distance between points
// negative bends up (to right)
// positive bends down (to left of line)
arrowLen // in pixels
arrowWidth // in pixels,
arrowStart // boolean if arrow at start
arrowEnd // boolean if arrow at end.
Basic method steps
Find the mid-point between the two end points.
Get distance between points
Get the normalised vector from start to end.
Rotate norm 90deg
Multiply distance by bend by rotated norm and add to mid point to find mid point on arc
With the 3 points find the radius of a circle that will fit all 3 points.
Use the radius to find the center of the arc
From the center find the direction to the start and end
Use the arrow len to find angular length of arrows now we have the radius
Draw the arc from inside arrows or start / end (depending if arrows shown)
Draw arrow from point with flat side along line from arc center
Additional problems.
I assume you want the lines to be from one circle to the next. Thus you want to specify the circle centers and the radius of the circles. This will require two additional arguments one for the start circle radius and one for the end.
There is also the problem of what to do when the two points are two close (ie they overlap). There is not real solution apart from not to draw the lines and arrows if they don't fit.
The Solution as a Demo
The demo has to circles that change size over time, there are 6 arcs with different bend values of 0.1,0.3, 0.6 and -0.1, -0.3, -0.6. Move the mouse to change end circles position.
The function that does it all is called drawBend and I have put a lot of comments in there, There is also some commented lines that let you change how the arcs change when the distance between start and end changes. If you uncomment one, setting the variable b1 (where you assign to x3,y3 the mid point on the arc) you MUST comment out the other assignments
The solution to finding the arc radius and center is complicated and there is most likely a better solution due to the symmetry. That part will find a circle to fit any 3 points, (if not all on a line) so may have other uses for you.
Update I have found a much better method of finding the arc radius and thus center point. The symmetry provided a very convenient set of similar triangles and and thus I could shorten the function by 9 lines. I have updated the demo.
The arc is draw as a stroke, and the arrowheads as a fill.
Its reasonably quick, but if you plan to draw many 100's in real-time you can optimise by having the arc from and then back share some calcs. The arc from start to end will bend the other way if you swap the start and end, and there are many values that remain unchanged, so you can get two arcs for about a 75% CPU load of drawing 2
const ctx = canvas.getContext("2d");
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
// x1,y1 location of a circle start
// x2,y2 location of the end circle
// bend factor. negative bends up for, positive bends down. If zero the world will end
// aLen is Arrow head length in pixels
// aWidth is arrow head width in pixels
// sArrow boolean if true draw start arrow
// eArrow boolean if true draw end arrow
// startRadius = radius of a circle if start attached to circle
// endRadius = radius of a circle if end attached to circle
function drawBend(x1, y1, x2, y2, bend, aLen, aWidth, sArrow, eArrow, startRadius, endRadius){
var mx, my, dist, nx, ny, x3, y3, cx, cy, radius, vx, vy, a1, a2;
var arrowAng,aa1,aa2,b1;
// find mid point
mx = (x1 + x2) / 2;
my = (y1 + y2) / 2;
// get vector from start to end
nx = x2 - x1;
ny = y2 - y1;
// find dist
dist = Math.sqrt(nx * nx + ny * ny);
// normalise vector
nx /= dist;
ny /= dist;
// The next section has some optional behaviours
// that set the dist from the line mid point to the arc mid point
// You should only use one of the following sets
//-- Uncomment for behaviour of arcs
// This make the lines flatten at distance
//b1 = (bend * 300) / Math.pow(dist,1/4);
//-- Uncomment for behaviour of arcs
// Arc bending amount close to constant
// b1 = bend * dist * 0.5
b1 = bend * dist
// Arc amount bend more at dist
x3 = mx + ny * b1;
y3 = my - nx * b1;
// get the radius
radius = (0.5 * ((x1-x3) * (x1-x3) + (y1-y3) * (y1-y3)) / (b1));
// use radius to get arc center
cx = x3 - ny * radius;
cy = y3 + nx * radius;
// radius needs to be positive for the rest of the code
radius = Math.abs(radius);
// find angle from center to start and end
a1 = Math.atan2(y1 - cy, x1 - cx);
a2 = Math.atan2(y2 - cy, x2 - cx);
// normalise angles
a1 = (a1 + Math.PI * 2) % (Math.PI * 2);
a2 = (a2 + Math.PI * 2) % (Math.PI * 2);
// ensure angles are in correct directions
if (bend < 0) {
if (a1 < a2) { a1 += Math.PI * 2 }
} else {
if (a2 < a1) { a2 += Math.PI * 2 }
}
// convert arrow length to angular len
arrowAng = aLen / radius * Math.sign(bend);
// get angular length of start and end circles and move arc start and ends
a1 += startRadius / radius * Math.sign(bend);
a2 -= endRadius / radius * Math.sign(bend);
aa1 = a1;
aa2 = a2;
// check for too close and no room for arc
if ((bend < 0 && a1 < a2) || (bend > 0 && a2 < a1)) {
return;
}
// is there a start arrow
if (sArrow) { aa1 += arrowAng } // move arc start to inside arrow
// is there an end arrow
if (eArrow) { aa2 -= arrowAng } // move arc end to inside arrow
// check for too close and remove arrows if so
if ((bend < 0 && aa1 < aa2) || (bend > 0 && aa2 < aa1)) {
sArrow = false;
eArrow = false;
aa1 = a1;
aa2 = a2;
}
// draw arc
ctx.beginPath();
ctx.arc(cx, cy, radius, aa1, aa2, bend < 0);
ctx.stroke();
ctx.beginPath();
// draw start arrow if needed
if(sArrow){
ctx.moveTo(
Math.cos(a1) * radius + cx,
Math.sin(a1) * radius + cy
);
ctx.lineTo(
Math.cos(aa1) * (radius + aWidth / 2) + cx,
Math.sin(aa1) * (radius + aWidth / 2) + cy
);
ctx.lineTo(
Math.cos(aa1) * (radius - aWidth / 2) + cx,
Math.sin(aa1) * (radius - aWidth / 2) + cy
);
ctx.closePath();
}
// draw end arrow if needed
if(eArrow){
ctx.moveTo(
Math.cos(a2) * radius + cx,
Math.sin(a2) * radius + cy
);
ctx.lineTo(
Math.cos(aa2) * (radius - aWidth / 2) + cx,
Math.sin(aa2) * (radius - aWidth / 2) + cy
);
ctx.lineTo(
Math.cos(aa2) * (radius + aWidth / 2) + cx,
Math.sin(aa2) * (radius + aWidth / 2) + cy
);
ctx.closePath();
}
ctx.fill();
}
/** SimpleUpdate.js begin **/
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var globalTime = new Date().valueOf(); // global to this
// main update function
function update(timer){
globalTime = timer;
if(w !== innerWidth || h !== innerHeight){ // resize if needed
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
var startRad = (Math.sin(timer / 2000) * 0.5 + 0.5) * 20 + 5;
var endRad = (Math.sin(timer / 7000) * 0.5 + 0.5) * 20 + 5;
ctx.lineWidth = 2;
ctx.fillStyle = "white";
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.arc(cw,ch,startRad,0,Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.arc(mouse.x,mouse.y,endRad,0,Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.lineWidth = 2;
ctx.fillStyle = "black";
ctx.strokeStyle = "black";
drawBend(cw,ch,mouse.x,mouse.y,-0.1,10,10,true,true,startRad + 1,endRad + 1);
drawBend(cw,ch,mouse.x,mouse.y,-0.3,10,10,true,true,startRad + 1,endRad + 1);
drawBend(cw,ch,mouse.x,mouse.y,-0.6,10,10,true,true,startRad + 1,endRad + 1);
drawBend(cw,ch,mouse.x,mouse.y,0.1,10,10,true,true,startRad + 1,endRad + 1);
drawBend(cw,ch,mouse.x,mouse.y,0.3,10,10,true,true,startRad + 1,endRad + 1);
drawBend(cw,ch,mouse.x,mouse.y,0.6,10,10,true,true,startRad + 1,endRad + 1);
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>

Calculating a concentric arc in canvas

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>

Categories