pack in small circles into a bigger circle - javascript

I'm trying to figure out a way to pack in smaller circles into a bigger one , but for some reason smaller circles are not correctly aligned.
I believe it's the position calculation that is probably missing something.
Please see the code for more details:
var c_el = document.getElementById("myCanvas");
var ctx = c_el.getContext("2d");
var canvas_width = c_el.clientWidth;
var canvas_height = c_el.clientHeight;
var circle1 = {
r: 50, /// radius
pos: {
x: (canvas_width / 2),
y: (canvas_height / 2)
}
}
var circle2 = {
r: 5,
pos:{}
}
var c2h = circle2.r * 2; /// circle height ////
var c2w = c2h; //// circle width /////
var c1h = circle1.r * 2; /// circle height ////
var c1w = c1h; //// circle width /////
var max_circles2_H = c1h / c2h;
var r = circle1.r;
var d = circle1.r - 2; //// segement distance from center ////
drawCircle( circle1 );
for(var col = 1; col < max_circles2_H; col++){
var d = circle1.r - ( col * c2h); /// distance from center to segment ///
var c = 2 * (Math.sqrt((r*r) - (d * d))); //// circle's chord length
var max_circles2_W = c / c2w;
for(var row = 1; row < max_circles2_W; row++){
circle2.pos.x = (row * c2w) + (circle1.pos.x - circle1.r);
circle2.pos.y = (col * c2h) + (circle1.pos.y - circle1.r);
drawCircle(circle2);
}
}
function drawCircle( circle ){
ctx.beginPath();
ctx.arc(circle.pos.x, circle.pos.y, circle.r, 0, 2 * Math.PI);
ctx.stroke();
}
<canvas id="myCanvas" width="300" height="150">
Any help is greatly appreciated.

This is close but it's needs a little adjusting. You can see where x_offset is calculated and try some different math to tighten it up.
update
Added Math.ceil to max_circles2_W and it seems to fit.
var c_el = document.getElementById("myCanvas");
var ctx = c_el.getContext("2d");
var canvas_width = c_el.clientWidth;
var canvas_height = c_el.clientHeight;
var circle1 = {
r: 50, /// radius
pos: {
x: (canvas_width / 2),
y: (canvas_height / 2)
}
}
var circle2 = {
r: 5,
pos:{}
}
var c2h = circle2.r * 2; /// circle height ////
var c2w = c2h; //// circle width /////
var c1h = circle1.r * 2; /// circle height ////
var c1w = c1h; //// circle width /////
var max_circles2_H = c1h / c2h;
var r = circle1.r;
var d = circle1.r - 2; //// segement distance from center ////
drawCircle( circle1 );
for(var col = 1; col < max_circles2_H; col++){
var d = circle1.r - ( col * c2h); /// distance from center to segment ///
var c = 2 * (Math.sqrt((r*r) - (d * d))); //// circle's chord length
var max_circles2_W = Math.ceil(c / c2w);
// CALCULATE OFFSET HERE
var x_offset = Math.floor((c1w - (max_circles2_W * c2w)) / 2);
for(var row = 1; row < max_circles2_W; row++){
circle2.pos.x = (row * c2w) + (circle1.pos.x - circle1.r) + x_offset;
circle2.pos.y = (col * c2h) + (circle1.pos.y - circle1.r);
ctx.fillText(row, circle2.pos.x-3, circle2.pos.y+4);
drawCircle(circle2);
}
}
function drawCircle( circle ){
ctx.beginPath();
ctx.arc(circle.pos.x, circle.pos.y, circle.r, 0, 2 * Math.PI);
ctx.stroke();
}
<canvas id="myCanvas" width="300" height="150">

Related

Identify points in each quadrant of a canvas

How do you divide a canvas into four quadrants (top-left, bottom-left, top-right, bottom-right) and then identify points drawn at random in each quadrant?
Here is my not so futile attempt:
var myCanvas = document.getElementById("showCanvas");
var myContext = myCanvas.getContext("2d");
var xAxis = [];
var yAxis = [];
for (var i = 0; i < 4; i++) {
var x = Math.floor(Math.random() * 600);
var y = Math.floor(Math.random() * 350);
var r = 5;
xAxis.push(x);
yAxis.push(y);
myContext.arc(x, y, r, 0, 2 * Math.PI, true);
myContext.closePath();
myContext.fill();
}
var startingPoint = xAxis[0] + ", " + yAxis[0];
var endingPoint = xAxis[3] + ", " + yAxis[3];
var horizontalSlash = myCanvas.width / 2;
var verticalSlash = myCanvas.height / 2;
var remainder = horizontalSlash % verticalSlash;
<canvas id="showCanvas"></canvas>
And the result should show up in these tags:
<h3>The starting point of the line is: <label id="sumPoints">e.g. top, right</label></h3>
<h3>The end point of the line is: <label id="sumPoints">e.g. bottom, left</label></h3>
The suggestion to check the point's coordinates is correct...
Here is an example using a bit of math.
const canvas = document.getElementById("showCanvas");
const ctx = canvas.getContext("2d");
const names = [
["top left", "bottom left"],
["top right", "bottom right"]
]
var points = []
for (var i = 0; i < 4; i++) {
var x = Math.random() * canvas.width
var y = Math.random() * canvas.height
var quadrant = names[Math.floor(x / (canvas.width / 2))][Math.floor(y / (canvas.height / 2))]
points.push({ x, y, quadrant })
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.fillText(quadrant, x-8, y-8)
ctx.closePath();
ctx.fill();
}
console.log(points[0])
canvas {
border:2px dotted gray;
}
<canvas id="showCanvas" width=200 height=200></canvas>

Rotate a cube to be isometric

