HTML5 画布斐波那契螺旋

Posted

技术标签:

【中文标题】HTML5 画布斐波那契螺旋【英文标题】:HTML5 Canvas Fibonacci Spiral 【发布时间】:2016-01-18 20:20:19 【问题描述】:

目前我正在查看这段代码,但不知道出了什么问题。

 function fibNumbers() 
    return [0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]


function continiusFib(a) 
    var b = fibNumbers(),
    c = Math.floor(a),
    d = Math.ceil(a);
    if (d >= b.length)
        return null;
    a = Math.pow(a - c, 1.15);
    return b[c] + (b[d] - b[c]) * a


function drawSpiral(pointA, pointB) 
    var b = pointA;
    var c = pointB;
    ctx.translate(b.x, b.y);
    b = Math.sqrt(((c.x - b.x) * (c.x - b.x)) + ((c.y - b.y) * (c.y - b.y)));
    d = 1 / Math.sqrt(((c.x - b.x) * (c.x - b.x)) + ((c.y - b.y) * (c.y - b.y)));
    c = Math.acos(c.x - b.x);
    0 > Math.asin(c.y - b.y) && (c = 2 * Math.PI - c);
    ctx.rotate(c);
    ctx.scale(b / 5, b / 5);
    var d = Math.PI / 100;
    ctx.moveTo(0, 0);
    for (var e = 0; e < 50 * (fibNumbers().length - 1) ; e++) 
        var f = e * d, g = continiusFib(e / 50),
        h = Math.cos(f) * g,
        f = Math.sin(f) * g;
        ctx.lineTo(h, f);
    
    ctx.scale(5 / b, 5 / b);
    ctx.rotate(-c);
    //ctx.stroke();

我想要画出不同于黄金螺旋的斐波那契螺旋

我也有这个question 供其他参考。

【问题讨论】:

