How to draw an oval in html5 canvas? - javascript

There doesnt seem to be a native function to draw an oval-like shape. Also i am not looking for the egg-shape.
Is it possible to draw an oval with 2 bezier curves?
Somebody expierenced with that?
My purpose is to draw some eyes and actually im just using arcs.
Thanks in advance.
Solution
So scale() changes the scaling for all next shapes.
Save() saves the settings before and restore is used to restore the settings to draw new shapes without scaling.
Thanks to Jani
ctx.save();
ctx.scale(0.75, 1);
ctx.beginPath();
ctx.arc(20, 21, 10, 0, Math.PI*2, false);
ctx.stroke();
ctx.closePath();
ctx.restore();

updates:
scaling method can affect stroke width appearance
scaling method done right can keep stroke width intact
canvas has ellipse method that Chrome now supports
added updated tests to JSBin
JSBin Testing Example (updated to test other's answers for comparison)
Bezier - draw based on top left containing rect and width/height
Bezier with Center - draw based on center and width/height
Arcs and Scaling - draw based on drawing circle and scaling
see Deven Kalra's answer
Quadratic Curves - draw with quadratics
test appears to not draw quite the same, may be implementation
see oyophant's answer
Canvas Ellipse - using W3C standard ellipse() method
test appears to not draw quite the same, may be implementation
see Loktar's answer
Original:
If you want a symmetrical oval you could always create a circle of radius width, and then scale it to the height you want (edit: notice this will affect stroke width appearance - see acdameli's answer), but if you want full control of the ellipse here's one way using bezier curves.
<canvas id="thecanvas" width="400" height="400"></canvas>
<script>
var canvas = document.getElementById('thecanvas');
if(canvas.getContext)
{
var ctx = canvas.getContext('2d');
drawEllipse(ctx, 10, 10, 100, 60);
drawEllipseByCenter(ctx, 60,40,20,10);
}
function drawEllipseByCenter(ctx, cx, cy, w, h) {
drawEllipse(ctx, cx - w/2.0, cy - h/2.0, w, h);
}
function drawEllipse(ctx, x, y, w, h) {
var kappa = .5522848,
ox = (w / 2) * kappa, // control point offset horizontal
oy = (h / 2) * kappa, // control point offset vertical
xe = x + w, // x-end
ye = y + h, // y-end
xm = x + w / 2, // x-middle
ym = y + h / 2; // y-middle
ctx.beginPath();
ctx.moveTo(x, ym);
ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
//ctx.closePath(); // not used correctly, see comments (use to close off open path)
ctx.stroke();
}
</script>

Here is a simplified version of solutions elsewhere. I draw a canonical circle, translate and scale and then stroke.
function ellipse(context, cx, cy, rx, ry){
context.save(); // save state
context.beginPath();
context.translate(cx-rx, cy-ry);
context.scale(rx, ry);
context.arc(1, 1, 1, 0, 2 * Math.PI, false);
context.restore(); // restore to original state
context.stroke();
}

There is now a native ellipse function for canvas, very similar to the arc function although now we have two radius values and a rotation which is awesome.
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)
Live Demo
ctx.ellipse(100, 100, 10, 15, 0, 0, Math.PI*2);
ctx.fill();
Only seems to work in Chrome currently

The bezier curve approach is great for simple ovals. For more control, you can use a loop to draw an ellipse with different values for the x and y radius (radiuses, radii?).
Adding a rotationAngle parameter allows the oval to be rotated around its center by any angle. Partial ovals can be drawn by changing the range (var i) over which the loop runs.
Rendering the oval this way allows you to determine the exact x,y location of all points on the line. This is useful if the postion of other objects depend on the location and orientation of the oval.
Here is an example of the code:
for (var i = 0 * Math.PI; i < 2 * Math.PI; i += 0.01 ) {
xPos = centerX - (radiusX * Math.sin(i)) * Math.sin(rotationAngle * Math.PI) + (radiusY * Math.cos(i)) * Math.cos(rotationAngle * Math.PI);
yPos = centerY + (radiusY * Math.cos(i)) * Math.sin(rotationAngle * Math.PI) + (radiusX * Math.sin(i)) * Math.cos(rotationAngle * Math.PI);
if (i == 0) {
cxt.moveTo(xPos, yPos);
} else {
cxt.lineTo(xPos, yPos);
}
}
See an interactive example here: http://www.scienceprimer.com/draw-oval-html5-canvas

You could also try using non-uniform scaling. You can provide X and Y scaling, so simply set X or Y scaling larger than the other, and draw a circle, and you have an ellipse.

You need 4 bezier curves (and a magic number) to reliably reproduce an ellipse. See here:
www.tinaja.com/glib/ellipse4.pdf
Two beziers don't accurately reproduce an ellipse. To prove this, try some of the 2 bezier solutions above with equal height and width - they should ideally approximate a circle but they won't. They'll still look oval which goes to prove they aren't doing what they are supposed to.
Here's something that should work:
http://jsfiddle.net/BsPsj/
Here's the code:
function ellipse(cx, cy, w, h){
var ctx = canvas.getContext('2d');
ctx.beginPath();
var lx = cx - w/2,
rx = cx + w/2,
ty = cy - h/2,
by = cy + h/2;
var magic = 0.551784;
var xmagic = magic*w/2;
var ymagic = h*magic/2;
ctx.moveTo(cx,ty);
ctx.bezierCurveTo(cx+xmagic,ty,rx,cy-ymagic,rx,cy);
ctx.bezierCurveTo(rx,cy+ymagic,cx+xmagic,by,cx,by);
ctx.bezierCurveTo(cx-xmagic,by,lx,cy+ymagic,lx,cy);
ctx.bezierCurveTo(lx,cy-ymagic,cx-xmagic,ty,cx,ty);
ctx.stroke();
}

I did a little adaptation of this code (partially presented by Andrew Staroscik) for peoplo who do not want a so general ellipse and who have only the greater semi-axis and the excentricity data of the ellipse (good for astronomical javascript toys to plot orbits, for instance).
Here you go, remembering that one can adapt the steps in i to have a greater precision in the drawing:
/* draw ellipse
* x0,y0 = center of the ellipse
* a = greater semi-axis
* exc = ellipse excentricity (exc = 0 for circle, 0 < exc < 1 for ellipse, exc > 1 for hyperbole)
*/
function drawEllipse(ctx, x0, y0, a, exc, lineWidth, color)
{
x0 += a * exc;
var r = a * (1 - exc*exc)/(1 + exc),
x = x0 + r,
y = y0;
ctx.beginPath();
ctx.moveTo(x, y);
var i = 0.01 * Math.PI;
var twoPi = 2 * Math.PI;
while (i < twoPi) {
r = a * (1 - exc*exc)/(1 + exc * Math.cos(i));
x = x0 + r * Math.cos(i);
y = y0 + r * Math.sin(i);
ctx.lineTo(x, y);
i += 0.01;
}
ctx.lineWidth = lineWidth;
ctx.strokeStyle = color;
ctx.closePath();
ctx.stroke();
}

My solution is a bit different than all of these. Closest I think is the most voted answer above though, but I think this way is a bit cleaner and easier to comprehend.
http://jsfiddle.net/jaredwilli/CZeEG/4/
function bezierCurve(centerX, centerY, width, height) {
con.beginPath();
con.moveTo(centerX, centerY - height / 2);
con.bezierCurveTo(
centerX + width / 2, centerY - height / 2,
centerX + width / 2, centerY + height / 2,
centerX, centerY + height / 2
);
con.bezierCurveTo(
centerX - width / 2, centerY + height / 2,
centerX - width / 2, centerY - height / 2,
centerX, centerY - height / 2
);
con.fillStyle = 'white';
con.fill();
con.closePath();
}
And then use it like this:
bezierCurve(x + 60, y + 75, 80, 130);
There are a couple use examples in the fiddle, along with a failed attempt to make one using quadraticCurveTo.

I like the Bezier curves solution above. I noticed the scale also affects the line width so if you're trying to draw an ellipse that is wider than it is tall, your top and bottom "sides" will appear thinner than your left and right "sides"...
a good example would be:
ctx.lineWidth = 4;
ctx.scale(1, 0.5);
ctx.beginPath();
ctx.arc(20, 20, 10, 0, Math.PI * 2, false);
ctx.stroke();
you should notice the width of the line at the peak and valley of the ellipse are half as wide as at the left and right apexes (apices?).

Chrome and Opera support ellipse method for canvas 2d context, but IE,Edge,Firefox and Safari don't support it.
We can implement the ellipse method by JS or use a third-party polyfill.
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)
Usage example:
ctx.ellipse(20, 21, 10, 10, 0, 0, Math.PI*2, true);
You can use a canvas-5-polyfill to provide ellipse method.
Or just paste some js code to provide ellipse method:
if (CanvasRenderingContext2D.prototype.ellipse == undefined) {
CanvasRenderingContext2D.prototype.ellipse = function(x, y, radiusX, radiusY,
rotation, startAngle, endAngle, antiClockwise) {
this.save();
this.translate(x, y);
this.rotate(rotation);
this.scale(radiusX, radiusY);
this.arc(0, 0, 1, startAngle, endAngle, antiClockwise);
this.restore();
}
}