I'm following this rotating cube tutorial and I'm trying to rotate the cube to an isometric perspective (45 degrees, 30 degrees).
The problem is, I think, is that the rotateY and rotateX functions alter the original values such that the two red dots in the middle of the cube (visually) don't overlap. (If that makes any sense)
How can I rotate the cube on it's X and Y axis at the same time so the functions don't effect each other?
const canvas = document.getElementById('stage');
canvas.width = canvas.parentElement.clientWidth
canvas.height = canvas.parentElement.clientHeight
const context = canvas.getContext('2d');
context.translate(200,200)
var node0 = [-100, -100, -100];
var node1 = [-100, -100, 100];
var node2 = [-100, 100, -100];
var node3 = [-100, 100, 100];
var node4 = [ 100, -100, -100];
var node5 = [ 100, -100, 100];
var node6 = [ 100, 100, -100];
var node7 = [ 100, 100, 100];
var nodes = [node0, node1, node2, node3, node4, node5, node6, node7];
var edge0 = [0, 1];
var edge1 = [1, 3];
var edge2 = [3, 2];
var edge3 = [2, 0];
var edge4 = [4, 5];
var edge5 = [5, 7];
var edge6 = [7, 6];
var edge7 = [6, 4];
var edge8 = [0, 4];
var edge9 = [1, 5];
var edge10 = [2, 6];
var edge11 = [3, 7];
var edges = [edge0, edge1, edge2, edge3, edge4, edge5, edge6, edge7, edge8, edge9, edge10, edge11];
var draw = function(){
for (var e=0; e<edges.length; e++){
var n0 = edges[e][0]
var n1 = edges[e][1]
var node0 = nodes[n0];
var node1 = nodes[n1];
context.beginPath();
context.moveTo(node0[0],node0[1]);
context.lineTo(node1[0],node1[1]);
context.stroke();
}
//draw nodes
for (var n=0; n<nodes.length; n++){
var node = nodes[n];
context.beginPath();
context.arc(node[0], node[1], 3, 0, 2 * Math.PI, false);
context.fillStyle = 'red';
context.fill();
}
}
var rotateZ3D = function(theta){
var sin_t = Math.sin(theta);
var cos_t = Math.cos(theta);
for (var n=0; n< nodes.length; n++){
var node = nodes[n];
var x = node[0];
var y = node[1];
node[0] = x * cos_t - y * sin_t;
node[1] = y * cos_t + x * sin_t;
};
};
var rotateY3D = function(theta){
var sin_t = Math.sin(theta);
var cos_t = Math.cos(theta);
for (var n=0; n<nodes.length; n++){
var node = nodes[n];
var x = node[0];
var z = node[2];
node[0] = x * cos_t - z * sin_t;
node[2] = z * cos_t + x * sin_t;
}
};
var rotateX3D = function(theta){
var sin_t = Math.sin(theta);
var cos_t = Math.cos(theta);
for (var n = 0; n< nodes.length; n++){
var node = nodes[n];
var y = node[1];
var z = node[2];
node[1] = y * cos_t - z * sin_t;
node[2] = z * cos_t + y * sin_t;
}
}
rotateY3D(Math.PI/4);
rotateX3D(Math.PI/6);
draw();
#stage {
background-color: cyan;
}
<canvas id="stage" height='500px' width='500px'></canvas>
Edit: I should have included a picture to further explain what I'm trying to achieve. I have a room picture that is isometric (45°,30°) and I'm overlaying it with a canvas so that I can draw the cube on it. As you can see it's slightly off, and I think its the effect of two compounding rotations since each function alters the original node coordinates.
You want projection not rotation
Your problem is that you are trying to apply a projection but using a transformation matrix to do it.
The transformation matrix will keep the box true to its original shape, with each axis at 90 deg to the others.
You want to have one axis at 45deg and the other at 30deg. You can not do that with rotations alone.
Projection matrix
The basic 3 by 4 matrix represents 4 3D vectors. These vectors are the direction and scale of the x,y,z axis in 3D space and the 4th vector is the origin.
The projection matrix removes the z part converting coordinates to 2D space. The z part of each axis is 0.
As the isometric projection is parallel we can just create a matrix that sets the 2D axis directions on the canvas.
The axis
The xAxis at 45 deg
const xAxis = Math.PI * ( 1 / 4);
iso.x.set(Math.cos(xAxis), Math.sin(xAxis), 0);
The yAxis at 120 deg
const yAxis = Math.PI * ( 4 / 6);
iso.y.set(Math.cos(yAxis), Math.sin(yAxis), 0);
And also the z axis which is up the page
iso.z.set(0,-1,0);
The transformation
Then we just multiply each vertex coord by the appropriate axis
// m is the matrix (iso)
// a is vertex in
// b is vertex out
// m.o is origin (not used in this example
b.x = a.x * m.x.x + a.y * m.y.x + a.z * m.z.x + m.o.x;
b.y = a.x * m.x.y + a.y * m.y.y + a.z * m.z.y + m.o.y;
b.z = a.x * m.x.z + a.y * m.y.z + a.z * m.z.z + m.o.z;
// ^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^
// move x dist move y dist move z dist
// along x axis along y axis along y axis
// 45deg 120deg Up -90deg
An example of above code
I have laid out a very basic Matrix in the snippet for reference.
The snippet creates 3D object using your approx layout.
The transform needs a second object for the result
I also added a projectIso that takes the directions of x,y,z axis and the scale of the x,y,z axis and creates a projection matrix as outlined above.
So thus the above is done with
const mat = Mat().projectIso(
Math.PI * ( 1 / 4),
Math.PI * ( 4 / 6),
Math.PI * ( 3 / 2) // up
); // scales default to 1
const ctx = canvas.getContext('2d');
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
const V = (x,y,z) => ({x,y,z,set(x,y,z){this.x = x;this.y = y; this.z = z}});
const Mat = () => ( {
x : V(1,0,0),
y : V(0,1,0),
z : V(0,0,1),
o : V(0,0,0), // origin
ident(){
const m = this;
m.x.set(1,0,0);
m.y.set(0,1,0);
m.z.set(0,0,1);
m.o.set(0,0,0);
return m;
},
rotX(r) {
const m = this.ident();
m.y.set(0, Math.cos(r), Math.sin(r));
m.z.set(0, -Math.sin(r), Math.cos(r));
return m;
},
rotY(r) {
const m = this.ident();
m.x.set(Math.cos(r), 0, Math.sin(r));
m.z.set(-Math.sin(r), 0, Math.cos(r));
return m;
},
rotZ(r) {
const m = this.ident();
m.x.set(Math.cos(r), Math.sin(r), 0);
m.y.set(-Math.sin(r), Math.cos(r), 0);
return m;
},
projectIso(xAxis, yAxis, zAxis, xScale = 1, yScale = 1, zScale = 1) {
const m = this.ident();
iso.x.set(Math.cos(xAxis) * xScale, Math.sin(xAxis) * xScale, 0);
iso.y.set(Math.cos(yAxis) * yScale, Math.sin(yAxis) * yScale, 0);
iso.z.set(Math.cos(zAxis) * zScale, Math.sin(zAxis) * zScale, 0);
return m;
},
transform(obj, result){
const m = this;
const na = obj.nodes;
const nb = result.nodes;
var i = 0;
while(i < na.length){
const a = na[i];
const b = nb[i++];
b.x = a.x * m.x.x + a.y * m.y.x + a.z * m.z.x + m.o.x;
b.y = a.x * m.x.y + a.y * m.y.y + a.z * m.z.y + m.o.y;
b.z = a.x * m.x.z + a.y * m.y.z + a.z * m.z.z + m.o.z;
}
return result;
}
});
// create a box
const Box = (size = 35) =>( {
nodes: [
V(-size, -size, -size),
V(-size, -size, size),
V(-size, size, -size),
V(-size, size, size),
V(size, -size, -size),
V(size, -size, size),
V(size, size, -size),
V(size, size, size),
],
edges: [[0, 1],[1, 3],[3, 2],[2, 0],[4, 5],[5, 7],[7, 6],[6, 4],[0, 4],[1, 5],[2, 6],[3, 7]],
});
// draws a obj that has nodes, and edges
function draw(obj) {
ctx.fillStyle = 'red';
const edges = obj.edges;
const nodes = obj.nodes;
var i = 0;
ctx.beginPath();
while(i < edges.length){
var edge = edges[i++];
ctx.moveTo(nodes[edge[0]].x, nodes[edge[0]].y);
ctx.lineTo(nodes[edge[1]].x, nodes[edge[1]].y);
}
ctx.stroke();
i = 0;
ctx.beginPath();
while(i < nodes.length){
const x = nodes[i].x;
const y = nodes[i++].y;
ctx.moveTo(x+3,y);
ctx.arc(x,y, 3, 0, 2 * Math.PI, false);
}
ctx.fill();
}
// create boxes (box1 is the projected result)
var box = Box();
var box1 = Box();
var box2 = Box();
// create the projection matrix
var iso = Mat();
// angles for X, and Y axis
const xAxis = Math.PI * ( 1 / 4);
const yAxis = Math.PI * ( 4 / 6);
iso.x.set(Math.cos(xAxis), Math.sin(xAxis),0);
iso.y.set(Math.cos(yAxis), Math.sin(yAxis), 0);
// the direction of Z
iso.z.set(0, -1, 0);
// center rendering
ctx.setTransform(1,0,0,1,cw* 0.5,ch);
// transform and render
draw(iso.transform(box,box1));
iso.projectIso(Math.PI * ( 1 / 6), Math.PI * ( 5 / 6), -Math.PI * ( 1 / 2))
ctx.setTransform(1,0,0,1,cw* 1,ch);
draw(iso.transform(box,box1));
iso.rotY(Math.PI / 4);
iso.transform(box,box1);
iso.rotX(Math.atan(1/Math.SQRT2));
iso.transform(box1,box2);
ctx.setTransform(1,0,0,1,cw* 1.5,ch);
draw(box2);
<canvas id="canvas" height='200' width='500'></canvas>
I think the issue may be that the rotation about the x-axis of the room is not 30°. In isometric images there is often an angle of 30° between the sides of a cube and the horizontal. But in order to get this horizontal angle, the rotation around the x-axis should be about 35° (atan(1/sqrt(2))). See the overview in the Wikipedia article.
Having said that, sometimes in computer graphics, the angle between the sides of a cube and the horizontal is about 27° (atan(0.5)), since this produces neater rastered lines on a computer screen. In that case, the rotation around the x-axis is 30°. Check out this article for a lot more information about the different types of projection.

