I am looking for a way to draw multiple rectangles around a circle at an angle facing the centre. What i have so far, currently just drawing the rectangles around a circle facing one direction lacking the angle inclination towards the centre -
https://thysultan.com/projects/thyplayer/
What i want is for the rectangles to incline at an angle such that each rectangle is facing the centre of the circle at it's designated position.
How would one do that?
Trigonometry approach
To animate the lines towards the center you can use either transforms or trigonometry math. Personally I find it easier with math in cases like this so here is such an example.
See #markE's answer for an example on how to use transforms (transforms can be easier on the eye and in the code in general).
Some prerequisites:
We know canvas is oriented so that a 0° angle points towards right. This is essential if you want to mark the frequency range somehow.
We need to calculate the length of the line from outer radius towards center (inner radius)
We need to calculate the end points of the line based on the angle.
An easy solution can be to make a function that calculates a single line at a center (cx, cy) at a certain angle (in radians), with t as length, t being in the range [0, 1] (as the FFT floating point buffer bins). We also provide an outer and inner radius to enable limiting the line:
function getLine(cx, cy, angle, t, oRadius, iRadius) {
var radiusDiff = oRadius - iRadius, // calc radius diff to get max length
length = radiusDiff * t; // now we have the line length
return {
x1: oRadius * Math.cos(angle), // x1 point (outer)
y1: oRadius * Math.sin(angle), // y1 point (outer)
x2: (oRadius - length) * Math.cos(angle), // x2 point (inner)
y2: (oRadius - length) * Math.sin(angle) // y2 point (inner)
}
}
All we need to do now is to feed it data from the FFT analyzer.
NB: Since all lines points towards center, you will have a crowded center area. Something to have in mind when determine the line widths and inner radius as well as number of bins to use.
Example demo
For the example I will just use some random data for the "FFT" and plot 64 bins.
// angle - in radians
function getLine(cx, cy, angle, t, oRadius, iRadius) {
var radiusDiff = oRadius - iRadius, // calc radius diff to get max length
length = radiusDiff * t; // now we have the line length
return {
x1: cx + oRadius * Math.cos(angle), // x1 point (outer)
y1: cy + oRadius * Math.sin(angle), // y1 point (outer)
x2: cx + (oRadius - length) * Math.cos(angle), // x2 point (inner)
y2: cy + (oRadius - length) * Math.sin(angle) // y2 point (inner)
}
}
// calculate number of steps based on bins
var ctx = document.querySelector("canvas").getContext("2d"),
fftLength = 64,
angleStep = Math.PI * 2 / fftLength,
angle = 0,
line;
ctx.beginPath(); // not needed in demo, but when animated
while(angle < Math.PI*2) {
// our line function in action:
line = getLine(250, 250, angle, getFFT(), 240, 50);
ctx.moveTo(line.x1, line.y1); // add line to path
ctx.lineTo(line.x2, line.y2);
angle += angleStep // get next angle
}
ctx.lineWidth = 5; // beware of center area
ctx.stroke(); // stroke all lines at once
// to smooth the "FFT" random data
function getFFT() {return Math.random() * 0.16 + 0.4}
<canvas width=500 height=500></canvas>
Related
I have an instance of HTML 5 canvas and a rectangle drawn on it.
My drawing function takes a resizing angle into account and uses relative coordinates.
Relative coordinates're based upon three variables: top left rectangle point, rectangle width and rectangle height.
Rectangle width and rectangle height're calculated using two points: top left rectangle point and bottom right rectangle point.
To sum up, drawing function depends on top left rectangle point, bottom right rectangle point and rotation. It's an important point for the following text!
Here's a code snippet:
var canvas = document.getElementById('imageCanvas');
var ctx = canvas.getContext('2d');
var xTopLeft = 550;
var yTopLeft = 200;
var xBottomRight = 750;
var yBottomRight = 450;
var w = Math.max(xTopLeft, xBottomRight) - Math.min(xTopLeft, xBottomRight);
var h = Math.max(yTopLeft, yBottomRight) - Math.min(yTopLeft, yBottomRight);
var r = 1;
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save()
ctx.translate(xTopLeft + w / 2, yTopLeft + h / 2);
ctx.rotate(r);
ctx.fillStyle = "yellow";
ctx.fillRect(w / 2 * (-1), h / 2 * (-1), w, h);
ctx.restore()
}
Here's my rectangle with a bunch of controls: eight resizing handles (white) and one rotation handle (green):
Rotating works fine.
Resizing works fine.
And I also try to implement resizing after rotation. Here's my approach with a humble illustration:
Grab the coordinates of the red point (it's mouse cursor coordiantes)
Derotate the point using negative angle to get derotated coordinates
function rotatePoint(x, y, center_x, center_y, angle) {
var new_x = (x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx;
var new_y = (x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy;
return [new_x, new_y]
}
Update xTopLeft, yTopLeft and redraw
Done
The idea behind this approach is simple. Since my drawing function depeneds on top left rectangle point and bottom right rectangle point I just need to get their new coordinates.
For instance, here's a simplified code for B point:
if (point == 'B') {
var newPointB = rotatePoint(mouse.x, mouse.y, center_x, center_y, -r);
xBottomRight = newPointB[0];
yTopLeft = newPointB[1];
}
But it doesn't work as expected: while resizing my rotated rectangle shifts, jumps and totally misbehaves.
In search of insights I've stumbled upon this article. The article covers my problem, but I don't get author's approach and can't implement it.
Why should I always lock the coordinates of the A point? My top left handle is intended to resize the rectangle in a north-west direction, so it would be necessary to change the coordinates of the A point...
Why should we recalculate the center point before derotation? It breaks the idea of uniform matrix transformations...
What's the correct algorithm in my case?
I was also facing same problem. It turned out that the angle I was using was in degree. Try multiplying angle in rotatePoint function with (Math.PI / 180).
As you can see in the demo the L shape is getting cropped off the top of the screen and should be rotated 180 degrees and flush with the top left corner. I noticed two things that don't work as expected, the first is when I change ctx.translate(x, y) to ctx.moveTo(x, y) and increase the shape position to 100, 100 it moves more than 100px with translate, where as moveTo seems accurate. The second is that using a negative translate after ctx.stroke() has no affect on the shapes position.
var shape = {};
function draw(shape) {
var canvas = document.getElementById('canvas');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
var x = shape.position.x + 0.5;
var y = shape.position.y + 0.5;
ctx.translate(x, y);
ctx.translate(shape.width * shape.scale/2, shape.height * shape.scale/2);
ctx.rotate(shape.orientation * Math.PI/180);
ctx.beginPath();
for (var i = 0; i < shape.points.length; i++) {
x = shape.points[i].x * shape.scale + shape.position.x + 0.5;
y = shape.points[i].y * shape.scale + shape.position.y + 0.5;
ctx.lineTo(x, y);
}
ctx.strokeStyle = '#fff';
ctx.stroke();
ctx.translate(-shape.width * shape.scale/2, -shape.height * shape.scale/2);
ctx.restore();
}
}
// L Shape
shape.points = [];
shape.points.push({ x:0, y:0 });
shape.points.push({ x:0, y:3 });
shape.points.push({ x:2, y:3 });
shape.points.push({ x:2, y:2 });
shape.points.push({ x:1, y:2 });
shape.points.push({ x:1, y:0 });
shape.points.push({ x:0, y:0 });
shape.position = {x: 0, y: 0};
shape.scale = 30;
shape.width = 3;
shape.height = 2;
shape.orientation = 180;
draw(shape);
#canvas {
background: #272B34; }
<canvas id="canvas" width="400" height="600"></canvas>
The easiest way to do 2D tranforms is via the setTransform function which takes 6 numbers, 2 vectors representing the direction and scale of the X and y axis, and one coordinate representing the new origin.
Unlike the other transform functions which are dependent of the current state setTransform is not effected by any transform done before it is called.
To set the transform for a matrix that has a square aspect (x and y scale are the same) and that the y axis is at 90 deg to the x ( no skewing) and a rotation is as follows
// x,y the position of the orign
function setMatrix(x,y,scale,rotate){
var xAx = Math.cos(rotate) * scale; // the x axis x
var xAy = Math.sin(rotate) * scale; // the x axis y
ctx.setTransform(xAx, xAy, -xAy, xAx, x, y);
}
//use
setMatrix(100,100,20,Math.PI / 4);
ctx.strokeRect(-2,-2,4,4); // draw a square centered at 100,100
// scaled 20 times
// and rotate clockwise 45 deg
Update
In response to the questions in the comments.
Why sin and cos?
Can you also explain why you used cos and sin for the axis?
I use Math.sin and Math.cos to calculate the X axis and thus the Y axis as well (because y is at 90 deg to x) because it is slightly quicker than adding the rotation as a separate transform.
When you use any of the transform functions apart from setTransform you are doing a matrix multiplication. The next snippet is the JS equivalent minimum calculations done when using ctx.rotate, ctx.scale, ctx.translate, or ctx.transform
// mA represent the 2D context current transform
mA = [1,0,0,1,0,0]; // default transform
// mB represents the transform to apply
mB = [0,1,-1,0,0,0]; // Rotate 90 degree clockwise
// m is the resulting matrix
m[0] = mA[0] * mB[0] + mA[2] * mB[1];
m[1] = mA[1] * mB[0] + mA[3] * mB[1];
m[2] = mA[0] * mB[2] + mA[2] * mB[3];
m[3] = mA[1] * mB[2] + mA[3] * mB[3];
m[4] = mA[0] * mB[0] + mA[2] * mB[1] + mA[4];
m[5] = mA[1] * mB[0] + mA[3] * mB[1] + mA[5];
As you can see there are 12 multiplications and 6 additions plus the need for memory to hold the intermediate values and if the call was to ctx.rotation the sin and cos of the angle would also be done. This is all done in native code in the JavaScript engine so is quicker than doing in JS, but side stepping the matrix multiplication by calculating the axis in JavaScript results in less work. Using setTransform simply replaces the current matrix and does not require a matrix multiplication to be performed.
The alternative to the answer's setMatrix function can be
function setMatrix(x,y,scale,rotate){
ctx.setTransform(scale,0,0,scale, x, y); // set current matrix
ctx.rotate(rotate); // multiply current matrix with rotation matrix
}
which does the same and does look cleaner, though is slower and when you want to do things like games where performance is very important often called functions should be as quick as possible.
To use the setMatrix function
So how would I use this for custom shapes like the L in my demo?
Replacing your draw function. BTW you should be getting the context outside any draw function.
// assumes ctx is the 2D context in scope for this function.
function draw(shape) {
var i = 0;
setMatrix(shape.position.x, shape.position.y, shape.scale, shape.orientation); // orientation is in radians
ctx.strokeStyle = '#fff';
ctx.beginPath();
ctx.moveTo(shape.points[i].x, shape.points[i++].y)
while (i < shape.points.length) {
ctx.lineTo(shape.points[i].x, shape.points[i++].y);
}
ctx.closePath(); // draw line from end to start
ctx.stroke();
}
In your code you have the line stored such that its origin (0,0) is at the top left. When defining shapes you should define it in terms of its local coordinates. This will define the point of rotation and scaling and represents the coordinate that will be at the transforms origin (position x,y).
Thus you should define your shape at its origin
function createShape(originX, originY, points){
var i;
const shape = [];
for(i = 0; i < points.length; i++){
shape.push({
x : points[i][0] - originX,
y : points[i][1] - originY,
});
}
return shape;
}
const shape = {};
shape.points = createShape(
1,1.5, // the local origin relative to the coords on next line
[[0,0],[0,3],[2,3],[2,2],[1,2],[1,0]] // shape coords
);
I am working on a separate project where I'm trying to draw a line from a turret, to an enemy. But I want the line to stay within the boundaries of the turret, and not extend all the way to the enemy. A smaller example that I have made can be found below in the code snippet.
Please be thorough in your answers, because I'm definitely no expert. It'd be much appreciated! Thanks.
var canvas = document.getElementById("canvas");
var c = canvas.getContext("2d");
var turret = {x:20, y:20, w:20, h:20};
var randomBox = {x:30+Math.random() * 300, y:30+Math.random() * 300, w:40, h:40};
c.fillStyle = "red";
c.fillRect(turret.x, turret.y, turret.w, turret.h);
c.fillStyle = "yellow";
c.fillRect(randomBox.x, randomBox.y, randomBox.w, randomBox.h);
c.beginPath();
c.moveTo(turret.x + (turret.w/2), turret.y + (turret.h/2));
c.lineTo(randomBox.x, randomBox.y);
c.stroke();
#canvas {
background-color: #eee;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<canvas id="canvas" width=500 height=500></canvas>
</body>
</html>
Vector maths.
A line can be thought of as a coordinate and a vector, the vector describes the direction and distance from the start coordinate to the end point. When you describe a line by its endpoints you are using 2 coordinates. Usually we call a line with a start and end as a line segment, as opposed to a line which is actually infinitely long.
The line segment as coordinates
var x1 = 100;
var y1 = 100;
var x2 = 300;
var y2 = 400;
Line segment's vector
To get the vector component of a line you simple subtract the start point from the end point. A vector is a line segment of sorts that always has its start point at the coordinates 0,0.
var vx = x2 - x1; // v for vector
var vy = y2 - y1;
Magnitude or length
All vectors have a magnitude (or length) which is always positive by virtue of how the length is calculated. The x,and y components of the vector make up the two shorter side of a triangle and the line along the vector is the longest line, the hypotenuse.
We use pythagoras to calculate the vectors magnitude.
var mag = Math.sqrt(vx * vx + vy * vy); // mag for magnitude
Normalise vector
In many situations regarding lines (like in this case) we are not so much interested in the length of the line but rather just the direction. To make calculation easy we like to use a special form of the vector called a unit vector. This is a vector that is precisely 1 unit long (in our case 1 the unit is a pixel)
The unit vector has many properties that are very useful when doing geometry.
The process of creating a unit vector is called normalising. It is just done by dividing the x,y parts of the vector by its total length (as calculated above)
// normalise vector
var nx = vx / mag; // n for normal
var ny = vy / mag;
Scaling the normal
Now that you have the unit vector representing the direction of the line it is easy to find a point a fixed distance from the start. Like on the number line if you want a point 10 units from say 5 you add 10 to 5. But its is also the same as 10 "units" to 5 or 10 * 1 + 5. In Geometry the unit is the unit vector.
So if we want the point 10 units (pixels) from the start we multiply both components of the unit vector by 10 and add them to the start coordinate.
var endX = x1 + nx * 10;
var endY = y1 + ny * 10;
Now endX,endY is 10 pixels away from the line start.
Vector math.
As you can see having the unit vector makes it easy to get any point any distance from the start point.
All vector libraries include the above operations for vectors and coordinates. We call a set of two coordinates (x,y) a 2D vector vec and we have operations like add(), sub(), multiply(), divide(), magnitude(), normalise(), and a few equally handy operations like cross() and dot() but that is out of scope for this question for now.
If you are interested in writing games I strongly suggest that you write you own small 2D vector library, it should take no more than a hour to create (maybe two if you are new to programing) and you will learn some essential math skills required to write almost any type of game (BTW 3D vector math is almost identical to 2D with just the extra component called z).
To your problem
I have copied your code and made a few changes. I have also taken a few short cuts in the math described above. Like with any set of formulas if you put them all together you can simplify them to get a more effective and easy to understand formula. This is a particularly hand thing to do in games where you may be doing some calculations millions to 100's of millions of times a second.
The function drawLineLength takes the line segment x1,y1,x2,y2 and the value maxLen to draw a line that is no longer than maxLen pixels long.
If you have question please do ask in the comments.
Your code adjusted as to your question.
var canvas = document.getElementById("canvas");
var c = canvas.getContext("2d");
c.lineWidth = 4;
c.lineCap = "round";
var turret = {x:20, y:20, w:20, h:20};
const maxLineLength = 50; // max length of line in pixels
var rBox = { // FKA randomBox
x : 0,
y : 0,
w : 40,
h : 40
};
// drawLineLength draws a line with maximum length
// x1,y1 from
// x2,y2 line to
// maxLen the maximum length of the line. If line is shorter it will not be changed
function drawLineLength (x1, y1, x2, y2, maxLen) {
var vx = x2 - x1; // get dist between start and end of line
var vy = y2 - y1; // for x and y
// use pythagoras to get line total length
var mag = Math.sqrt(vx * vx + vy * vy);
if (mag > maxLen) { // is the line longer than needed?
// calculate how much to scale the line to get the correct distance
mag = maxLen / mag;
vx *= mag;
vy *= mag;
}
c.beginPath();
c.moveTo(x1, y1);
c.lineTo(x1 + vx, y1 + vy);
c.stroke();
}
// Test function is on a timer and run every 500ms (1/2 second)
function doTest () {
// clear the canvas of last render
c.clearRect(0, 0, canvas.width, canvas.height);
// find a random position for the box inside the canvas and 10 pixels from any edge
rBox.x = 10 + Math.random() * (canvas.width - rBox.w - 20);
rBox.y = 10 + Math.random() * (canvas.height - rBox.h - 20);
// draw both objects
c.fillStyle = "red";
c.fillRect(turret.x, turret.y, turret.w, turret.h);
c.fillStyle = "yellow";
c.fillRect(rBox.x, rBox.y, rBox.w, rBox.h);
// call function to draw line that will have a maximum length
drawLineLength(
turret.x + turret.w / 2,
turret.y + turret.h / 2,
rBox.x + rBox.w / 2,
rBox.y + rBox.h / 2,
maxLineLength
);
setTimeout(doTest,500);
}
doTest();
#canvas { background-color: #eee; }
<canvas id="canvas" width=500 height=500></canvas>
Edit: I could divide the radius with the angle?
Problem: For the sake of learning the arts of collision in HTML5 Canvas, I am currently trying to get a full circle to collide with a segmented circle, in this case a semi circle.
What I Tried: My first thought was a simple circle to circle collision would do but I was wrong. I read various sources on collision detection but all of them were either the standard circle / circle, box / circle, box / box, or polygon collision formulas.
My Question: What is the formula for colliding a full circle with only a segmented circle? It seems something other than just the radius comes into play. Maybe the radians as well?
Attempt:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var C1 = {x: 45, y: 65, radius: 20};
var C2 = {x: 60, y: 20, radius: 20};
var dx = C1.x - C2.x;
var dy = C1.y - C2.y;
var distance = Math.sqrt(dx * dx + dy * dy);
ctx.beginPath();
ctx.arc(C1.x, C1.y, C1.radius, 0, Math.PI * 2);
ctx.fillStyle = 'green';
ctx.fill();
ctx.beginPath();
ctx.rotate(0.3);
ctx.arc(C2.x, C2.y, C2.radius, 0, Math.PI * 1);
ctx.fillStyle = 'red';
ctx.fill();
if (distance < C1.radius + C2.radius) {
alert('true')
}
else {
alert('false')
}
A demo for to play around with: jsfiddle.net/tonyh90g/1
My learning resource: https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
You're on the right tracks, you will indeed need to not only calculate the distance between centres but also the angle of the intersection point(s) on the segment.
In general cartesian coordinates that angle can be calculated using Math.atan2(dy, dx) where dx and dy are the difference in coordinates from the segment to the circle. If those angles fall between the segment's angle, you have a hit.
[NB: At the exact point of touching there's only one point, but if the two objects end up slightly overlapping, which is not uncommon in animation, you'll get two points].
It's also possible that the circle could intersect the line segments rather than the round portion of the segment, but I think those cases will be correctly caught anyway. This may require further investigation, and if required would need a line-segment / circle intersection test.
I also note in your fiddle that you're using rotate() transforms on the sector. This will foul up your angle calculations unless you specifically account for it. It may be easier to use absolute angles throughout.
0 to 0,-70 by this :
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
ctx.rotate(Math.PI/-10;);
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(0,-70);
ctx.stroke();
And I can rotate that by 'PI/-10', and that works.
How i can get the x,y points of this after using rotate?
Your x and y points will still be 0 and -70 as they are relative to the translation (rotation). It basically means you would need to "reverse engineer" the matrix to get the resulting position you see on the canvas.
If you want to calculate a line which goes 70 pixels at -10 degrees you can use simple trigonometry to calculate it yourself instead (which is easier than going sort of backwards in the matrix).
You can use a function like this that takes your context, the start position of the line (x, y) the length (in pixels) and angle (in degrees) of the line you want to draw. It draw the line and returns an object with x and y for the end point of that line:
function lineToAngle(ctx, x1, y1, length, angle) {
angle *= Math.PI / 180;
var x2 = x1 + length * Math.cos(angle),
y2 = y1 + length * Math.sin(angle);
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
return {x: x2, y: y2};
}
Then just call it as:
var pos = lineToAngle(ctx, 0, 0, 70, -10);
//show result of end point
console.log('x:', pos.x.toFixed(2), 'y:', pos.y.toFixed(2));
Result:
x: 68.94 y: -12.16
Or you can instead extend the canvas' context by doing this:
if (typeof CanvasRenderingContext2D !== 'undefined') {
CanvasRenderingContext2D.prototype.lineToAngle =
function(x1, y1, length, angle) {
angle *= Math.PI / 180;
var x2 = x1 + length * Math.cos(angle),
y2 = y1 + length * Math.sin(angle);
this.moveTo(x1, y1);
this.lineTo(x2, y2);
return {x: x2, y: y2};
}
}
And then use it directly on your context like this:
var pos = ctx.lineToAngle(100, 100, 70, -10);
ctx.stroke(); //we stroke separately to allow this being used in a path
console.log('x:', pos.x.toFixed(2), 'y:', pos.y.toFixed(2));
(0 degrees will point to the right).
So you're asking "after I set a transform, how can I run points through that transform"?
In that case, see HTML5 Canvas get transform matrix? . The question and answers are somewhat old, but seem up-to-date. I can't find anything in the current HTML5 spec that lets you access and use the transform matrix. (I see that it's theoretically accessable through context.currentTransform, but I don't see any functionality to let you use the matrix - you'd have to multiply your point through the matrix yourself, or fake it by creating a full SVGMatrix for your point vector.)
The top answer shows a transform class you can use. Track your changes through that, and use their transformPoint function to get the point you want transformed to its endpoint.