Yes, it is possible with two bezier curves - here's a brief tutorial/example:
http://www.williammalone.com/briefs/how-to-draw-ellipse-html5-canvas/

Since nobody came up with an approach using the simpler quadraticCurveTo I am adding a solution for that. Simply replace the bezierCurveTo calls in the #Steve's answer with this:
ctx.quadraticCurveTo(x,y,xm,y);
ctx.quadraticCurveTo(xe,y,xe,ym);
ctx.quadraticCurveTo(xe,ye,xm,ye);
ctx.quadraticCurveTo(x,ye,x,ym);
You may also remove the closePath. The oval is looking slightly different though.

This is another way of creating an ellipse like shape, although it uses the "fillRect()" function this can be used be changing the arguments in the fillRect() function.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Sine and cosine functions</title>
</head>
<body>
<canvas id="trigCan" width="400" height="400"></canvas>
<script type="text/javascript">
var canvas = document.getElementById("trigCan"), ctx = canvas.getContext('2d');
for (var i = 0; i < 360; i++) {
var x = Math.sin(i), y = Math.cos(i);
ctx.stroke();
ctx.fillRect(50 * 2 * x * 2 / 5 + 200, 40 * 2 * y / 4 + 200, 10, 10, true);
}
</script>
</body>
</html>

