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

3

11

First, just to give a visual idea of what I'm after, here's the closest result (yet not exactly what I'm after) image that I've found:

enter image description here

Here's the entire site-reference: http://www.mathematische-basteleien.de/spiral.htm

BUT, it doesn't exactly solve the problem I'm after. I would like to store an array of points of a very specific spiral algorithm.

  • The points are evenly distributed
  • The 360 degree cycles have an even gap

If I'm not mistaken, the first two points would be:

  • point[ 0 ] = new Point(0,0);
  • point[ 1 ] = new Point(1,0);

But where to go from here?

The only arguments I'd like to provide are:

  • the quantity of points I wish to resolve (length of array).
  • the distance between each points (pixels gap).
  • the distance between cycles.

It almost sounds, to me, that I have to calculate the "spiral-circumference" (if there's such a term) in order to plot the evenly distributed points along the spiral.

Can 2*PI*radius be reliably used for this calculation you think?

If it's been done before, please show some code example!

Generate answered 3/12, 2010 at 19:58 Comment(10)
By "Each points are evenly distributed", do you mean "The angle between consecutive points is a constant", or something else?Montpellier
This question is probably a better match for math.stackexchange.com .Kellum
By "Each 360degree cycles have an even gap", do you mean "The difference between the radius at angle x and the radius at angle x + 2*Pi is a constant", or something else?Montpellier
@Montpellier I mean evenly distributed distance between each point. The angle would probably vary as you approach the end of the spiral (since the angles wouldn't have to be as "steep" to yield to the next point). Does that make sense?Generate
@Jim Lewis - Sorry, you're probably right. It's intended to be used in Flash AS3, but yeah this is applicable to any language really.Generate
@bigp - Trying to nail down your definition of "evenly distributed". Do you mean the distance between consecutive points is constant?Montpellier
@Montpellier - Yes, I believe your statement is probably closer to what I meant. Basically, like an Archimedean Spiral - but with evenly-distributed plotted points. The way I can "picture it in my head"... the resolved points in each 2*PI cycles wouldn't necessarly line-up with the ones in it's previous cycles, since each increments of degrees (based on the origin 0,0) will likely diminish overtime (er... over the length).Generate
@Montpellier Yes. Constant distance between consecutive points. Suggest that I update the title / question?Generate
@Jim Lewis - Is there an easy way to move a question to another stackexchange topic?Generate
@bigp: flag your post for moderator attention and ask them to move your post.Wiggs
A
23

Fun little problem :)

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

spiral diagram

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:

spiral 1

spiral 2

spiral 3

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>
Adis answered 3/12, 2010 at 23:31 Comment(4)
Dude... I'm still looking at this and quite blown away it could be resolved with that few lines of code (at least to me it ain't a whole lot). That's exactly how I would of seperated the code too (drawing / generating points). This is fabulous! Wish I could vote up a few extra on your answer :DGenerate
Also... thanks for showing a very practical use of Point.polar - I never realized it could resolve such a long expression. So basically, in this case, is it used to "snap" the point along the "Archimedean" path?Generate
@bigp Glad I could help. Regarding Point.polar, "snap" might be way to look at it. I'd rather imagine moving about on circles, not to grids. Say imagine you're the handle of a watch and you want to move to 3 o'clock, or 12 o'clock...that would be your angle and how far from the center you want to go would be your radius. You easily draw circles, but if you increment the radius, then the circles stop overlapping and become a spiral. Imagine drawing circles with a compass/divider. Now imagine attempting to drawing a circle, but every now and then you increase the distance between handles.Adis
Excellent way to look at it. I'll consider using this in my next application of trigonometry (games, apps or components). Looks like a cleaner (and hopefully more performant) way of solving the X and Y components of a known angle and radius.Generate
H
3

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
         **/
    }   
}
Hygro answered 12/6, 2011 at 10:3 Comment(1)
Thanks for sharing that! Neat to see how it can be reproduced in other languages and different contexts :)Generate
L
0

interesting spiral.

Here is one made in C++/VCL (sorry I do not code in AS3) solution without goniometrics:

double i,x,y,dx,dy,ii,scale=20.0;
int xx,yy,rr=3;
bmp->Canvas->Pen->Color=clAqua;
bmp->Canvas->Brush->Color=clBlue;
xx=xs2; yy=ys2;
bmp->Canvas->Ellipse(xx-rr,yy-rr,xx+rr,yy+rr);
for (i=1,x=1.0,y=0.0;;i++)
    {
    // get screen coordinates
    xx=xs2+(scale*x);
    yy=ys2-(scale*y);
    // render actual segment of spiral
    if (i==1) bmp->Canvas->MoveTo(xx,yy);
     else     bmp->Canvas->LineTo(xx,yy);
    bmp->Canvas->Ellipse(xx-rr,yy-rr,xx+rr,yy+rr);
    // update position
    ii=1.0/sqrt(i);
    dx=-y*ii;
    dy=+x*ii;
    x+=dx;
    y+=dy;
    // stop if out of image area
    if ((xx>=xs)||(xx<=0)) break;
    if ((yy>=ys)||(xx<=0)) break;
    }

where bmp is output bitmap and xs,ys is its resolution and xs2,ys2 its half resolution (center). You can ignore the VCL stuff its not that important what you really need is just the MoveTo/LineTo.

The idea is simply take vector pointing to last known point (x,y) of spiral rotate it by 90 degrees (by swapping coordinates and negate one) and normalize it to unit size (dx,dy) and just add it to last known point to obtain next one. as we known the vector size sqrt(i) we can use directly that.

This method should be faster as it does not use any goniometrics however its iterative so rounding errors might be a problem for spirals with many points.

Here's a preview:

spiral

Landmark answered 29/9, 2023 at 6:23 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.