为什么不使用生成器函数来生成术语? 类似jsfiddle.net/kf6aevsw/2 的东西(我在两分钟内完成了它可能无法正常工作,但请检查此developer.mozilla.org/en-US/docs/Web/javascript/Reference/…* 现在,没关系 -> jsfiddle.net/kf6aevsw/3 【参考方案1】:

我就是这样做的。要做的事情是找到从点A到B的角度的螺旋半径,然后缩放螺旋以适应。

该函数在画布上以点 A 和相交点 B 为中心渲染螺旋。它使用ctx.setTransform 来定位螺旋线以适应约束,或者您可以只使用比例和中心偏移来变换螺旋点并保持默认的画布变换(以防您正在绘制其他东西);

注意事项

如果 pointB === pointA 则不绘制,因为没有解决方案。 如果 pointA 远离画布,则可能无法绘制(我没有 测试过)。 总是从中心向外拉。不考虑剪裁 除了在哪里停止之外,螺旋式上升。

代码也是如此。 (更新)

// Assume ctx is canvas 2D Context and ready to render to
var cx = ctx.canvas.width / 2;
var cy = ctx.canvas.height / 2;
var font = "Verdana";       // font for annotation
var fontSize = 12;          // font size for annotation
var angleWind = 0;
var lastAng;

function getScale() // gets the current transform scale
    // assumes transform is square. ie Y and X scale are equal and at right angles
    var a = ctx.currentTransform.a;  // get x vector from current trans
    var b = ctx.currentTransform.b;
    return Math.sqrt(a * a + b * b);  // work out the scale    


// Code is just a quicky to annotate line and aid visualising current problem
// Not meant for anything but this example. Only Tested on Chrome
// This is needed as the canvas text API can not handle text at very small scales
// so need to draw at unit scale over existing transformation
function annotateLine(pA, pB, text, colour, where)  
    var scale, size, ang, xdx, xdy, len, textStart, ox, oy;

    scale = getScale(); // get the current scale
    size = fontSize;  // get font size

    // use scale to create new origin at start of line
    ox = ctx.currentTransform.e + pA.x * scale ;
    oy = ctx.currentTransform.f + pA.y * scale;

    // get direction of the line
    ang = Math.atan2(pB.y - pA.y, pB.x - pA.x);
    xdx = Math.cos(ang); // get the new x vector for transform
    xdy = Math.sin(ang);

    // get the length of the new line to do annotation positioning
    len = Math.sqrt( Math.pow(pB.y - pA.y, 2) + Math.pow(pB.x - pA.x, 2) ) * scale;

    ctx.save();  // save current state

    //Set the unit scaled transform to render in
    ctx.setTransform(xdx, xdy, -xdy, xdx, ox, oy); 

    // set fint
    ctx.font= size + "px " + font;

    // set start pos
    textStart = 0;
    where = where.toLowerCase();  // Because I can never get the cap right
    if(where.indexOf("start") > -1)
        textStart = 0;  // redundent I know but done
    else
    if(where.indexOf("center") > -1 || where.indexOf("centre") > -1 ) // both spellings 
        // get the size of text and calculate where it should start to be centred
        textStart = (len - ctx.measureText(text).width) / 2;
    else
        textStart = (len - ctx.measureText(text).width);
    
    if(where.indexOf("below") > -1)  // check if below
        size = -size * 2;
    
    // draw the text
    ctx.fillStyle = colour;
    ctx.fillText(text, textStart,-size / 2);    

    ctx.restore(); // recall saved state




// Just draws a circle and should be self exlainatory 
function circle(pA, size, colour1, colour2)
    size = size * 1 / getScale();
    ctx.strokeStyle = colour1;
    ctx.fillStyle = colour2;
    ctx.beginPath();
    ctx.arc(pA.x, pA.y, size , 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();


function renderSpiral(pointA, pointB, turns)
    var dx, dy, rad, i, ang, cx, cy, dist, a, c, angleStep, numberTurns, nTFPB, scale, styles, pA, pB;
    // clear the canvas
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

    // spiral stuff
    c = 1.358456;   // constant See https://en.wikipedia.org/wiki/Golden_spiral
    angleStep = Math.PI/20;  // set the angular resultion for drawing
    numberTurns = 6;  // total half turns drawn

    nTFPB = 0;   //  numberOfTurnsForPointB is the number of turns to point
                     // B should be integer and describes the number off
                     // turns made befor reaching point B

    // get the ang from pointA to B
    ang = (Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) + Math.PI * 2) % (Math.PI *2 );

    // Check for winding. If the angle crosses 2PI boundary from last call
    // then wind up or wind down the number of turns made to get to current
    // solution.
    if(lastAng !== undefined)
        if(lastAng > Math.PI * 1.5 && ang < Math.PI * 0.5 )
            angleWind += 1;
        else
        if(lastAng < Math.PI * 0.5 && ang > Math.PI * 1.5 )
            if(angleWind > 0)
                angleWind -= 1;
            
        
    
    lastAng = ang;  // save last angle

    // Add the windings
    nTFPB += angleWind;

    // get the distance from A to B
    dist = Math.sqrt(Math.pow(pointB.y-pointA.y,2)+Math.pow((pointB.x)-pointA.x,2));
    if(dist === 0)
        return;  // this makes no sense so exit as nothing to draw
    
    // get the spiral radius at point B
    rad = Math.pow(c,ang + nTFPB * 2 * Math.PI); // spiral radius at point2

    // now just need to get the correct scale so the spiral fist to the
    // constraints required.
    scale = dist / rad;

    while(Math.pow(c,Math.PI*numberTurns)*scale < ctx.canvas.width)
        numberTurns += 2;
    

    // set the scale, and origin to centre
    ctx.setTransform(scale, 0, 0, scale, pointA.x, pointA.y);

    // make it look nice create some line styles
    styles = [
            colour:"black",
            width:6
        ,
            colour:"gold",
            width:5
        
    ];

    // Now draw the spiral. draw it for each style 
    styles.forEach( function(style) 
        ctx.strokeStyle = style.colour;
        ctx.lineWidth = style.width * ( 1 / scale); // because it is scaled invert the scale
                                                    // can calculate the width required
        // ready to draw                               
        ctx.beginPath();
        for( i = 0; i <= Math.PI *numberTurns; i+= angleStep)
            dx = Math.cos(i);  // get the vector for angle i
            dy = Math.sin(i);
            var rad = Math.pow(c, i);  // calculate the radius
            if(i === 0)                 
                ctx.moveTo(dx * rad , dy * rad );        // start at center
            else
                ctx.lineTo(dx * rad , dy * rad );  // add line
            
        
        ctx.stroke();  // draw it all
    );

    // first just draw the line A-B
    ctx.strokeStyle = "black";
    ctx.lineWidth = 2 * ( 1 / scale); // because it is scaled invert the scale
                                      // can calculate the width required

    // some code to help me work this out. Having hard time visualising solution                                      
    pA = x: 0, y: 0;                                      
    pB = x: 1, y: 0;                                      
    pB.x = ( pointB.x - pointA.x ) * ( 1 / scale );
    pB.y = ( pointB.y - pointA.y ) * ( 1 / scale );
    // ready to draw                               
    ctx.beginPath();
    ctx.moveTo( pA.x, pA.y );        // start at center
    ctx.lineTo( pB.x, pB.y );  // add line
    ctx.stroke();  // draw it all

    if(scale > 10)
        ctx.strokeStyle = "blue";
        ctx.lineWidth = 1 * ( 1 / scale); 
        ctx.beginPath();
        ctx.moveTo( 0, 0 );        // start at center
        ctx.lineTo( 1, 0 );  // add line
        ctx.stroke();  // draw it all
    

    annotateLine(pA, pB, "" + ((ang + angleWind * Math.PI * 2) / Math.PI).toFixed(2) + "π", "black", "centre");
    annotateLine(pA, pB, "" + rad.toFixed(2), "black", "centre below");

    if(scale > 10)
        annotateLine(x: 0, y: 0, x: 1, y: 0, "1 Unit", "blue", "centre");
    

    circle(pA, 5, "black", "white");
    circle(pB, 5, "black", "white");

    ctx.setTransform(1,0,0,1,0,0); // reset transform to default;


var centerMove = 0;
canvasMouseCallBack = function()
    centerMove += 0.0;
    renderSpiral(
        
            x:cx+Math.sin(centerMove)*100,
            y:cy+Math.cos(centerMove)*100
        ,
        x:mouse.x,y:mouse.y
    );
;

希望这会有所帮助。对多余的水果感到抱歉,但我必须对其进行测试,所以尽管我将其全部复制为答案。

我为那些希望看到它运行的人添加了一个小提琴。 PointA 是自动移动的(所以当你移动鼠标时看起来有点奇怪),因为我无法添加适当的界面。

更新: 我已经更新了答案并试图为更新的问题找到更好的解决方案。不幸的是,我无法匹配新的要求,尽管从我的分析中我发现这些要求提出了一个无法解决的问题。即当螺旋角接近零时,尺度(在解决方案中)接近无穷大,渐近线在 PI/4 附近,但因为这只是一个近似值,所以一切都变得毫无意义。 A 点和 B 点有一组位置无法拟合螺旋。这是我的解释,并不意味着没有解决方案,因为我没有提供证据。

Fiddle (updated)

【讨论】:

@Blindman67 我添加了新图像。图 1 和图 2。我在获取图 2 时遇到了困难。希望你能帮助我。提前致谢。 好的,我认为只需将nTFPB(参见代码 // numberOfTurnsForPointB)值更改为 0 即可,但这不起作用。只要我有一点空闲时间(在接下来的 24 小时内),我就会解决这个问题 我看过了,我认为你想要的(A点从(1,0)开始)在某个角度下没有解决方案。该角度在 0 度时与切线成 90 度,但正如我给出的近似值,这在给定的(我的)解决方案中是不准确的。很抱歉,我无法满足您的要求。我将更新我的答案并提供一个更接近您需要的新小提琴,但我没有时间投入更多。【参考方案2】:

在你的函数 drawSpiral 中,在第四行你这样做:

b = Math.sqrt(((c.x - b.x) * (c.x - b.x)) + ((c.y - b.y) * (c.y - b.y)));

所以,b 现在应该是一个标量,但是您尝试在下一行访问 b.xb.y,它们不再存在:

d = 1 / Math.sqrt(((c.x - b.x) * (c.x - b.x)) + ((c.y - b.y) * (c.y - b.y)));

第 6-7 行的 c 再次发生这种情况。这可能是您的代码无法正常工作的原因。


我试图让它与我自己的代码一起工作。我完全不确定数学,但我的算法基于你在问题上发布的 sn-p,使用了@Blindman67 答案中的一些鼠标跟踪代码。

螺旋

这是重要的部分。它返回一个带有螺旋点的数组(我使用另一个函数来实际渲染它们)。这个想法是使用您提供的连续斐波那契函数绘制螺旋线。它从 A 点开始并强制缩放,因此一圈的半径是 A 点和 B 点之间的距离。它还添加了角度偏移,因此一圈的角度是 A 点和 B 点之间的角度。

编辑地址评论:我将for 循环更改为while 循环,该循环继续绘制直到螺旋达到最大半径。我还更改了一些名称并添加了 cmets 以尝试使算法更清晰。

var getSpiral = function(pA, pB, maxRadius)
    // 1 step = 1/4 turn or 90º    
    var precision = 50; // Lines to draw in each 1/4 turn
    var stepB = 4; // Steps to get to point B

    var angleToPointB = getAngle(pA,pB); // Angle between pA and pB
    var distToPointB = getDistance(pA,pB); // Distance between pA and pB

    var fibonacci = new FibonacciGenerator();

    // Find scale so that the last point of the curve is at distance to pB
    var radiusB = fibonacci.getNumber(stepB);
    var scale = distToPointB / radiusB;

    // Find angle offset so that last point of the curve is at angle to pB
    var angleOffset = angleToPointB - stepB * Math.PI / 2;

    var path = [];    
    var i, step , radius, angle;

    // Start at the center
    i = step = radius = angle = 0;

    // Continue drawing until reaching maximum radius
    while (radius * scale <= maxRadius)

        path.push(
            x: scale * radius * Math.cos(angle + angleOffset) + pA.x,
            y: scale * radius * Math.sin(angle + angleOffset) + pA.y
        );

        i++; // Next point
        step = i / precision; // 1/4 turns at point    
        radius = fibonacci.getNumber(step); // Radius of Fibonacci spiral
        angle = step * Math.PI / 2; // Radians at point
        
    return path;
;

斐波那契数列

生成连续斐波那契数的代码基本上是你的,但我更改了一些名称以帮助我理解它。我还添加了一个生成器函数,因此它可以工作到任意数量:

var FibonacciGenerator = function()
    var thisFibonacci = this;

    // Start with 0 1 2... instead of the real sequence 0 1 1 2...
    thisFibonacci.array = [0, 1, 2];

    thisFibonacci.getDiscrete = function(n)

        // If the Fibonacci number is not in the array, calculate it
        while (n >= thisFibonacci.array.length)
            var length = thisFibonacci.array.length;
            var nextFibonacci = thisFibonacci.array[length - 1] + thisFibonacci.array[length - 2];
            thisFibonacci.array.push(nextFibonacci);
        

        return thisFibonacci.array[n];
    ;

    thisFibonacci.getNumber = function(n)
        var floor = Math.floor(n);
        var ceil = Math.ceil(n);

        if (Math.floor(n) == n)
            return thisFibonacci.getDiscrete(n);
        

        var a = Math.pow(n - floor, 1.15);

        var fibFloor = thisFibonacci.getDiscrete(floor);
        var fibCeil = thisFibonacci.getDiscrete(ceil);

        return fibFloor + a * (fibCeil - fibFloor);
    ;

    return thisFibonacci;
;

两点之间的距离和角度

为了使代码更清晰,我使用了几个辅助函数来处理 2D 点:

var getDistance = function(p1, p2)
    return Math.sqrt(Math.pow(p1.x-p2.x, 2) + Math.pow(p1.y-p2.y, 2));
;

var getAngle = function(p1, p2)
    return Math.atan2(p2.y-p1.y, p2.x-p1.x);
;

整个事情:JSFiddle 和 Updated-to-address-comment JSFiddle

【讨论】:

@Thomas 您只需要更改循环的结束条件。我编辑了我的答案,所以它会“无限期地”绘制(画布大小)

以上是关于HTML5 画布斐波那契螺旋的主要内容,如果未能解决你的问题,请参考以下文章

斐波那契数列的介绍?

斐波那契数列有啥规律

斐波那契数列的全部规律

菲波那契数列是啥

斐波那契数列

08《算法入门教程》递归算法之斐波那契数列