With this you can even draw segments of an ellipse:
function ellipse(color, lineWidth, x, y, stretchX, stretchY, startAngle, endAngle) {
for (var angle = startAngle; angle < endAngle; angle += Math.PI / 180) {
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineTo(x + Math.cos(angle) * stretchX, y + Math.sin(angle) * stretchY)
ctx.lineWidth = lineWidth
ctx.strokeStyle = color
ctx.stroke()
ctx.closePath()
}
}
http://jsfiddle.net/FazAe/1/

Here's a function I wrote that uses the same values as the ellipse arc in SVG. X1 & Y1 are the last coordinates, X2 & Y2 are the end coordinates, radius is a number value and clockwise is a boolean value. It also assumes your canvas context has already been defined.
function ellipse(x1, y1, x2, y2, radius, clockwise) {
var cBx = (x1 + x2) / 2; //get point between xy1 and xy2
var cBy = (y1 + y2) / 2;
var aB = Math.atan2(y1 - y2, x1 - x2); //get angle to bulge point in radians
if (clockwise) { aB += (90 * (Math.PI / 180)); }
else { aB -= (90 * (Math.PI / 180)); }
var op_side = Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) / 2;
var adj_side = Math.sqrt(Math.pow(radius, 2) - Math.pow(op_side, 2));
if (isNaN(adj_side)) {
adj_side = Math.sqrt(Math.pow(op_side, 2) - Math.pow(radius, 2));
}
var Cx = cBx + (adj_side * Math.cos(aB));
var Cy = cBy + (adj_side * Math.sin(aB));
var startA = Math.atan2(y1 - Cy, x1 - Cx); //get start/end angles in radians
var endA = Math.atan2(y2 - Cy, x2 - Cx);
var mid = (startA + endA) / 2;
var Mx = Cx + (radius * Math.cos(mid));
var My = Cy + (radius * Math.sin(mid));
context.arc(Cx, Cy, radius, startA, endA, clockwise);
}

