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

StackOverflow https://stackoverflow.com/questions/22779068

Вопрос

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();
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top