Reputation: 525
I'm trying to program a circle that itself consists of 12x30 other circles that touch (or are really close) but never overlap each other. Each row of such a circle should represent a month and each circle a day. So additionally I need full control over each generated element to manipulate them further …
Based on that I'm trying to program something like in the example below.
I did it very rough and have absolutely no clue how I can write the code so that it executes once and generates the full shape/generative shape.
I guess I should check for a minDistance between the circles and then execute some function to draw the next column?
// window.addEventListener("mousemove", draw);
//
// var mouseX;
// var mouseY;
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var strokeWidth = 1;
var radius = 60;
var maxCircle = 12;
var size = 10
var maxCircle2 = 12;
var size2 = 20
var radius2 = 95;
var maxCircle3 = 12;
var size3 = 40
var radius3 = 160;
var maxCircle4 = 12;
var size4 = 65
var radius4 = 270;
ctx.translate(canvas.width/2, canvas.height/2);
//Draw January
for (var i = 0; i <= maxCircle; i++) {
ctx.beginPath();
ctx.arc(0, radius, size, -Math.PI/2, 2*Math.PI, false);
ctx.rotate(2*Math.PI/maxCircle);
ctx.stroke();
}
for (var i = 0; i <= maxCircle2; i++) {
ctx.beginPath();
ctx.arc(0, radius2, size2, -Math.PI/2, 2*Math.PI, false);
ctx.rotate(2*Math.PI/maxCircle2);
ctx.stroke();
}
for (var i = 0; i <= maxCircle3; i++) {
ctx.beginPath();
ctx.arc(0, radius3, size3, -Math.PI/2, 2*Math.PI, false);
ctx.rotate(2*Math.PI/maxCircle3);
ctx.stroke();
}
for (var i = 0; i <= maxCircle4; i++) {
ctx.beginPath();
ctx.arc(0, radius4, size4, -Math.PI/2, 2*Math.PI, false);
ctx.rotate(2*Math.PI/maxCircle4);
ctx.stroke();
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body style="background-color: #fff;">
<canvas id="canvas" width="800" height="500" style="border: 1px solid black;">
</canvas>
<script src="script.js"></script>
</body>
</html>
Upvotes: 1
Views: 92
Reputation: 20228
Problem: Create a shape made up of 30 concentric rings. Each ring bears 12 identically sized circles.
The radii of the concentric rings and their circles needs to be chosen according to these constraints:
Given a ring radius r
, the radius s
for the 12 circles on that ring has to be chosen so that neighboring circles just touch but don't overlap.
Given a ring radius r
, the radius of the next larger concentric ring r'
has to be chosen so that the circles on both rings just touch but don't overlap.
Illustration: Concentric rings and the circles on top of them as well as the connections between circle centers forming a dodecagon are drawn in the same color:
We know that side angles of a dodecagon change in 15° steps. If we then place the circles with radius s
at a distance of r + s
from the center, we can use the formula s = sin(15°) / (1 - sin(15°)) * r
to compute the circle radius s
for a given ring with radius r
. See e.g. https://www.illustrativemathematics.org/content-standards/tasks/710 for a geometrical explanation.
The distance between two rings equals the diameter 2 * s
of its circles.
Applying above formulas and precomputing all involved factors yields:
function drawRingsOfCircles(r) {
var RADIUS_FACTOR = 0.34919818620854987;
var ARC_START = -0.5 * Math.PI;
var ARC_END = 2 * Math.PI;
var ROTATE = Math.PI * 0.16666666666666666;
for (var i = 0; i < 30; i++) {
var s = RADIUS_FACTOR * r;
for (var j = 0; j < 12; j++) {
ctx.beginPath();
ctx.arc(0, r + s, s, ARC_START, ARC_END);
ctx.rotate(ROTATE);
ctx.stroke();
}
r = r + s + s;
}
}
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.translate(canvas.width * 0.5, canvas.height * 0.5);
drawRingsOfCircles(20);
<canvas id="canvas" width="800" height="500"></canvas>
Upvotes: 2
Reputation: 54026
One possible solution is given below. It is a brute force solution that fits circles to a master circle by progressively calculating how many circles can be fitted in rings inside the master circle using progressively smaller radius if a fit can not be found.
I have used days of the year and show the process. But you only need to calculate the fitting radius once. The fitting code is effective if you know the correct radius. It only slows down if the radius you give it is too large.
The is most likely some computational ways of giving you a good guess at a starting radius but as you are only doing a small set I could not see the point in going to deep. (But an interesting problem no less.)
Demo code adds 365 days one every 100ms plus positioning and rendering time. A different hue for each month and sat sun darker (mostly for my own interest). The packing function is in the object circles
called reposition
and is called as needed. The rest is just support for rendering/styles/testing
You add the master circle that defines the circle to fit, also includes a margin (3 pixels in the example) that is the min spacing between any circles. (note less than 4 circles in the center will touch)
Note you don't have to add the circles one at a time. It will do them all at once if needed. I have not tested it beyond 400 circles so have no clue how far it will go or how it performs as the circle count get high (10000 plus)
var ctx = canvas.getContext("2d");
const P = (x, y) => { return {x, y}}; // shorthand point creation function
// qEach is a fast callback itterator.
const qEach = (array,callback) =>{ for(var i = 0; i < array.length; i++){ callback(array[i], i) } };
const setStyle = (ctx,style) => {
if( style ){ qEach(Object.keys(style), key => {if(ctx[key]){ ctx[key] = style[key] }}) }
return style;
};
const styles = {
named : {},
add(name, style){ return this.named[name] = style },
addQ(name, strokeStyle, fillStyle, lineWidth){ return this.add(name, {strokeStyle, fillStyle, lineWidth})},
}
var circles = {
items : [],
dirty : true, // indicates that this object can not be drawn
masterCircle : null,
setFontStyle(fontStyle){ return this.fontStyle = fontStyle },
createCircle(text,style){
return {
radius : 0,
pos : P(0,0),
style,text,
};
},
createMaster(radius,pos,margin,style){
this.dirty = true;
this.masterCircle = {
pos,radius,style,margin,
};
this.circleRadius = radius / 4;
},
clean(){
if(this.dirty){
if(this.masterCircle === null){
throw new RangeError("No master circle");
}
this.reposition();
this.dirty = false;
}
},
nearest(point){
this.clean();
var minDist = Infinity;
var circle;
for(var i = 0; i < this.items.length; i ++){
var x = this.items[i].pos.x - point.x;
var y = this.items[i].pos.y - point.y;
var dist = Math.sqrt(x * x + y * y);
if(minDist > dist){
minDist = dist;
circle = this.items[i];
}
}
return circle;
},
draw(ctx){
this.clean();
setStyle(ctx,this.masterCircle.style);
ctx.beginPath();
ctx.arc(this.masterCircle.pos.x, this.masterCircle.pos.y, this.masterCircle.radius, 0, Math.PI * 2);
if(this.masterCircle.style.fillStyle) { ctx.fill() }
if(this.masterCircle.style.strokeStyle) { ctx.stroke() }
for(var i = 0; i < this.items.length; i ++){
var cir = this.items[i];
setStyle(ctx,cir.style);
ctx.beginPath();
ctx.arc(cir.pos.x, cir.pos.y, cir.radius, 0, Math.PI * 2);
if(cir.style.fillStyle) { ctx.fill() }
if(cir.style.strokeStyle) { ctx.stroke() }
}
if(this.fontStyle){
setStyle(ctx,this.fontStyle);
for(var i = 0; i < this.items.length; i ++){
var cir = this.items[i];
if(this.fontStyle.fillStyle){
ctx.fillText(cir.text,cir.pos.x, cir.pos.y);
}
if(this.fontStyle.strokeStyle){
ctx.strokeText(cir.text,cir.pos.x, cir.pos.y);
}
}
}
},
reposition(){
// set circles in position on a ring
const setRing = (count,start,end,cr) => {
var angStep = (Math.PI * 2) / count;
for(var i = start; i < end; i ++){
var cir = this.items[i];
cir.pos.x = Math.cos((i-start) * angStep) * cr + this.masterCircle.pos.x;
cir.pos.y = Math.sin((i-start) * angStep) * cr + this.masterCircle.pos.y;
cir.radius = R;
}
}
// get the number of items
var count = this.items.length;
if(count === 0){ return } // code below can not handle 0 as count
var positioned = 0; // number of circle that have been positioned
var r = this.masterCircle.radius; // radius
var m = this.masterCircle.margin; // margin
// get last circle radius (save some calculation steps)
// warning if you remove circles you need to reset circleRadius to a larger size
// or the best fit is found for the smaller radius leaving more of a hole in the middle
var R = this.circleRadius === undefined ? 45 : this.circleRadius;
var maxRingCount = 0; // counts number of rings so to guess at what size the next radius down
// should be if we can not fit the circles
var protect = 0; // I am not 100% confident this function will solve all problems
// This counter prevents any infinit looping
// keep trying to fit circles untill min radius (5) or all positioned or protect overflows
while(positioned < this.items.length && R > 5 && protect < 1300){
protect ++;
r = this.masterCircle.radius; // Get the outer radius
positioned = 0; // reset the number of circles positioned
count = this.items.length; // number of circles to position
var ringCount = 0; // counts the number of rings
while(positioned < this.items.length && r > R + m){ // add rings of circles until out of space
var cr = ((R + m) * count)/(Math.PI); // get the radius if we fit all circles at current R
if(cr + R + m > r){ // is this radius greater than the current radius
while(cr + R + m > r){ // yes decrease count untill we find a fit
count -= 1;
cr = ((R + m) * count)/(Math.PI);
}
}
if(count > 0){ // if we found the number of circle that can fit in a ring inside the radius
setRing(count,positioned,positioned + count ,r-m-R); // add the ring
positioned += count // count the positioned circles
count = this.items.length - positioned; // get the number of circle remaining
ringCount += 1; // count the ring
maxRingCount = Math.max(ringCount,maxRingCount) // keep the max ring count
}else{
break; // could not fit circles. exit this loop
}
r -= R * 2 + m;
}
if(positioned === this.items.length){ // have all circles been ppositions
this.circleRadius = R; // save the current radius that fits all circles
break; // all done the loop
}
R-= 3/maxRingCount; // could not fit all circles. Reduce the radius and try again.
}
},
add(circle){
this.dirty = true;
this.items.push(circle);
return circle;
}
}
//Test code adds 365 circles to the ring.
var hue = -210;
var hueStep = 210;
/*circles.setFontStyle(styles.add("font",{
font : "18px arial",
textAlign : "center",
textBaseline : "middle",
fillStyle : "black",
}));*/
var masterStyle = styles.addQ("Master","black","hsl(120,90%,90%)",2);
styles.addQ("Jan","black",`hsl(${hue = (hue + hueStep) % 360},90%,90%)`,2);
styles.addQ("Feb","black",`hsl(${hue = (hue + hueStep) % 360},90%,90%)`,2);
styles.addQ("Mar","black",`hsl(${hue = (hue + hueStep) % 360},90%,90%)`,2);
styles.addQ("Apr","black",`hsl(${hue = (hue + hueStep) % 360},90%,90%)`,2);
styles.addQ("May","black",`hsl(${hue = (hue + hueStep) % 360},90%,90%)`,2);
styles.addQ("Jun","black",`hsl(${hue = (hue + hueStep) % 360},90%,90%)`,2);
styles.addQ("Jul","black",`hsl(${hue = (hue + hueStep) % 360},90%,90%)`,2);
styles.addQ("Aug","black",`hsl(${hue = (hue + hueStep) % 360},90%,90%)`,2);
styles.addQ("Sep","black",`hsl(${hue = (hue + hueStep) % 360},90%,90%)`,2);
styles.addQ("Oct","black",`hsl(${hue = (hue + hueStep) % 360},90%,90%)`,2);
styles.addQ("Nov","black",`hsl(${hue = (hue + hueStep) % 360},90%,90%)`,2);
styles.addQ("Dec","black",`hsl(${hue = (hue + hueStep) % 360},90%,90%)`,2);
hue = -210;
styles.addQ("SatJan","black",`hsl(${hue = (hue + hueStep) % 360},90%,80%)`,2);
styles.addQ("SatFeb","black",`hsl(${hue = (hue + hueStep) % 360},90%,80%)`,2);
styles.addQ("SatMar","black",`hsl(${hue = (hue + hueStep) % 360},90%,80%)`,2);
styles.addQ("SatApr","black",`hsl(${hue = (hue + hueStep) % 360},90%,80%)`,2);
styles.addQ("SatMay","black",`hsl(${hue = (hue + hueStep) % 360},90%,80%)`,2);
styles.addQ("SatJun","black",`hsl(${hue = (hue + hueStep) % 360},90%,80%)`,2);
styles.addQ("SatJul","black",`hsl(${hue = (hue + hueStep) % 360},90%,80%)`,2);
styles.addQ("SatAug","black",`hsl(${hue = (hue + hueStep) % 360},90%,80%)`,2);
styles.addQ("SatSep","black",`hsl(${hue = (hue + hueStep) % 360},90%,80%)`,2);
styles.addQ("SatOct","black",`hsl(${hue = (hue + hueStep) % 360},90%,80%)`,2);
styles.addQ("SatNov","black",`hsl(${hue = (hue + hueStep) % 360},90%,80%)`,2);
styles.addQ("SatDec","black",`hsl(${hue = (hue + hueStep) % 360},90%,80%)`,2);
hue = -210;
styles.addQ("SunJan","black",`hsl(${hue = (hue + hueStep) % 360},90%,70%)`,2);
styles.addQ("SunFeb","black",`hsl(${hue = (hue + hueStep) % 360},90%,70%)`,2);
styles.addQ("SunMar","black",`hsl(${hue = (hue + hueStep) % 360},90%,70%)`,2);
styles.addQ("SunApr","black",`hsl(${hue = (hue + hueStep) % 360},90%,70%)`,2);
styles.addQ("SunMay","black",`hsl(${hue = (hue + hueStep) % 360},90%,70%)`,2);
styles.addQ("SunJun","black",`hsl(${hue = (hue + hueStep) % 360},90%,70%)`,2);
styles.addQ("SunJul","black",`hsl(${hue = (hue + hueStep) % 360},90%,70%)`,2);
styles.addQ("SunAug","black",`hsl(${hue = (hue + hueStep) % 360},90%,70%)`,2);
styles.addQ("SunSep","black",`hsl(${hue = (hue + hueStep) % 360},90%,70%)`,2);
styles.addQ("SunOct","black",`hsl(${hue = (hue + hueStep) % 360},90%,70%)`,2);
styles.addQ("SunNov","black",`hsl(${hue = (hue + hueStep) % 360},90%,70%)`,2);
styles.addQ("SunDec","black",`hsl(${hue = (hue + hueStep) % 360},90%,70%)`,2);
var months = ["Jan",31,"Feb",28,"Mar",31,"Apr",30,"May",31,"Jun",30,"Jul",31,"Aug",31,"Sep",30,"Oct",31,"Nov",30,"Dec",31];
circles.createMaster( Math.min(canvas.width, canvas.height) / 2 - 4, P(canvas.width / 2, canvas.height / 2),3, styles.named.Master);
circles.draw(ctx)
var count = 0;
var currentStyle;
var currentMonthDayCount = 0;
function addSome(){
count ++;
if(currentMonthDayCount === 0){
if(months.length === 0){
return;
}
currentStyle = months.shift();
currentMonthDayCount = months.shift();
}
currentMonthDayCount -= 1;
ctx.clearRect(0,0,canvas.width,canvas.height);
if(count %7 === 5){ // saturday style
circles.add(circles.createCircle(count,styles.named["Sat" + currentStyle]));
}else if(count %7 === 6){ // sunday style
circles.add(circles.createCircle(count,styles.named["Sun" + currentStyle]));
}else{
circles.add(circles.createCircle(count,styles.named[currentStyle]));
}
circles.draw(ctx)
setTimeout(addSome,100);
}
addSome();
<canvas id="canvas" width=1024 height=1024></canvas>
Upvotes: 0