If you want the ellipse to fully fit inside a rectangle, it's really like this:
function ellipse(canvasContext, x, y, width, height){
var z = canvasContext, X = Math.round(x), Y = Math.round(y), wd = Math.round(width), ht = Math.round(height), h6 = Math.round(ht/6);
var y2 = Math.round(Y+ht/2), xw = X+wd, ym = Y-h6, yp = Y+ht+h6, cs = cards, c = this.card;
z.beginPath(); z.moveTo(X, y2); z.bezierCurveTo(X, ym, xw, ym, xw, y2); z.bezierCurveTo(xw, yp, X, yp, X, y2); z.fill(); z.stroke();
return z;
}
Make sure your canvasContext.fillStyle = 'rgba(0,0,0,0)'; for no fill with this design.

Related

Using trigonometry to animate HTML5 canvas, but how to position this square?

Sometimes you wish you could go back in time to tell your younger self that maths are indeed important! But I doubt I would've listened back then. I've been playing with trigonometry lately for animation purposes with the HTML5 canvas in this particular example.
It's a super simple animation: It positions an arc in a circular manner around the center of the canvas. The X and Y positions are calculated based upon the basic trigonometry functions sinus and cosinus. "SohCahToa". I think I'm starting to get it. But somehow I cannot figure out how to draw a square in the middle of one of the triangular sides.
let radius = 200;
let angle = 0;
x = centerX + Math.cos(angle) * radius;
ctx.beginPath();
ctx.fillRect(x/2, centerY, 20, 20);
https://codepen.io/melvinidema/pen/wvKPepa?editors=1010
So the arc is drawn by adding up the center of the canvas with the
rework formulas: (co)sinus - for X = Cos, for Y = Sin) of the angle times the radius of the circle.
If we only take the X position (red line) and want to draw the square half of the position of the arc. ( So in the middle of the red line ) we should just be able to divide the freshly calculated X position by two right? But if I do that, the square is magically drawn completely outside the circle.
What's happening? Why does it behave like this? And what calculation should I use instead for the square to position in the middle of the red line throughout the animation?
Thanks in advance!
Your x defined as:
x = centerX + Math.cos(angle) * radius;
but when you want to divide by 2, you just need to divide the Math.cos(angle) * radius, while the centerX is the zero point, and its stand as it is.
So the rect should be placed at:
centerX + Math.cos(angle)/2
Also, I think will be better if you reduce half of the rect width, and get:
centerX + Math.cos(angle)/2 - 10
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
let radius = 200;
function frame(angle) {
const cx = canvas.width / 2,
cy = canvas.height / 2,
x = Math.cos(angle) * radius,
y = Math.sin(angle) * radius;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx+x, cy+y);
ctx.lineTo(cx+x, cy);
ctx.lineTo(cx, cy);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.arc(cx+x, cy+y, 10, 0, Math.PI*2);
ctx.fill();
ctx.fillRect(cx + x/2 - 10, cy - 10, 20, 20);
ctx.closePath();
requestAnimationFrame(()=>frame(angle+.03));
}
frame(0)
canvas {
display: block;
max-height: 100vh;
margin: auto;
}
<canvas width="500" height="500"></canvas>

How to let the user draw an ellipse?

I'm making a simple paint program with javascript. The circle-tool is working as intended, but how do I add a tool for drawing an ellipse/oval?
I have reused much of the code for making the different tools (pencil, line, rectangle, and circle), and so far so good. But I can't get the code for drawing the ellipse right.
Here's the current and working code for drawing a circle filled with a random color:
var radius = Math.max(
Math.abs(ev._x - tool.x0),
Math.abs(ev._y - tool.y0)
) / 2;
var x = Math.min(ev._x, tool.x0) + radius;
var y = Math.min(ev._y, tool.y0) + radius;
context.fillStyle = 'hsl(' + 360 * Math.random() + ', 85%, 50%)';
context.beginPath();
context.arc(x, y, radius, 0, Math.PI*2, false);
context.stroke();
context.closePath();
context.fill();
I've tried to integrate it with the code already in use and the context.scale, but I don't get it right:
var radius = Math.max(
Math.abs(ev._x - tool.x0),
Math.abs(ev._y - tool.y0)
) / 2;
var radius2 = ;
var x = Math.min(ev._x, tool.x0) + radius;
var y = Math.min(ev._y, tool.y0) + radius;
context.save();
context.scale(1, radius2/radius);
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI);
context.restore();
context.lineWidth=2;
context.strokeStyle="#000";
context.stroke();
Is the var radius correct, and do I write the var radius2? The entire code is here JS Bin
The x attribute defines the x coordinate of the center of the ellipse
The y attribute defines the y coordinate of the center of the ellipse
The radiusX attribute defines the horizontal radius
The ry attribute defines the vertical radius
Image for your reference

