程序生成形状
Program generative shape
我正在尝试编写一个圆圈,该圆圈本身由 12x30 个其他圆圈组成,这些圆圈相互接触(或非常接近)但从不相互重叠。这样一个圆圈的每一行应该代表一个月,每个圆圈代表一天。因此,我还需要完全控制每个生成的元素以进一步操纵它们……
基于此,我正在尝试编写类似于以下示例的程序。
我做得很粗糙,完全不知道如何编写代码才能执行一次并生成完整的 shape/generative 形状。
我想我应该检查圆圈之间的 minDistance,然后执行一些函数来绘制下一列?
// 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>
问题:创建一个由 30 个同心环组成的形状。每个环有 12 个相同大小的圆圈。
需要根据这些约束来选择同心环及其圆的半径:
给定一个环半径 r
,必须选择该环上 12 个圆的半径 s
,以便相邻的圆仅接触但不重叠。
给定圆环半径 r
,必须选择下一个较大的同心圆环 r'
的半径,以便两个圆环上的圆仅接触但不接触重叠。
插图:同心环和其上的圆以及构成十二边形的圆心连线用相同的颜色绘制:
我们知道十二边形的边角以 15° 为步长变化。如果我们然后将半径为 s
的圆放置在距离中心 r + s
的位置,我们可以使用公式 s = sin(15°) / (1 - sin(15°)) * r
来计算给定环的圆半径 s
半径 r
。参见例如https://www.illustrativemathematics.org/content-standards/tasks/710 几何解释。
两个环之间的距离等于其圆的直径2 * s
。
应用以上公式并预先计算所有涉及的因素得出:
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>
下面给出了一种可能的解决方案。这是一种蛮力解决方案,通过逐渐计算可以在主圆内的环中安装多少圆,如果找不到合适的,则使用逐渐变小的半径将圆拟合到主圆。
我用一年中的几天来展示这个过程。但是你只需要计算一次拟合半径。如果您知道正确的半径,则拟合代码是有效的。只有当你给它的半径太大时它才会减速。
很可能是一些计算方法可以让您很好地猜测起始半径,但由于您只做一小部分,所以我看不出深入的意义。 (但同样是一个有趣的问题。)
演示代码每100ms添加一个365天加上定位和渲染时间。每个月都有不同的色调,坐着的太阳更暗(主要是为了我自己的兴趣)。打包函数在名为reposition
的对象circles
中,需要时调用。剩下的只是支持 rendering/styles/testing
您添加定义要适合的圆的主圆,还包括边距(示例中为 3 像素),这是任何圆之间的最小间距。 (注意中间少于4个圆会接触)
请注意,您不必一次添加一个圈子。如果需要,它将一次完成所有操作。我没有对它进行超过 400 圈的测试,所以不知道它会走多远,也不知道随着圈数变高(10000 以上)它的表现如何
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>
我正在尝试编写一个圆圈,该圆圈本身由 12x30 个其他圆圈组成,这些圆圈相互接触(或非常接近)但从不相互重叠。这样一个圆圈的每一行应该代表一个月,每个圆圈代表一天。因此,我还需要完全控制每个生成的元素以进一步操纵它们……
基于此,我正在尝试编写类似于以下示例的程序。
我做得很粗糙,完全不知道如何编写代码才能执行一次并生成完整的 shape/generative 形状。
我想我应该检查圆圈之间的 minDistance,然后执行一些函数来绘制下一列?
// 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>
问题:创建一个由 30 个同心环组成的形状。每个环有 12 个相同大小的圆圈。
需要根据这些约束来选择同心环及其圆的半径:
给定一个环半径
r
,必须选择该环上 12 个圆的半径s
,以便相邻的圆仅接触但不重叠。给定圆环半径
r
,必须选择下一个较大的同心圆环r'
的半径,以便两个圆环上的圆仅接触但不接触重叠。
插图:同心环和其上的圆以及构成十二边形的圆心连线用相同的颜色绘制:
我们知道十二边形的边角以 15° 为步长变化。如果我们然后将半径为 s
的圆放置在距离中心 r + s
的位置,我们可以使用公式 s = sin(15°) / (1 - sin(15°)) * r
来计算给定环的圆半径 s
半径 r
。参见例如https://www.illustrativemathematics.org/content-standards/tasks/710 几何解释。
两个环之间的距离等于其圆的直径2 * s
。
应用以上公式并预先计算所有涉及的因素得出:
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>
下面给出了一种可能的解决方案。这是一种蛮力解决方案,通过逐渐计算可以在主圆内的环中安装多少圆,如果找不到合适的,则使用逐渐变小的半径将圆拟合到主圆。
我用一年中的几天来展示这个过程。但是你只需要计算一次拟合半径。如果您知道正确的半径,则拟合代码是有效的。只有当你给它的半径太大时它才会减速。
很可能是一些计算方法可以让您很好地猜测起始半径,但由于您只做一小部分,所以我看不出深入的意义。 (但同样是一个有趣的问题。)
演示代码每100ms添加一个365天加上定位和渲染时间。每个月都有不同的色调,坐着的太阳更暗(主要是为了我自己的兴趣)。打包函数在名为reposition
的对象circles
中,需要时调用。剩下的只是支持 rendering/styles/testing
您添加定义要适合的圆的主圆,还包括边距(示例中为 3 像素),这是任何圆之间的最小间距。 (注意中间少于4个圆会接触)
请注意,您不必一次添加一个圈子。如果需要,它将一次完成所有操作。我没有对它进行超过 400 圈的测试,所以不知道它会走多远,也不知道随着圈数变高(10000 以上)它的表现如何
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>