解决均匀分布/等间距螺旋点的算法?

11

首先,为了让您有一个直观的想法,这是我找到的最接近(但不完全符合)我要求的图像:

enter image description here

这是整个网站的参考:http://www.mathematische-basteleien.de/spiral.htm

但是,它并没有完全解决我的问题。我想存储一个非常特定螺旋算法的点数组。

  • 这些点均匀分布
  • 360度循环具有均匀间隔

如果我没弄错,前两个点应该是:

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

但从这里去哪里呢?

我想提供的唯一参数是:

  • 我希望解决的点的数量(数组长度)。
  • 每个点之间的距离(像素间隙)。
  • 周期之间的距离。

对我来说,它几乎听起来要计算“螺旋周长”(如果有这样的术语),以便沿着螺旋线绘制均匀分布的点。

你认为可以可靠地使用 2 * PI * 半径 进行此计算吗?

如果以前做过,请展示一些代码示例!


“每个点均匀分布”是指“相邻点之间的角度是恒定的”,还是其他什么意思? - mbeckish
这个问题可能更适合于math.stackexchange.com。 - Jim Lewis
“每个360度周期都有一个均匀的间隔”,您是指“在角度x处的半径与角度x + 2 * Pi处的半径之间的差值是恒定的”,还是其他什么? - mbeckish
@bigp - 试图明确你对“均匀分布”的定义。你是指连续点之间的距离是恒定的吗? - mbeckish
@mbeckish - 是的,我相信你的说法可能更接近我的意思。基本上,就像一个阿基米德螺旋线-但是具有均匀分布的绘制点。我可以在脑海中“想象出来”的方式是...每个2 * PI周期中的解决点不一定会与其先前周期中的点对齐,因为每个度数增量(基于原点0,0)随着时间的推移(嗯...随着长度)可能会减少。 - chamberlainpi
显示剩余5条评论
3个回答

22

有趣的小问题 :)

如果您仔细查看图表,序列就会清楚地显示出来:

spiral diagram

可能有许多解决方案可以绘制这些图形,也许更加简洁,但这是我的方法:

您知道斜边等于当前段数加1的平方根,并且三角形的对边始终为1。

此外,您知道角度的正弦(Math.sin)等于对边除以斜边。从旧的记忆法 SOH(正弦、对边、斜边),CAH-TOA 中可以得到。

Math.sin(angle) = opp/hyp

你已知一个角度的正弦值和两条边的长度,但还不知道这个角度本身,此时可以使用反正弦函数(Math.asin)来求解。

angle = Math.asin(opp/hyp)

现在你知道了每个线段的角度,并且注意到它随着每条线递增。

既然你有了一个角度和半径(斜边),你可以使用极坐标转换为直角坐标公式,将这个角度和半径对转换为x,y坐标对。

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

由于您要求使用 actionscript 解决问题,Point 类已经通过 polar() 方法为您提供了此功能。您可以传递半径和角度,它将在 Point 对象中返回 x 和 y 值。(点击此处查看文档)

以下是一个小片段,用于绘制螺旋线。您可以通过在 Y 轴上移动鼠标来控制线段的数量。

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;
}
这段代码可以用更少的行数编写,但我想将其拆分为两个函数: 一个仅处理计算数字的函数,另一个则处理绘制线条。
以下是一些屏幕截图: spiral 1 spiral 2 spiral 3 为了好玩,在这里添加了使用ProcessingJS的版本 (点击这里查看)。 运行速度有点慢,建议在Chromium / Chrome中运行。
现在你可以在此处运行此代码(将鼠标上下移动):
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>


哥们儿...我还在看这个,简直被这几行代码解决了(对我来说并不是很多)。这正是我分离代码的方法(绘制/生成点)。太棒了!希望我能为你的回答点赞: D - chamberlainpi
另外...感谢您展示了Point.polar的实际用途 - 我从未意识到它可以解决如此长的表达式。因此,在这种情况下,它是用于沿着“阿基米德”路径“捕捉”点吗? - chamberlainpi
1
@bigp 很高兴能帮到你。关于 Point.polar,"snap" 可能是一种看待它的方式。我更愿意想象在圆上移动,而不是在网格上移动。比如,想象你是手表的指针,你想要移动到3点钟或12点钟...那就是你的角度,你想要离中心有多远就是你的半径。你可以轻松地画出圆圈,但如果你增加半径,那么圆圈就不会重叠,变成了一个螺旋形。想象用圆规/分度器画圆。现在想象试图画一个圆,但每隔一段时间你就增加两个手柄之间的距离。 - George Profenza
这是一个很好的思路。我会考虑在三角函数(游戏、应用或组件)的下一个应用中使用它。看起来这是一种更干净(并且希望更高效)的解决已知角度和半径的X和Y分量的方法。 - chamberlainpi

3

George的回答非常好!我已经寻找解决方案很长时间了。

以下是针对PHP进行调整的相同代码,以防有人需要。我使用此脚本为具有X、Y坐标的地图绘制点(=城市)。X从左侧开始,Y从左下角开始。

<?
/**
 * 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
         **/
    }   
}

谢谢分享!很高兴看到它如何在其他语言和不同的情境中被复制 :) - chamberlainpi

0
有趣的螺旋。
这是一个用C++/VCL制作的解决方案(抱歉,我不会AS3编程),没有使用三角函数。
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;
    }

其中bmp是输出位图,xs,ys是其分辨率,xs2,ys2是其半分辨率(中心)。你可以忽略VCL的东西,真正需要的只是MoveTo/LineTo

这个想法很简单,只需取指向螺旋的最后已知点(x,y)的向量,将其旋转90度(通过交换坐标并取反一个坐标),并将其归一化为单位大小(dx,dy),然后将其添加到最后已知点以获得下一个点。由于我们知道向量大小为sqrt(i),我们可以直接使用它。

这种方法应该更快,因为它不使用任何三角函数,但由于其迭代性质,舍入误差可能会对具有许多点的螺旋造成问题。

以下是预览:

spiral


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接