I have a pie chart in canvas and I wanted to plot random points in each sector of that pie.
I have got the area of each sector. using the arc sector
var arcsector = Math.PI * (2 * sector / total);
var startAngle = (lastend - offset) * (radius/Math.PI);
var endAngle = (lastend + arcsector - offset) * (radius/Math.PI);
var sectorAngle = arcsector * (radius/Math.PI);
var sectorArea = .5 * (sectorAngle*Math.PI/180) * (radius*radius);
How can I randomly plot points within that area?
A pie is a part of a circle, which, with your notations, starts at startAngle and ends at endAngle.
Most simple way to get a random point is to build a random angle (between
startAngle and endAngle) and a random radius, then you have your point with those lines :
var randAngle = startAngle + Math.random()*( endAngle - startAngle );
var randRadius = Math.random()*radius;
var randX = centerX + randRadius * Math.cos(randAngle);
var randY = centerY + randRadius * Math.sin(randAngle);
ctx.fillRect ( randX, randY, 1, 1 ) ;
repeat the number of times required !
The simple approach is to:
Create a temporary arc shape on path
Create a random point
Hit-test the point against the shape and plot if inside
You can create a temporary arc path by doing something like this (adjust to match your situation) (and no need to stroke/fill):
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, radius, startAngle, endAngle);
ctx.closePath();
Then create random points within the bounds of that arc, or just use a very basic approach (which is probably fast enough in most case unless you would need a lot of points) - and the spread is even compared to using a radius based approach:
var randomX = cx + radius * 2 * Math.random() - radius;
var randomY = cy + radius * 2 * Math.random() - radius;
and finally hit-test:
if (ctx.isPointInPath(randomX, randomY)) {
// plot point, count etc.
}
FIDDLE
Update
An even more efficient way to generate random points in the arc shape (and spread them more even) is to draw directly to an off-screen canvas without using any bound checking and no cos/sin operations, which are expensive, and finally composite that on top of your arc shape (or use arc as clip).
// create off-screen canvas
var ocanvas = document.createElement('canvas');
var octx = ocanvas.getContext('2d');
var d;
d = ocanvas.width = ocanvas.height = 300;
octx.fillStyle = '#fff';
while(count) {
var randomX = d * Math.random();
var randomY = d * Math.random();
octx.fillRect(randomX - 1, randomY - 1, 2, 2);
count--;
}
// composite random points with main arc
ctx.globalCompositeOperation = 'source-atop';
ctx.drawImage(ocanvas, 0, 0);
ctx.globalCompositeOperation = 'source-over';
It can be optimized further by having the off-screen canvas represent only the bounds of the arc shape.
FIDDLE
Demo: http://jsfiddle.net/jv6nP/3/
it's not perfect that points are at border and thus their radius being bigger than zero makes them overlap onto other parts of pie. And this also results in them going over black border.
var can = $('#can')[0].getContext('2d'),
border=2,
x=100,
y=75,
r=60,
sRadius= 0,
leadAngle=null,
points= [],
dotRadius=2,
data = {
water:[30,'#5CC5FA'],
earth:[60,'#F0A71F'],
air:[10,'#26EDE3']
};
function reDraw(){
//making border...
can.beginPath();
can.arc(x,y,r+border,0,2*Math.PI);
can.fillStyle='black';
can.fill();
var newAngle=null;
for (var k in data) { //making piechart..
leadAngle = (2*Math.PI)*(data[k][0]/100);
newAngle = sRadius+leadAngle;
calPoints(sRadius,leadAngle,k);
can.beginPath();
can.arc(x,y,r,sRadius,newAngle);
can.lineTo(x,y);
can.fillStyle=data[k][1];
can.fill();
sRadius= newAngle;
}
//calculating points..
function calPoints(s,e,name) {
if (name!='water') return;
var py,px,rAngle,rRad;
for (var i=0; i<15; i++) {
rAngle=s+Math.random()*(e);
rRad = Math.random()*r;
px = (Math.cos(rAngle) * rRad)+x;
py = (Math.sin(rAngle) * rRad)+y;
points.push([px,py]);
}
}
//plotting dots from data...
points.forEach(function(v){
can.beginPath();
can.arc(v[0],v[1],dotRadius,0,2*Math.PI);
can.fillStyle='fff';
can.fill();
});
points=[];
requestAnimationFrame(reDraw);
}
reDraw();
Related
I want draw something like this in javascript:
Example:
The bigger circle: r = 5
What do I want to do? to position circles by forming a circle
My questions are (I would like to know):
How much points can I draw on a circle if I know the ray,
the distance between each circle and the radius of each circle?
How can I find the position of each circle (to draw automatically)?
Like the question 1 but the circles do not have the same radius.
Thank you!
Displace points around a circle edge equidistantly
Using HTML Canvas and a bit of trigonometry
Create a reusable circles() function and pass the desired arguments for Number of circles, Size, Radius, Color:
const ctx = document.getElementById("canvas").getContext("2d");
const cvsSize = 400;
ctx.canvas.width = cvsSize;
ctx.canvas.height = cvsSize;
function circles(tot, rad, dist, color) {
const arc = Math.PI * 2 / tot; // Arc in Radians
let ang = 0; // Start at angle 0 (East)
for (let i = 0; i < tot; i++) {
const x = dist * Math.cos(ang) + (cvsSize / 2);
const y = dist * Math.sin(ang) + (cvsSize / 2);
ctx.beginPath();
ctx.arc(x, y, rad, 0, Math.PI * 2, false);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
ang += arc;
}
}
// Circles, Radius, Distance, Color
circles(3, 5, 10, "#f0b");
circles(10, 8, 50, "#0bf");
circles(17, 10, 90, "#bf0");
circles(21, 15, 140, "#b0f");
<canvas id="canvas"></canvas>
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
);
For instance, say I have the following path.
<canvas id="main" width="500" height="250"></canvas>
var canvas = document.getElementById("main");
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.moveTo(20,20);
ctx.lineTo(100,20);
ctx.arcTo(150,20,150,70,50);
ctx.lineTo(150,120);
ctx.lineWidth = 3;
ctx.stroke();
Is it possible to draw images on the arc of the line? If so, How?
Slice an image to draw on curves.
Yes it is possible, though ideally this would be a job for WebGL. The next best solution is a scan line render but that is way to much CPU load for poor Javascript to manage.
The next best I mean "OK sort of." option is a little image slicing.
You simply draw the image in thin slices around the arc. The 2D renderer is not perfect and tries to draw half pixels as best it can. The result is some noise along the edge of each slice where you can see through. To overcome this I draw each slice slightly wider to cover up any holes.
If you need high quality, rendering it all at double the size on an offscreen canvas and then scale down to a onscreen canvas (don't forget smoothing) will make most think it was drawn that way.
As the inner and outer edges of the arc have different circumferences some of the image must be squashed or stretched. In the demo I keep the inner edge of the image to the correct width and stretch the outer edge. It is easy to change but ensure that you use the outer edge to workout how many slices to draw.
WARNING the radius given is for the inner edge. It is vetted to stop the for loop getting too long and blocking the page. You may want to limit the radius so the inner circumference is the same as the image width. radius = radius < img.width / (Math.PI * 2) ? img.width / (Math.PI * 2) : radius;
It is easy to adapt to lines and curves. All you need is the tangent or curve normal (should be unit vector ie length 1) Use this vector to set the transform ctx.setTransform(nx,ny,tx,ty,px,py). THe first two values point out from the bottom of the image to the top, the next two numbers are along the tangent from left to right. The last two are the point on the curve to draw the slice.
// creates a blank image with 2d context
var createImage=function(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}
// create a canvas and add to dom
var can = createImage(512,512);
document.body.appendChild(can);
var ctx = can.ctx;
// create a image (canvas) to draw on the arc.
const textToDisplay = "<<Image on arc>>"
ctx.font = "64px arial";
var w = ctx.measureText(textToDisplay).width + 8;
var text = createImage(w + 64,84);
text.ctx.fillStyle = "#F90";
text.ctx.strokeStyle = "black";
text.ctx.lineWidth = 16;
text.ctx.fillRect(0,0,text.width,text.height);
text.ctx.strokeRect(0,0,text.width,text.height);
text.ctx.font = "64px arial";
text.ctx.fillStyle = "#0F0";
text.ctx.strokeStyle = "Black";
text.ctx.lineWidth = 4;
text.ctx.strokeText(textToDisplay,38,58);
text.ctx.fillText(textToDisplay,38,58);
// draws image on arc
// img image to render
// x,y center of arc
// radius the inner edge (bottom of image) radius
// fromAng The angle to start drawing the image in radians
// toAng (optional if not given image width will be used to get toAng)
// returns undefined
function drawArcImage(img,x,y,radius,fromAng,toAng){
// WARNING if you let the radius get to small the ratio between the inner and out circumference
// gets very large. This will result in the image being stretched over a quintabazzilon pixels.
// so must vet the radius or you will block the page and upset the browser gods.
radius = Math.abs(radius); // only positive
radius = radius < img.height / 8 ? img.height / 8 : radius;
var outRad = radius + img.height;
var cir = Math.PI * 2 * radius; // get inner circumference
if(toAng === undefined){
var toAng = (img.width / cir) * Math.PI * 2 ; // get the angle the image will cover
}
var cirOut = toAng * outRad; // get the out edge distance in pixels
var imgStep = img.width / cirOut; // the image step per slice
var imgX = 0; // track the image line to draw
var angStep = toAng / cirOut; // the angle steps
// For each pixel on the out edge draw a slice
for(var i = 0; i < toAng; i += angStep){
var dx = Math.cos(fromAng + i);
var dy = Math.sin(fromAng + i);
// set up the transform to draw a slice from the inner to outer edges
ctx.setTransform(dy,-dx,-dx,-dy,dx * radius + x,dy * radius + y);
// get and draw the slice. I stretch it a little (2pix) to cover imperfect rendering
ctx.drawImage(img,imgX,0,imgStep,img.height,-1,-img.height,2,img.height);
// move to next slice
imgX += imgStep;
}
ctx.setTransform(1,0,0,1,0,0); // reset the transform
}
// animate the image to prove it is real.. LOL
var animTick = 0;
var animRate = 0.01;
var pos = 0;
// update function call via RAF
function update(){
animTick += animRate; // update tick
// random anim sin waves.
var rad = Math.sin(animTick) * (256-text.height - 20) + 20;
pos += Math.sin(animTick*10) * 0.02;
pos += Math.sin(animTick/ 3) * 0.02;
pos += Math.sin(animTick/ 7) * 0.05;
// clear
ctx.clearRect(0,0,can.width,can.height)
// draw
drawArcImage(text,256,256,rad,pos)
// do again and again and again
requestAnimationFrame(update);
}
update();
This is an answer to a similar question:
You could, in the draw loop implement a "line drawing algorithm" that does not exactly draw a line but draws an item at a place where that point would be. Except, replace the line algorithm here to draw an arc instead.
function line(x0, y0, x1, y1){
var dx = Math.abs(x1-x0);
var dy = Math.abs(y1-y0);
var sx = (x0 < x1) ? 1 : -1;
var sy = (y0 < y1) ? 1 : -1;
var err = dx-dy;
while(true){ // put draw loop here.
drawImage(image,x0,y0);//setPixel(x0,y0); // Do what you need to for this
if ((x0==x1) && (y0==y1)) break;
var e2 = 2*err;
if (e2 >-dy){ err -= dy; x0 += sx; }
if (e2 < dx){ err += dx; y0 += sy; }
}
}
code taken from: Bresenham algorithm in Javascript
I would suggest using a library like p5.js to do something like this. http://p5js.org
Here is my question : I have an animation, that builds are circle. See : http://jsfiddle.net/2TUnE/
JavaScript:
var currentEndAngle = 0
var currentStartAngle = 0;
var currentColor = 'black';
setInterval(draw, 50);
function draw() { /***************/
var can = document.getElementById('canvas1'); // GET LE CANVAS
var canvas = document.getElementById("canvas1");
var context = canvas.getContext("2d");
var x = canvas.width / 2;
var y = canvas.height / 2;
var radius = 75;
var startAngle = currentStartAngle * Math.PI;
var endAngle = (currentEndAngle) * Math.PI;
currentEndAngle = currentEndAngle + 0.01;
var counterClockwise = false;
context.beginPath();
context.arc(x, y, radius, startAngle, endAngle, counterClockwise);
context.lineWidth = 15;
// line color
context.strokeStyle = currentColor;
context.stroke();
/************************************************/
}
When the circle is completely drawn, I would like it to start erasing, the same way it was created (so slowly removes the black). Once the whole circle is erased, i would create the black circle again, creating some kind of "waiting / loading" effect.
What i tried to do, is check if the currentEndAngle is 2 (so the circle is complete), and then move the startAngle, but it didn't work.
Any idea?
Thanks!
EDIT : Forgot to say, the animation is gonna be over an image, so it has to be "transparent" and not white
Look whats up in this JSFiddle: http://jsfiddle.net/fNTsA/
This method is basically your code, only we use a modulo to control state. Checking if the radius is 2 is only half-right, to toggle drawing white or drawing black you should do half the radius modulo 2. The first time around you have floor(0..2/2) % 2 == 0, the second you have floor(2..4/2) % 2 == 1, and so on.
Also because the line is antialiased, it helps to have the start angle overwrite what's been drawn already, otherwise you get extra white lines you probably don't want. For the same reason, when drawing the white circle, you should draw a slightly thicker line (smaller radius, thicker line). Otherwise the antialiasing leaves behind some schmutz -- a faint outline of the erased circle.
I put the radius and width into globals which you'd put at the top:
var lineRadius = 75;
var lineWidth = 15;
And likewise this is my modulo thing, pretty standard:
currentStartAngle = currentEndAngle - 0.01;
currentEndAngle = currentEndAngle + 0.01;
if (Math.floor(currentStartAngle / 2) % 2) {
currentColor = "white";
radius = lineRadius - 1;
width = lineWidth + 3;
} else {
currentColor = "black";
radius = lineRadius;
width = lineWidth;
}
Fun challenge! Try the following (updated fiddle here). I've tried to include plenty of comments to show my thinking.
// Moved these to global scope as you don't want to re-declare
// them in your draw method each time your animation loop runs
var canvas = document.getElementById("canvas1");
var context = canvas.getContext("2d");
var x = canvas.width / 2;
var y = canvas.height / 2;
var radius = 75;
// Use objects to hold our draw and erase props
var drawProps = {
startAngle: 0,
speed: 2,
color: 'black',
counterClockwise: false,
globalCompositeOperation: context.globalCompositeOperation,
lineWidth: 15
};
var eraseProps = {
startAngle: 360,
speed: -2,
color: 'white',
counterClockwise: true,
globalCompositeOperation: "destination-out",
lineWidth: 17 // artefacts appear unless we increase lineWidth for erase
};
// Let's work in degrees as they're easier for humans to understand
var degrees = 0;
var props = drawProps;
// start the animation loop
setInterval(draw, 50);
function draw() { /***************/
degrees += props.speed;
context.beginPath();
context.arc(
x,
y,
radius,
getRadians(props.startAngle),
getRadians(degrees),
props.counterClockwise
);
context.lineWidth = props.lineWidth;
context.strokeStyle = props.color;
context.stroke();
// Start erasing when we hit 360 degrees
if (degrees >= 360) {
context.closePath();
props = eraseProps;
context.globalCompositeOperation = props.globalCompositeOperation;
}
// Start drawing again when we get back to 0 degrees
if (degrees <= 0) {
canvas.width = canvas.width; // Clear the canvas for better performance (I think)
context.closePath();
props = drawProps;
context.globalCompositeOperation = props.globalCompositeOperation;
}
/************************************************/
}
// Helper method to convert degrees to radians
function getRadians(degrees) {
return degrees * (Math.PI / 180);
}
I have a quadratic curve that I use to create a slice of a piechart. The slice is situated in an axis of x and y, with the center point at (0,0). The radius is variable at radiusX and radiusY. This slice travels 90 degrees.
I need to split this slice into 3 seperate slices (each having 30 degree angle) and have them match whatever curve their parent slice had.
The following images show possible examples of the slice. Black circles adjust the size/shape of the slice:
Here is the function I've made but it's just not working correctly:
//globalPosX and globalPosY equal whatever position each of the two large black circles have repectively.
var canvas = document.getElementById('CV_slices');
var context = canvas.getContext('2d');
var cenX = canvas.width/2;
var cenY = canvas.height/2;
var blackDotX = globalPosX - cenX;
var blackDotY = cenY - globalPosY;
var endX;
var endY;
var controlX;
var controlY;
//set first slice
var startCoOrds = {
x: cenX ,
y: globalPosY
};
for (i=1; i < 4; i++) {
//make end(x,y) of previous slice the start(x,y) for the next slice.
endX = startCoOrds.x - (blackDotX*Math.sin(30));
endY = startCoOrds.y + (blackDotY*Math.cos(30));
//set position of control point using position of start/end positions (at the moment only adjustibng using +10 -10 at end)
controlX = ((endX - startCoOrds.x) /2) + (startCoOrds.x) + 10;
controlY = ((endY - startCoOrds.y) / 2) + (startCoOrds.y) - 10;
// draw slice
context.save();
context.beginPath();
context.moveTo(cenX, cenY);
context.lineTo(startCoOrds.x, startCoOrds.y);
context.quadraticCurveTo(controlX, controlY, endX, endY);
context.lineTo(cenX, cenY);
//make end(x,y) of previous slice the start(x,y) for the next slice
startCoOrds.x = endX;
startCoOrds.y = endY;
context.closePath();
context.globalAlpha = 0.1;
context.fillStyle = "#333333";
context.fill();
context.lineWidth = 2;
context.strokeStyle = "#ffffff";
context.stroke();
context.restore();
}
Use the closest "blackDot" as the radius of a circle,
using the circle, divide your quadrant into 3 (see wiki)
then scale the points by a ratio of the distance between 0,0 and the "blackDot"'s
In effect your arc is a quadrant of a circle that has been scaled in the x or y axis.