程序生成形状

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 个相同大小的圆圈。

需要根据这些约束来选择同心环及其圆的半径:

  1. 给定一个环半径 r,必须选择该环上 12 个圆的半径 s,以便相邻的圆仅接触但不重叠。

  2. 给定圆环半径 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>