How to curve a texture by offsetting X Pixels

Refer to this fiddle:
// get canvas references (canvas=collar, canvas1=texture)
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var canvas1 = document.getElementById("canvas1");
var ctx1 = canvas1.getContext("2d");
// preload the texture and collar images before starting
var textureImg, collarImg;
var imageURLs = [];
var imagesOK = 0;
var imgs = [];
imageURLs.push("https://dl.dropboxusercontent.com/u/139992952/stackoverflow/checkered.png");
imageURLs.push("https://dl.dropboxusercontent.com/u/139992952/stackoverflow/collar.png");
loadAllImages();
function loadAllImages(callback) {
for (var i = 0; i < imageURLs.length; i++) {
var img = new Image();
img.crossOrigin = "anonymous";
imgs.push(img);
img.onload = function () {
imagesOK++;
if (imagesOK == imageURLs.length) {
textureImg = imgs[0];
collarImg = imgs[1];
start();
}
};
img.src = imageURLs[i];
}
}
function start() {
// set both canvas dimensions
canvas.width = collarImg.width;
canvas.height = collarImg.height + 5;
canvas1.width = textureImg.width;
canvas1.height = textureImg.height;
// draw the textureImg on canvas1
ctx1.drawImage(textureImg, 0, 0, canvas1.width, canvas1.height);
// curve the texture into a collar shaped curved
curveTexture(collarImg.width, collarImg.height);
// draw the collarImg on canvas
ctx.drawImage(collarImg, 0, 0);
// set compositing to source-atop
// any new drawing will ONLY fill existing non-transparent pixels
ctx.globalCompositeOperation = "source-atop";
// draw the curved texture from canvas1 onto the collar of canvas
// (the existing pixels are the collar, so only the collar is filled)
ctx.drawImage(canvas1, 0, 0);
}
function curveTexture(w, h) {
// define a quadratic curve that fits the collar bottom
// These values change if the collar image changes (+5,-32)
var x0 = 0;
var y0 = h + 5;
var cx = w / 2;
var cy = h - 32;
var x1 = w;
var y1 = h + 5;
// get a,b,c for quadratic equation
// equation is used to offset columns of texture pixels
// in the same shape as the collar
var Q = getQuadraticEquation(x0, y0, cx, cy, x1, y1);
// get the texture canvas pixel data
// 2 copies to avoid self-referencing
var imageData0 = ctx1.getImageData(0, 0, w, h);
var data0 = imageData0.data;
var imageData1 = ctx1.getImageData(0, 0, w, h);
var data1 = imageData1.data;
// loop thru each vertical column of pixels
// Offset the pixel column into the shape of the quad-curve
for (var y = 0; y < h; y++) {
for (var x = 0; x < w; x++) {
// the pixel to write
var n = ((w * y) + x) * 4;
// the vertical offset amount
var yy = parseInt(y + h - (Q.a * x * x + Q.b * x + Q.c));
// the offset pixel to read
var nn = ((w * yy) + x) * 4;
// offset this pixel by the quadCurve Y value (yy)
data0[n + 0] = data1[nn + 0];
data0[n + 1] = data1[nn + 1];
data0[n + 2] = data1[nn + 2];
data0[n + 3] = data1[nn + 3];
}
}
ctx1.putImageData(imageData0, 0, 0);
}
// Quadratic Curve: given x coordinate, find y coordinate
function getQuadraticY(x, Q) {
return (Q.a * x * x + Q.b * x + Q.c);
}
// Quadratic Curve:
// Given: start,control,end points
// Find: a,b,c in quadratic equation ( y=a*x*x+b*x+c )
function getQuadraticEquation(x0, y0, cx, cy, x2, y2) {
// need 1 more point on q-curve, so calc its midpoint XY
// Note: since T=0.5 therefore TT=(1-T)=0.5 also [so could simplify]
var T = 0.50;
var TT = 1 - T;
var x1 = TT * TT * x0 + 2 * TT * T * cx + T * T * x2;
var y1 = TT * TT * y0 + 2 * TT * T * cy + T * T * y2;
var A = ((y1 - y0) * (x0 - x2) + (y2 - y0) * (x1 - x0)) / ((x0 - x2) * (x1 * x1 - x0 * x0) + (x1 - x0) * (x2 * x2 - x0 * x0));
var B = ((y1 - y0) - A * (x1 * x1 - x0 * x0)) / (x1 - x0);
var C = y0 - A * x0 * x0 - B * x0;
return ({
a: A,
b: B,
c: C
});
}
body {
background-color: ivory;
padding:20px;
}
canvas {
border:1px solid red;
}
<h3>"Curve" a texture</h3>
<p>by offsetting Y pixels based on Q-curve</p>
<canvas id="canvas" width=300 height=300></canvas>
<p>The temporary texture canvas (canvas1)</p>
<canvas id="canvas1" width=300 height=300></canvas>
http://jsfiddle.net/m1erickson/hdXyk/
I want to convert that horizontal generated lines to vertical. I tries to change the values but unable to achieved it.
I think that "Curve" a texture by offsetting X pixels based on Q-curve might work for getting vertical lines. Please help me for this.
For more you can refer this link : How to fill pattern in canvas and curving along the shape?