Rotation Matrix Spiraling inward

I am trying to make a square that rotates in place, however my square is spiraling inward, and I have no idea why. Here is the code, if someone could please explain what is happening as to why it is not just spinning in place.
var angle = 2 * (Math.PI / 180);
var rotate = [
[Math.cos(angle),Math.sin(angle)],
[-Math.sin(angle),Math.cos(angle)]
];
var points = [[300,0],[0,300],[-300,0],[0,-300]];
init.ctx.translate(init.canvas.width/2,init.canvas.height/2);
function loop(){
draw();
}
setInterval(loop,10);
function draw(){
init.ctx.beginPath();
init.ctx.moveTo(points[0][0],points[0][1]);
init.ctx.lineTo(points[1][0],points[1][1]);
init.ctx.lineTo(points[2][0],points[2][1]);
init.ctx.lineTo(points[3][0],points[3][1]);
init.ctx.closePath();
init.ctx.stroke();
for(let i=0;i<points.length;i++){
init.ctx.beginPath();
init.ctx.fillStyle = "red";
init.ctx.fillRect(points[i][0],points[i][1],5,5);
points[i][0] = points[i][0]*rotate[0][0] + points[i][1]*rotate[0][1];
points[i][1] = points[i][0]*rotate[1][0] + points[i][1]*rotate[1][1];
}
}
So, you are applying a small rotation each time draw is called, specifically 1/180th of a full rotation. Problem is that you are relying on floating point math to give you exact values, and it's not because it doesn't. This is compounded by the points array being calculated by iterations. I suggest calculate the new points on each step through draw by applying the correct rotate matrix for your current angle to the starting points.
var angle = 0;
var startPoints = [[300,0],[0,300],[-300,0],[0,-300]];
var points = [[300,0],[0,300],[-300,0],[0,-300]];
init.ctx.translate(init.canvas.width/2,init.canvas.height/2);
function loop(){
draw();
}
setInterval(loop,10);
function draw(){
init.ctx.beginPath();
init.ctx.moveTo(points[0][0],points[0][1]);
init.ctx.lineTo(points[1][0],points[1][1]);
init.ctx.lineTo(points[2][0],points[2][1]);
init.ctx.lineTo(points[3][0],points[3][1]);
init.ctx.closePath();
init.ctx.stroke();
angle = angle + Math.PI / 90;
var rotate = [
[Math.cos(angle),Math.sin(angle)],
[-Math.sin(angle),Math.cos(angle)]
];
for(let i=0;i<points.length;i++){
init.ctx.beginPath();
init.ctx.fillStyle = "red";
init.ctx.fillRect(points[i][0],points[i][1],5,5);
points[i][0] = startPoints[i][0]*rotate[0][0] + startPoints[i][1]*rotate[0][1];
points[i][1] = startPoints[i][0]*rotate[1][0] + startPoints[i][1]*rotate[1][1];
}
}
Some tips to improve your code.
As a beginner I can see some bad habits creeping in and as there is already an answer I thought I would just give some tips to improve your code.
Don't use setInterval to create animations. requestAnimationFrame gives much better quality animations.
Arrays were created in high level languages to make life easier, not harder.
You have painfully typed out
init.ctx.beginPath();
init.ctx.moveTo(points[0][0],points[0][1]);
init.ctx.lineTo(points[1][0],points[1][1]);
init.ctx.lineTo(points[2][0],points[2][1]);
init.ctx.lineTo(points[3][0],points[3][1]);
init.ctx.closePath();
init.ctx.stroke();
That would be a nightmare if you had 100 points. Much better to create a generic function to do that for you.
function drawShape(ctx,shape){
ctx.beginPath();
for(var i = 0; i < shape.length; i++){
ctx.lineTo(shape[i][0], shape[i][1]);
}
ctx.closePath();
ctx.stroke();
}
Now you can render any shape on any canvas context with the same code.
drawShape(init.ctx,points); // how to draw your shape.
If you use a uniform scale then you can shorten the transform a little by reusing the x axis of the transformation
var rotate = [
[Math.cos(angle),Math.sin(angle)],
[-Math.sin(angle),Math.cos(angle)]
];
Note how the second two values are just the first two swapped with the new x negated. You can also include a scale in that and just hold the first two values.
var angle = ?
var scale = 1; // can be anything
// now you only need two values for the transform
var xAx = Math.cos(angle) * scale; // direction and size of x axis
var xAy = Math.sin(angle) * scale;
And you apply the transform to a point as follows
var px = ?; // point to transform
var py = ?;
var tx = px * xAx - py * xAy;
var ty = px * xAy + py * xAx;
And to add a origin
var tx = px * xAx - py * xAy + ox; // ox,oy is the origin
var ty = px * xAy + py * xAx + oy;
But is is much better to let the canvas 2D API do the transformation for you. The example below shows the various methods described above to render your box and animate the box.
Example using best practice.
const ctx = canvas.getContext("2d");
var w = canvas.width; // w,h these are set if canvas is resized
var h = canvas.height;
var cw = w / 2; // center width
var ch = h / 2; // center height
var globalScale = 1; // used to scale shape to fit the canvas
var globalTime;
var angle = Math.PI / 2;
var rotateRate = 90; // deg per second
var points = [
[300, 0],
[0, 300],
[-300, 0],
[0, -300]
];
var maxSize = Math.hypot(600, 600); // diagonal size used to caculate scale
// so that shape fits inside the canvas
// Add path to the current path
// shape contains path points
// x,y origin of shape
// scale is the scale of the shape
// angle is the amount of rotation in radians.
function createShape(shape, x, y, scale, angle) {
var i = 0;
ctx.setTransform(scale, 0, 0, scale, x, y); // set the scale and origin
ctx.rotate(angle); // set the rotation
ctx.moveTo(shape[i][0], shape[i++][1]);
while (i < shape.length) { // create a line to each point
ctx.lineTo(shape[i][0], shape[i++][1]);
}
}
// draws fixed scale axis aligned boxes at vertices.
// shape contains the vertices
// vertSize size of boxes drawn at verts
// x,y origin of shape
// scale is the scale of the shape
// angle is the amount of rotation in radians.
function drawVertices(shape, vertSize, x, y, scale, angle) {
ctx.setTransform(1, 0, 0, 1, x, y);
const xAx = Math.cos(angle) * scale; // direction and size of x axis
const xAy = Math.sin(angle) * scale;
var i = 0;
while (i < shape.length) {
const vx = shape[i][0]; // get vert coordinate
const vy = shape[i++][1]; // IMPORTANT DONT forget i++ in the while loop
ctx.fillRect(
vx * xAx - vy * xAy - vertSize / 2, // transform and offset by half box size
vx * xAy + vy * xAx - vertSize / 2,
vertSize, vertSize
);
}
}
// draws shape outline and vertices
function drawFullShape(shape, scale, angle, lineCol, vertCol, lineWidth, vertSize) {
// draw outline of shape
ctx.strokeStyle = lineCol;
ctx.lineWidth = lineWidth / scale; // to ensure that the line with is 1 pixel
// set the width to in inverse scale
ctx.beginPath();
// shape origin at cw,ch
createShape(shape, cw, ch, scale, angle);
ctx.closePath();
ctx.stroke();
// draw the vert boxes.
ctx.fillStyle = vertCol;
drawVertices(shape, vertSize, cw, ch, scale, angle);
}
function loop(timer) {
globalTime = timer;
if (w !== innerWidth || h !== innerHeight) { // check if canvas need resize
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
globalScale = Math.min(w / maxSize, h / maxSize);
}
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.clearRect(0, 0, w, h);
const innerAngle = globalTime * (rotateRate * (Math.PI / 180)) / 1000;
drawFullShape(points, globalScale, angle, "black", "red", 2, 6);
drawFullShape(points, globalScale * 0.5, innerAngle, "black", "red", 2, 6);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>

Moving an object on a path using p5.js

I'm trying to write a code that mimics what happens in this video in an HTML canvas.
My code differs in that each time the page loads or is refreshed, the radiuses of the two circles are randomly generated. I need the "planets" to travel at the same speed along the circumferences of their respective circles.
I'm using the p5.js to draw to the canvas. Is there something in p5.js that draws an object according to a path, in my case a path that is a circle?
I've looked through the references and came across vectors but I don't quite understand what they do.
My code so far:
var w = window.innerWidth;
var h = window.innerHeight;
var r1, r1;
var d;
var x1, x2, y1, y2;
var angle = Math.PI / 4;
function setup() {
// canvas setup
createCanvas(w, h);
background(255);
angleMode(RADIANS);
// randomly generated radiuses
r1 = Math.floor(Math.random() * (h/2-300)) + 300;
r2 = Math.floor(Math.random() * (200-100)) + 100;
// drawing the two ellipses
ellipseMode(CENTER);
noFill();
ellipse(w/2, h/2, r1*2, r1*2);
ellipse(w/2, h/2, r2*2, r2*2);
}
function draw() {
// points on the circles
x1 = (r1 * (Math.cos(angle))) + (w/2);
y1 = (h/2) - (r1 * (Math.sin(angle)));
x2 = (r2 * (Math.cos(angle))) + (w/2);
y2 = (h/2) - (r2 * (Math.sin(angle)));
// drawing two circles at those points
ellipseMode(CENTER);
noStroke();
fill('rgb(140, 140, 140)');
ellipse(x1, y1, 20, 20);
ellipse(x2, y2, 20, 20);
// randomly generated color for the line
var r = random(0, 255);
var g = random(0, 255);
var b = random(0, 255);
stroke(r, g, b);
strokeWeight(1);
// drawing the line that connects the two points
line(x1, y1, x2, y2);
// animation????
angle = angle + (10 * (Math.PI / 180));
}
The problem with the last line is that it creates evenly spaces lines, not the pattern that is created in the video.
If the two planets move with the same angle increment they will always retain a relationship causing an evenly spaced line between them.
In order for a line connected between them to cross the center they have to have different incremental values. You would have to maintain two different angle values as well as step (or speed) per angle.
For the example in the video the speed ratio is 1:2.247 based on a real-world relationship between earth and Venus' day-ratio around the sun. Since they differ in speed the line between them will now cross over and produce the pentagram-ish pattern.
I don't know P5.js so I have added a plain JavaScript example below which can be considered pseudo code in case P5 is a requirement. From that you will be able to see how to calculate the two positions at variable speeds.
Example
var ctx = c.getContext("2d"),
ratio = 2.247, // earth:venus ratio 1:2.247
angle1 = 0, // planet 1 current angle
angle2 = 0, // planet 2 current angle
dlt = 0.05, // angle step (speed)
radius1 = 150, // radius path for planet 1
radius2 = 100, // radius path for planet 2
cx = c.width*0.5, // center canvas
cy = c.height*0.5,
t = 503; // abort animation per # of frames
ctx.strokeStyle = "rgba(0,120,255,0.5)";
(function loop() {
var x1, y1, x2, y2;
x1 = cx + Math.cos(angle1) * radius1; // current (x,y) for planet 1
y1 = cy + Math.sin(angle1) * radius1;
x2 = cx + Math.cos(angle2) * radius2; // current (x,y) for planet 2
y2 = cy + Math.sin(angle2) * radius2;
ctx.beginPath(); // draw line
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
angle1 += dlt; // increase angle planet 1
angle2 += dlt * ratio; // increase angle planet 2 per ratio
if (t--) requestAnimationFrame(loop)
})()
<canvas id=c height=300></canvas>

How to draw the line in canvas

I want to draw some lines inside a circle on a canvas, in the following manner.
I have no idea how to draw the lines as showed below. But I have the basic knowledge to draw line and arcs on a canvas. How to proceed?
You can use a bezier curve as suggested in comments with control points, however, these can turn out to be hard to control (no pun) as they do not pass through the points you have defined and you always need to define two control points.
In order to achieve a line through points using the actual points you need to use cardinal splines.
There is no built-in support for these but a while back I made an implementation of this for JavaScript and canvas (code can be downloaded from here, MIT license).
With this you can simply define three points as a minimum (to get a simple curve) and the function will take care of drawing a smooth curve between the points with a set tension value.
If you for example defined the following three points:
var pts = [10,100, 200,50, 390,100];
You would obviously just get a simple poly-line like this if we wanted to illustrate the points (for comparison):
Using a cardinal spline with the same three points would give you this:
The following code generates the above curve (without the red dots showing the point coordinates):
ctx.beginPath();
ctx.curve(pts);
ctx.stroke();
Now it is simply a matter of moving the points around (in particular the center point) and the curve will adopt. Adding a tension slider for the user can be an advantage:
Increasing the tension to for example 0.8 give you this result:
ctx.curve(pts, 0.8);
and lowering it to for example 0.3 will reduce the smoothness to this:
ctx.curve(pts, 0.3);
There are also other parameters (see the link at top for documentation) and you can have "unlimited" number of points in the point array in case you want to add super-fine control.
The implementation extends the canvas context but you can extract the method and use it separately if you are faint at heart. :-)
Implementing this for a circle
I hope I am interpreting your drawing correctly here... to use the above for a circle you would simply need to do the following:
Define the range, the side of the circle you want the lines to be drawn into.
Define the steps, ie. the space between each line.
Lets say you wanted to draw lines between -70° and 70° and maximum 5 lines you could do something like this:
var ctx = canvas.getContext('2d'),
cx = canvas.width * 0.5,
cy = canvas.height * 0.5,
pts,
startAngle = -70,
endAngle = 70,
lines = 5,
angle,
range,
steps,
radius = 90,
delta = 15,
x, y,
i;
ctx.lineWidth = 3;
ctx.strokeStyle = '#059';
/// draw arc
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, 2 * Math.PI);
ctx.stroke();
/// calculate angle range normalized to 0 degrees
startAngle = startAngle * Math.PI / 180;
endAngle = endAngle * Math.PI / 180;
range = endAngle - startAngle;
steps = range / (lines + 1);
/// calculate point at circle (vertical only)
for(i = 1; i <= lines; i++) {
pts = [];
/// right side
x = cx + radius * Math.cos(startAngle + steps * i);
y = cy + radius * Math.sin(startAngle + steps * i);
pts.push(x, y);
/// center
pts.push(cx, y + delta * ((y - cy)/ cy));
/// flip for left side
x = cx - (x - cx);
pts.push(x, y);
/// draw curve
ctx.beginPath();
ctx.curve(pts, 0.8);
ctx.stroke();
}
Which would result in this:
Fiddle here
It's now just a matter of playing around with the values (delta for example) and to calculate the horizontal row - I'll leave that as an exercise for the OP:
Using ellipse side to draw the lines
That being said - if you intended the globe to be more um, circular :-S, you could also have used a function to calculate part of an ellipse and draw that as lines instead. If would be about the same implementation as above here but with a sub function to calculate the ellipse between left and right side using the difference between the line and middle point as radius.
For example:
/// calculate point at circle (vertical only)
for(i = 1; i <= lines; i++) {
pts = [];
/// right side
x = cx + radius * Math.cos(startAngle + steps * i);
y = cy + radius * Math.sin(startAngle + steps * i);
pts.push(cx - radius, cy);
pts.push(cx, y);
pts.push(cx + radius, cy);
/// draw ellipse side
ctx.beginPath();
drawEllipseSide(pts, true);
ctx.stroke();
}
Then in the method (only vertical shown):
function drawEllipseSide(pts, horizontal) {
var radiusX,
radiusY,
cx, cy,
x, y,
startAngle,
endAngle,
steps = Math.PI * 0.01,
i = 0;
if (horizontal) {
radiusX = Math.abs(pts[4] - pts[0]) * 0.5;
radiusY = pts[3] - pts[1];
cx = pts[2];
cy = pts[1];
startAngle = 0;
endAngle = Math.PI;
x = cx + radiusX * Math.cos(startAngle);
y = cy + radiusY * Math.sin(startAngle);
ctx.moveTo(x, y);
for(i = startAngle + steps; i < endAngle; i += steps) {
x = cx + radiusX * Math.cos(i);
y = cy + radiusY * Math.sin(i);
ctx.lineTo(x, y);
}
}
}
Resulting in this (I cheat a bit in the final drawing to give a more clear picture (no pun) of what the end result will be if you continue down these lines (no pun either, I am pun-cursed) given here):
Fiddle here
My code-OCD kicked in :-P but you should at least have a few options here to go with. Study the code to see how the vertical lines are calculated and adopt that for horizontal ones.
Hope this helps!

Categories

Resources