Algorithm to solve the points of a evenly-distributed / even-gaps spiral?

廉价感情. 提交于 2019-11-29 07:01:23

Fun little problem :)

If you look at the diagram closer, the sequence is clearly stated:

There are probably many solutions to drawing these, maybe more elegant, but here's mine:

You know the hypotenuse is square root of the current segment count+1 and the opposite side of the triangle is always 1.

Also you know that Sine(Math.sin) of the angle is equal to the opposite side divided by the hypotenuse. from the old mnenonic SOH(Sine,Opposite,Hypotenuse),-CAH-TOA.

Math.sin(angle) = opp/hyp

You know the value of the sine for the angle, you know the two sides, but you don't know the angle yet, but you can use the arc sine function(Math.asin) for that

angle = Math.asin(opp/hyp)

Now you know the angle for each segment, and notice it increments with each line.

Now that you have an angle and a radius(the hypotenuse) you can use for polar to cartesian formula to convert that angle,radius pair to a x,y pair.

x = Math.cos(angle) * radius;
y = Math.sin(angle) * radius;

Since you asked for an actionscript solution, there Point class already provides this function for you through the polar() method. You pass it a radius and angle and it returns your x and y in a Point object.

Here's a little snippet which plots the spiral. You can control the number of segments by moving the mouse on the Y axis.

var sw:Number = stage.stageWidth,sh:Number = stage.stageHeight;
this.addEventListener(Event.ENTER_FRAME,update);
function update(event:Event):void{
    drawTheodorus(144*(mouseY/sh),sw*.5,sh*.5,20);
}
//draw points
function drawTheodorus(segments:int,x:Number,y:Number,scale:Number):void{
    graphics.clear();
    var points:Array = getTheodorus(segments,scale);
    for(var i:int = 0 ; i < segments; i++){
        points[i].offset(x,y);
        graphics.lineStyle(1,0x990000,1.05-(.05+i/segments));
        graphics.moveTo(x,y);//move to centre
        graphics.lineTo(points[i].x,points[i].y);//draw hypotenuse
        graphics.lineStyle(1+(i*(i/segments)*.05),0,(.05+i/segments));
        if(i > 0) graphics.lineTo(points[i-1].x,points[i-1].y);//draw opposite
    }
}
//calculate points
function getTheodorus(segments:int = 1,scale:Number = 10):Array{
    var result = [];
    var radius:Number = 0;
    var angle:Number = 0;
    for(var i:int = 0 ; i < segments ; i++){
        radius = Math.sqrt(i+1);
        angle += Math.asin(1/radius);//sin(angle) = opposite/hypothenuse => used asin to get angle
        result[i] = Point.polar(radius*scale,angle);//same as new Point(Math.cos(angle)*radius.scale,Math.sin(angle)*radius.scale)
    }
    return result;
}

This could've been written in less lines, but I wanted to split this into two functions: one that deals only with computing the numbers, and the other which deals with drawing the lines.

Here are some screenshots:

For fun I added a version of this using ProcessingJS here. Runs a bit slow, so I would recommend Chromium/Chrome for this.

Now you can actually run this code right here (move the mouse up and down):

var totalSegments = 850,hw = 320,hh = 240,segments;
var len = 10;
points = [];
function setup(){
  createCanvas(640,480);
  smooth();
  colorMode(HSB,255,100,100);
  stroke(0);
  noFill();
  //println("move cursor vertically");
}
function draw(){
  background(0);
  translate(hw,hh);
  segments = floor(totalSegments*(mouseY/height));
  points = getTheodorus(segments,len);
  for(var i = 0 ; i < segments ; i++){
    strokeWeight(1);
    stroke(255-((i/segments) * 255),100,100,260-((i/segments) * 255));
    line(0,0,points[i].x,points[i].y);
    // strokeWeight(1+(i*(i/segments)*.01));
    strokeWeight(2);
    stroke(0,0,100,(20+i/segments));
    if(i > 0) line(points[i].x,points[i].y,points[i-1].x,points[i-1].y);
  }
}
function getTheodorus(segments,len){
  var result = [];
  var radius = 0;
  var angle = 0;
  for(var i = 0 ; i < segments ; i++){
    radius = sqrt(i+1);
    angle += asin(1/radius);
    result[i] = new p5.Vector(cos(angle) * radius*len,sin(angle) * radius*len);
  }
  return result;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.4.4/p5.min.js"></script>

George's answer was excellent! I was looking for the solution for quite a while.

Here's the same code adjusted for PHP, in case it helps someone. I use the script to draw dots (= cities) for a map with X, Y coordinates. X starts from left, Y starts from bottom left.

<?
/**
 * Initialize variables
 **/

// MAXIMUM width & height of canvas (X: 0->400, Y: 0->400)
$width = 400;

// For loop iteration amount, adjust this manually
$segments = 10000;

// Scale for radius
$radiusScale = 2;

// Draw dot (e.g. a city in a game) for every N'th drawn point
$cityForEveryNthDot = 14; 

/**
 * Private variables
 **/
$radius = 0;
$angle = 0;
$centerPoint = $width/2;

/**
 * Container print
 **/
print("<div style=\"width: ${width}px; height: ${width}px; background: #cdcdcd; z-index: 1; position: absolute; left: 0; top: 0;\"></div>");

/**
 * Looper
 **/
for($i=0;$i<$segments;$i++) {
    // calculate radius and angle
    $radius = sqrt($i+1) * $radiusScale;
    $angle += asin(1/$radius);

    // skip this point, if city won't be created here
    if($i % $cityForEveryNthDot != 0) {
        continue;
    }   

    // calculate X & Y (from top left) for this point
    $x = cos($angle) * $radius;
    $y = sin($angle) * $radius;

    // print dot
    print("<div style=\"width: 1px; height: 1px; background: black; position: absolute; z-index: 2; left: " . round($x+$centerPoint) . "; top: " . round($y+$centerPoint) . ";\"></div>");

    // calculate rounded X & Y (from bottom left)
    $xNew = round($x+$centerPoint);
    $yNew = round($width - ($y+$centerPoint));

    // just some internal checks
    if($xNew > 1 && $yNew > 1 && $xNew < $width && $yNew < $width) {
        /**
         * do something (e.g. store to database). Use xNew and yNew
         **/
    }   
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!