How To Get Value Of Canvas From A Wheel

How To Get The Value Of A Canvas . I have wheel which is rotating on mouse over the wheel stops now i want to echo out the value on which it was stopped. It is printing the whole array . Not the one on which the wheel stop.
$("#canvas").mouseover(function(){
backup= ctx;
alert(myData);
ctx = null;
});
this is the fiddle: https://jsfiddle.net/z61n9ccx/3/
Here is the full code:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var cw = canvas.width;
var ch = canvas.height;
var PI2 = Math.PI * 2;
var myData = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
var cx = 150;
var cy = 150;
var radius = 150;
var wheel = document.createElement('canvas');
var wheelCtx = wheel.getContext('2d');
var indicator = document.createElement('canvas');
var indicatorCtx = indicator.getContext('2d');
var angle = PI2 - PI2 / 4;
var myColor = [];
for (var i = 0; i < myData.length; i++) {
myColor.push(randomColor());
}
makeWheel();
makeIndicator();
requestAnimationFrame(animate);
function makeWheel() {
wheel.width = wheel.height = radius * 2 + 2;
wheelCtx.lineWidth = 1;
wheelCtx.font = '40px Pacifico, cursive';
wheelCtx.textAlign = 'center';
wheelCtx.textBaseline = 'middle';
var cx = wheel.width / 2;
var cy = wheel.height / 2;
var sweepAngle = PI2 / myData.length;
var startAngle = 0;
for (var i = 0; i < myData.length; i++) {
// calc ending angle based on starting angle
var endAngle = startAngle + sweepAngle;
// draw the wedge
wheelCtx.beginPath();
wheelCtx.moveTo(cx, cy);
wheelCtx.arc(cx, cy, radius, startAngle, endAngle, false);
wheelCtx.closePath();
wheelCtx.fillStyle = myColor[i];
wheelCtx.strokeStyle = 'black';
wheelCtx.fill();
wheelCtx.stroke();
// draw the label
var midAngle = startAngle + (endAngle - startAngle) / 2;
var labelRadius = radius * .85;
var x = cx + (labelRadius) * Math.cos(midAngle);
var y = cy + (labelRadius) * Math.sin(midAngle);
wheelCtx.fillStyle = 'gold';
wheelCtx.fillText(myData[i], x, y);
wheelCtx.strokeText(myData[i], x, y);
// increment angle
startAngle += sweepAngle;
}
}
function makeIndicator() {
indicator.width = indicator.height = radius + radius / 10;
indicatorCtx.font = '40px Georgia';
indicatorCtx.textAlign = 'center';
indicatorCtx.textBaseline = 'middle';
indicatorCtx.fillStyle = 'skyblue';
indicatorCtx.strokeStyle = 'blue';
indicatorCtx.lineWidth = 1;
var cx = indicator.width / 2;
var cy = indicator.height / 2;
indicatorCtx.beginPath();
indicatorCtx.moveTo(cx - radius / 8, cy);
indicatorCtx.lineTo(cx, cy - indicator.height / 2);
indicatorCtx.lineTo(cx + radius / 8, cy);
indicatorCtx.closePath();
indicatorCtx.fillStyle = 'skyblue'
indicatorCtx.fill();
indicatorCtx.stroke();
indicatorCtx.beginPath();
indicatorCtx.arc(cx, cy, radius / 3, 0, PI2);
indicatorCtx.closePath();
indicatorCtx.fill();
indicatorCtx.stroke();
indicatorCtx.fillStyle = 'blue';
indicatorCtx.fillText('Prizes', cx, cy);
}
function animate(time) {
ctx.clearRect(0, 0, cw, ch);
ctx.translate(cw / 2, ch / 2);
ctx.rotate(angle);
ctx.drawImage(wheel, -wheel.width / 2, -wheel.height / 2);
ctx.rotate(-angle);
ctx.translate(-cw / 2, -ch / 2);
ctx.drawImage(indicator, cw / 2 - indicator.width / 2, ch / 2 - indicator.height / 2)
angle += PI2 / 360;
requestAnimationFrame(animate);
}
function randomColor() {
return ('#' + Math.floor(Math.random() * 16777215).toString(16));
}
var backup = null;
$("#canvas").mouseover(function() {
backup = ctx;
alert(myData);
ctx = null;
});
$("#canvas").mouseout(function() {
// backup= ctx;
ctx = backup;
animate();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<canvas id="canvas" width="600" height="600" style="background-color:#ffff">
</canvas>
I added a counter, and then use that as a index: https://jsfiddle.net/Twisty/L6nws9yz/2/
HTML
<canvas id="canvas" width="310" height="310" style="background-color:#ffff">
</canvas>
<div id="counterBox">
<label>Counter:</label>
<span></span>
</div>
<div id="countBox">
<label>Index:</label>
<span></span>
</div>
JS
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var cw = canvas.width;
var ch = canvas.height;
var PI2 = Math.PI * 2;
var myData = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
var cx = 150;
var cy = 150;
var radius = 150;
var wheel = document.createElement('canvas');
var wheelCtx = wheel.getContext('2d');
var indicator = document.createElement('canvas');
var indicatorCtx = indicator.getContext('2d');
var currentSelection = 12;
var counter = 360;
var angle = PI2 - PI2 / 4;
var myColor = [];
for (var i = 0; i < myData.length; i++) {
myColor.push(randomColor());
}
makeWheel();
makeIndicator();
requestAnimationFrame(animate);
function makeWheel() {
wheel.width = wheel.height = radius * 2 + 2;
wheelCtx.lineWidth = 1;
wheelCtx.font = '40px Pacifico, cursive';
wheelCtx.textAlign = 'center';
wheelCtx.textBaseline = 'middle';
var cx = wheel.width / 2;
var cy = wheel.height / 2;
var sweepAngle = PI2 / myData.length;
var startAngle = 0;
for (var i = 0; i < myData.length; i++) {
// calc ending angle based on starting angle
var endAngle = startAngle + sweepAngle;
// draw the wedge
wheelCtx.beginPath();
wheelCtx.moveTo(cx, cy);
wheelCtx.arc(cx, cy, radius, startAngle, endAngle, false);
wheelCtx.closePath();
wheelCtx.fillStyle = myColor[i];
wheelCtx.strokeStyle = 'black';
wheelCtx.fill();
wheelCtx.stroke();
// draw the label
var midAngle = startAngle + (endAngle - startAngle) / 2;
var labelRadius = radius * .85;
var x = cx + (labelRadius) * Math.cos(midAngle);
var y = cy + (labelRadius) * Math.sin(midAngle);
wheelCtx.fillStyle = 'gold';
wheelCtx.fillText(myData[i], x, y);
wheelCtx.strokeText(myData[i], x, y);
// increment angle
startAngle += sweepAngle;
}
}
function makeIndicator() {
indicator.width = indicator.height = radius + radius / 10;
indicatorCtx.font = '40px Georgia';
indicatorCtx.textAlign = 'center';
indicatorCtx.textBaseline = 'middle';
indicatorCtx.fillStyle = 'skyblue';
indicatorCtx.strokeStyle = 'blue';
indicatorCtx.lineWidth = 1;
var cx = indicator.width / 2;
var cy = indicator.height / 2;
indicatorCtx.beginPath();
indicatorCtx.moveTo(cx - radius / 8, cy);
indicatorCtx.lineTo(cx, cy - indicator.height / 2);
indicatorCtx.lineTo(cx + radius / 8, cy);
indicatorCtx.closePath();
indicatorCtx.fillStyle = 'skyblue'
indicatorCtx.fill();
indicatorCtx.stroke();
indicatorCtx.beginPath();
indicatorCtx.arc(cx, cy, radius / 3, 0, PI2);
indicatorCtx.closePath();
indicatorCtx.fill();
indicatorCtx.stroke();
indicatorCtx.fillStyle = 'blue';
indicatorCtx.fillText('Prizes', cx, cy);
}
var lastloop = new Date;
var thisloop = new Date;
var fps = 0;
function animate(time) {
ctx.clearRect(0, 0, cw, ch);
ctx.translate(cw / 2, ch / 2);
ctx.rotate(angle);
ctx.drawImage(wheel, -wheel.width / 2, -wheel.height / 2);
ctx.rotate(-angle);
ctx.translate(-cw / 2, -ch / 2);
ctx.drawImage(indicator, cw / 2 - indicator.width / 2, ch / 2 - indicator.height / 2)
angle += PI2 / 360;
thisloop = new Date;
fps = 1000 / (thisloop - lastloop);
lastloop = thisloop;
counter--;
if (counter < 1) {
counter = 360;
}
$("#counterBox span").html(counter);
var index = counter / 30;
$("#countBox span").html(Math.round(index));
//$("#fpsBox span").html(fps);
requestAnimationFrame(animate);
}
function randomColor() {
return ('#' + Math.floor(Math.random() * 16777215).toString(16));
}
var backup = null;
$("#canvas").mouseover(function() {
backup = ctx;
alert(myData[Math.round(counter / 30)-1]);
ctx = null;
});
$("#canvas").mouseout(function() {
// backup= ctx;
ctx = backup;
animate();
});
Counter is set to 360 and then each frame decreases it. Take that and divide by 30 (360 / 12), and you can count each wedge. I round up and now I have 0 - 11 count.
Update
I moved the Index into a global space. To make it more precise, I used the % operator like so:
counter--;
if (counter == 0) {
counter = 360;
}
$("#counterBox span").html(counter);
if (counter % 30 === 0) {
index--;
}
$("#countBox span").html(Math.round(index));
if (index === 0) {
index = 12;
}
When you mouse over, you get the selection:
$("#canvas").mouseover(function() {
backup = ctx;
alert(index);
ctx = null;
});
I wrapped everything in an IIFE so that there aren't any global variables.
Updated Example
It's important to note that the angle calculation is:
angle = degree * Math.PI / 180;
With that being said, you can calculate the current degree and normalize it using:
(angle * (180 / Math.PI)) % 360
I added a function called getValue which takes an angle parameter:
function getValue(angle) {
var degree = (angle * (180 / Math.PI)) % 360,
offsetIndex = (Math.floor(degree / sweepDegree) + offset) % myData.length,
normalizedIndex = Math.abs(offsetIndex - (myData.length - 1));
return myData[normalizedIndex];
}
It essentially calculates the current degree, normalizes it taking into account what the initial degree was when the animation was initialized (which is the offset). Then it divides the degree by the sweep degree, which is 30 in this case since there are 12 items (i.e., 360/12 === 30) and rounds down.
var sweepDegree = 360 / myData.length;
var offset = (360 - (angle * (180 / Math.PI)) % 360) / sweepDegree;
This should work for a varying number of array items. In other words, nothing is hardcoded for a set length of 12 items (like in your case), so it should work for any given number of items.
Then you can simply use the getValue function in the mouseover event listener:
Updated Example
$("#canvas").mouseover(function() {
// ...
alert(getValue(angle));
});
(function() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var cw = canvas.width;
var ch = canvas.height;
var PI2 = Math.PI * 2;
var myData = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
var cx = 150;
var cy = 150;
var radius = 150;
var wheel = document.createElement('canvas');
var wheelCtx = wheel.getContext('2d');
var indicator = document.createElement('canvas');
var indicatorCtx = indicator.getContext('2d');
var angle = PI2 - PI2 / 4;
var sweepDegree = 360 / myData.length;
var offset = (360 - (angle * (180 / Math.PI)) % 360) / sweepDegree;
var myColor = [];
for (var i = 0; i < myData.length; i++) {
myColor.push(randomColor());
}
makeWheel();
makeIndicator();
requestAnimationFrame(animate);
function makeWheel() {
wheel.width = wheel.height = radius * 2 + 2;
wheelCtx.lineWidth = 1;
wheelCtx.font = '40px Pacifico, cursive';
wheelCtx.textAlign = 'center';
wheelCtx.textBaseline = 'middle';
var cx = wheel.width / 2;
var cy = wheel.height / 2;
var sweepAngle = PI2 / myData.length;
var startAngle = 0;
for (var i = 0; i < myData.length; i++) {
// calc ending angle based on starting angle
var endAngle = startAngle + sweepAngle;
// draw the wedge
wheelCtx.beginPath();
wheelCtx.moveTo(cx, cy);
wheelCtx.arc(cx, cy, radius, startAngle, endAngle, false);
wheelCtx.closePath();
wheelCtx.fillStyle = myColor[i];
wheelCtx.strokeStyle = 'black';
wheelCtx.fill();
wheelCtx.stroke();
// draw the label
var midAngle = startAngle + (endAngle - startAngle) / 2;
var labelRadius = radius * .85;
var x = cx + (labelRadius) * Math.cos(midAngle);
var y = cy + (labelRadius) * Math.sin(midAngle);
wheelCtx.fillStyle = 'gold';
wheelCtx.fillText(myData[i], x, y);
wheelCtx.strokeText(myData[i], x, y);
// increment angle
startAngle += sweepAngle;
}
}
function makeIndicator() {
indicator.width = indicator.height = radius + radius / 10;
indicatorCtx.font = '40px Georgia';
indicatorCtx.textAlign = 'center';
indicatorCtx.textBaseline = 'middle';
indicatorCtx.fillStyle = 'skyblue';
indicatorCtx.strokeStyle = 'blue';
indicatorCtx.lineWidth = 1;
var cx = indicator.width / 2;
var cy = indicator.height / 2;
indicatorCtx.beginPath();
indicatorCtx.moveTo(cx - radius / 8, cy);
indicatorCtx.lineTo(cx, cy - indicator.height / 2);
indicatorCtx.lineTo(cx + radius / 8, cy);
indicatorCtx.closePath();
indicatorCtx.fillStyle = 'skyblue'
indicatorCtx.fill();
indicatorCtx.stroke();
indicatorCtx.beginPath();
indicatorCtx.arc(cx, cy, radius / 3, 0, PI2);
indicatorCtx.closePath();
indicatorCtx.fill();
indicatorCtx.stroke();
indicatorCtx.fillStyle = 'blue';
indicatorCtx.fillText('Prizes', cx, cy);
}
function animate(time) {
if (ctx === null) {
return
}
ctx.clearRect(0, 0, cw, ch);
ctx.translate(cw / 2, ch / 2);
ctx.rotate(angle);
ctx.drawImage(wheel, -wheel.width / 2, -wheel.height / 2);
ctx.rotate(-angle);
ctx.translate(-cw / 2, -ch / 2);
ctx.drawImage(indicator, cw / 2 - indicator.width / 2, ch / 2 - indicator.height / 2)
angle += PI2 / 360;
requestAnimationFrame(animate);
}
function randomColor() {
return ('#' + Math.floor(Math.random() * 16777215).toString(16));
}
var backup = null;
$("#canvas").mouseover(function() {
backup = ctx;
ctx = null;
alert(getValue(angle));
});
$("#canvas").mouseout(function() {
ctx = backup;
animate();
});
function getValue(angle) {
var degree = (angle * (180 / Math.PI)) % 360,
offsetIndex = (Math.floor(degree / sweepDegree) + offset) % myData.length,
normalizedIndex = Math.abs(offsetIndex - (myData.length - 1));
return myData[normalizedIndex];
}
})();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<canvas id="canvas" width="600" height="600" style="background-color:#ffff">
</canvas>

How do I draw x number of circles around a central circle, starting at the top of the center circle?

I'm trying to create a UI that has a lot of items in circles. Sometimes these circles will have related circles that should be displayed around them.
I was able to cobble together something that works, here.
The problem is that the outer circles start near 0 degrees, and I'd like them to start at an angle supplied by the consumer of the function/library. I was never a star at trigonometry, or geometry, so I could use a little help.
As you can see in the consuming code, there is a setting: startingDegree: 270 that the function getPosition should honor, but I haven't been able to figure out how.
Update 04/02/2014:
as I mentioned in my comment to Salix alba, I wasn't clear above, but what I needed was to be able to specify the radius of the satellite circles, and to have them go only partly all the way around. Salix gave a solution that calculates the size the satellites need to be to fit around the center circle uniformly.
Using some of the hints in Salix's answer, I was able to achieve the desired result... and have an extra "mode," thanks to Salix, in the future.
The working, though still rough, solution is here: http://jsfiddle.net/RD4RZ/11/. Here is the entire code (just so it's all on SO):
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<script type="text/javascript" src="//code.jquery.com/jquery-1.10.1.js"></script>
<style type="text/css">
.circle
{
position: absolute;
width: 100px;
height: 100px;
background-repeat: no-repeat;background-position: center center;
border: 80px solid #a19084;
border-radius: 50%;
-moz-border-radius: 50%;
}
.sm
{
border: 2px solid #a19084;
}
</style>
<script type="text/javascript">//<![CDATA[
$(function () {
function sind(x) {
return Math.sin(x * Math.PI / 180);
}
/*the law of cosines:
cc = aa + bb - 2ab cos(C), where c is the satellite diameter a and b are the legs
solving for cos C, cos C = ( aa + bb - cc ) / 2ab
Math.acos((a * a + b * b - c * c) / (2 * a * b)) = C
*/
function solveAngle(a, b, c) { // Returns angle C using law of cosines
var temp = (a * a + b * b - c * c) / (2 * a * b);
if (temp >= -1 && temp <= 1)
return radToDeg(Math.acos(temp));
else
throw "No solution";
}
function radToDeg(x) {
return x / Math.PI * 180;
}
function degToRad(x) {
return x * (Math.PI / 180);
}
var satellite = {
//settings must have: collection (array), itemDiameter (number), minCenterDiameter (number), center (json with x, y numbers)
//optional: itemPadding (number), evenDistribution (boolean), centerPadding (boolean), noOverLap (boolean)
getPosition: function (settings) {
//backwards compat
settings.centerPadding = settings.centerPadding || settings.itemPadding;
settings.noOverLap = typeof settings.noOverLap == 'undefined' ? true : settings.noOverLap;
settings.startingDegree = settings.startingDegree || 270;
settings.startSatellitesOnEdge = typeof settings.startSatellitesOnEdge == 'undefined' ? true : settings.startSatellitesOnEdge;
var itemIndex = $.inArray(settings.item, settings.collection);
var itemCnt = settings.collection.length;
var satelliteSide = settings.itemDiameter + (settings.itemSeparation || 0) + (settings.itemPadding || 0);
var evenDistribution = typeof settings.evenDistribution == 'undefined' ? true : settings.evenDistribution;
var degreeOfSeparation = (360 / itemCnt);
/*
we know all three sides:
one side is the diameter of the satellite itself (plus any padding). the other two
are the parent radius + the radius of the satellite itself (plus any padding).
given that, we need to find the angle of separation using the law of cosines (solveAngle)
*/
//if (!evenDistribution) {
var side1 = ((satelliteSide / 2)) + ((settings.minCenterDiameter + (2 * settings.centerPadding)) / 2);
var side2 = satelliteSide;;
var degreeOfSeparationBasedOnSatellite = solveAngle(side1, side1, side2); //Math.acos(((((side1 * side1) + (side2 * side2)) - (side2 * side2)) / (side2 * side2 * 2)) / 180 * Math.PI) * Math.PI;
degreeOfSeparation = evenDistribution? degreeOfSeparation: settings.noOverLap ? Math.min(degreeOfSeparation, degreeOfSeparationBasedOnSatellite) : degreeOfSeparationBasedOnSatellite;
//}
//angle-angle-side
//a-A-B
var a = satelliteSide;
var A = degreeOfSeparation;
/*
the three angles of any triangle add up to 180. We know one angle (degreeOfSeparation)
and we know the other two are equivalent to each other, so...
*/
var B = (180 - A) / 2;
//b is length necessary to fit all satellites, might be too short to be outside of base circle
var b = a * sind(B) / sind(A);
var offset = (settings.itemDiameter / 2) + (settings.itemPadding || 0); // 1; //
var onBaseCircleLegLength = ((settings.minCenterDiameter / 2) + settings.centerPadding) + offset;
var offBase = false;
if (b > onBaseCircleLegLength) {
offBase = true;
}
b = settings.noOverLap ? Math.max(b, onBaseCircleLegLength) : onBaseCircleLegLength;
var radianDegree = degToRad(degreeOfSeparation);
//log('b=' + b);
//log('settings.center.x=' + settings.center.x);
//log('settings.center.y=' + settings.center.y);
var degreeOffset = settings.startingDegree;
if (settings.startSatellitesOnEdge) {
degreeOffset += ((offBase ? degreeOfSeparation : degreeOfSeparationBasedOnSatellite) / 2);
}
var i = ((Math.PI * degreeOffset) / 180) + (radianDegree * itemIndex);// + (degToRad(degreeOfSeparationBasedOnSatellite) / 2); //(radianDegree) * (itemIndex);
var x = (Math.cos(i) * b) + (settings.center.x - offset);
var y = (Math.sin(i) * b) + (settings.center.y - offset);
return { 'x': Math.round(x), 'y': Math.round(y) };
}
,
/* if we ever want to size satellite by how many need to fit tight around the base circle:
x: function calcCircles(n) {
circles.splice(0); // clear out old circles
var angle = Math.PI / n;
var s = Math.sin(angle);
var r = baseRadius * s / (1 - s);
console.log(angle);
console.log(s);
console.log(r);
console.log(startAngle);
console.log(startAngle / (Math.PI * 2));
for (var i = 0; i < n; ++i) {
var phi = ((Math.PI * startAngle) / 180) + (angle * i * 2);
var cx = 150 + (baseRadius + r) * Math.cos(phi);
var cy = 150 + (baseRadius + r) * Math.sin(phi);
circles.push(new Circle(cx, cy, r));
}
},
*/
//settings must have: collection (array), itemDiameter (number), minCenterDiameter (number), center (json with x, y numbers)
//optional: itemPadding (number), evenDistribution (boolean), centerPadding (boolean), noOverLap (boolean)
getAllPositions: function (settings) {
var point;
var points = [];
var collection = settings.collection;
for (var i = 0; i < collection.length; i++) {
settings.item = collection[i]
points.push(satellite.getPosition(settings));
}
return points;
}
};
var el = $("#center"), cnt = 10, arr = [], itemDiameter= 100;
for (var c = 0; c < cnt; c++) {
arr.push(c);
}
var settings = {
collection: arr,
itemDiameter: itemDiameter,
minCenterDiameter: el.width(),
center: { x: el.width() / 2, y: el.width() / 2 },
itemPadding: 2,
evenDistribution: false,
centerPadding: parseInt(el.css("border-width")),
noOverLap: false,
startingDegree: 270
};
var points = satellite.getAllPositions(settings);
for (var i = 0; i < points.length; i++) {
var $newdiv1 = $("<div></div>");
var div = el.append($newdiv1);
$newdiv1.addClass("circle").addClass("sm");
$newdiv1.text(i);
$newdiv1.css({ left: points[i].x, top: points[i].y, width: itemDiameter +'px', height: itemDiameter +'px' });
}
});//]]>
</script>
</head>
<body>
<div id="center" class="circle" style="left:250px;top:250px" >
</div>
</body>
</html>
The central bit you need to work out is radius of the small circles. If you have R for radius of the central circle and you want to fit n smaller circles around it. Let the as yet unknown radius of the small circle be r. We can construct a right angle triangle with one corner in the center of the big circle one in the center of the small circle and one which is where a line from the center is tangent to the small circle. This will be a right angle. The angle at the center is a the hypotenuse has length R+r the opposite is r and we don't need the adjacent. Using trig
sin(a) = op / hyp = r / (R + r)
rearrange
(R+r) sin(a) = r
R sin(a) + r sin(a) = r
R sin(a) = r - r sin(a)
R sin(a) = (1 - sin(a)) r
r = R sin(a) / ( 1 - sin(a))
once we have r we are pretty much done.
You can see this as a fiddle http://jsfiddle.net/SalixAlba/7mAAS/
// canvas and mousedown related variables
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var $canvas = $("#canvas");
var canvasOffset = $canvas.offset();
var offsetX = canvasOffset.left;
var offsetY = canvasOffset.top;
var scrollX = $canvas.scrollLeft();
var scrollY = $canvas.scrollTop();
// save canvas size to vars b/ they're used often
var canvasWidth = canvas.width;
var canvasHeight = canvas.height;
var baseRadius = 50;
var baseCircle = new Circle(150,150,50);
var nCircles = 7;
var startAngle = 15.0;
function Circle(x,y,r) {
this.x = x;
this.y = y;
this.r = r;
}
Circle.prototype.draw = function() {
ctx.beginPath();
ctx.arc(this.x,this.y,this.r, 0, 2 * Math.PI, false);
ctx.stroke();
}
var circles = new Array();
function calcCircles(n) {
circles.splice(0); // clear out old circles
var angle = Math.PI / n;
var s = Math.sin(angle);
var r = baseRadius * s / (1-s);
console.log(angle);
console.log(s);
console.log(r);
for(var i=0;i<n;++i) {
var phi = startAngle + angle * i * 2;
var cx = 150+(baseRadius + r) * Math.cos(phi);
var cy = 150+(baseRadius + r) * Math.sin(phi);
circles.push(new Circle(cx,cy,r));
}
}
function draw() {
baseCircle.draw();
circles.forEach(function(ele){ele.draw()});
}
calcCircles(7);
draw();

Categories

Resources