ajbee
ajbee

Reputation: 3641

HTML5 Canvas Fibonacci Spiral

Currently I'm looking at this code but can't figure out what's wrong.

 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();
}

What I want is to draw Fibonacci Spiral which is different from Golden Spiral

I also have this question for other reference. enter image description here

enter image description here

Upvotes: 18

Views: 5893

Answers (2)

&#193;ngela
&#193;ngela

Reputation: 1425

In your function drawSpiral, in the fourth line you do:

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

So, b should be a scalar now, but then you try to access b.x and b.y in the next line, which don't exist anymore:

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

This happens again with c in the 6-7th lines. This might be why your code isn't working.


I tried to make it work with my own code. I'm not sure at all about the math, but I based my algorithm on the snippet you posted on the question, using some of the mouse-tracking code from @Blindman67's answer.

The spiral

This is the important part. It returns an array with the spiral's points (I use another function to actually render them). The idea is to draw a spiral using the continuous-fibonacci function you provided. It starts at point A and forces the scaling so that the radius at one turn is the distance between point A and point B. It also adds an angle offset so the angle at one turn is the angle between point A and B.

Edited to address comment: I changed the for loop to a while loop that continues drawing until the spiral reaches a maximum radius. I also changed some names and added comments to try to make the algorithm clearer.

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;
};

Fibonacci sequence

The code to generate the continuous fibonacci numbers is basically yours, but I changed some names to help me understand it. I also added a generator function so it could work up to any number:

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;
};

Distance and angle between points

To make code clearer, I used a couple helper functions to work with 2D points:

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);
};

The whole thing: JSFiddle and Updated-to-address-comment JSFiddle

Upvotes: 9

Blindman67
Blindman67

Reputation: 54049

This is how I did it. The thing to do is to find the radius of the spiral at the angle from pointA to B and then scale the spiral to fit.

The function renders the spiral on the canvas centered at pointA and intersecting pointB. It uses ctx.setTransform to position the spiral to fit the constraints or you can just use the scale and center offsets to transform the siral points and keep the default canvas transformation (incase you are drawing other stuff);

Caveats

  • Does not draw if pointB === pointA as there is no solution.
  • May not draw if pointA is to far outside the canvas (I have not tested that).
  • Always draws from the center out. Does not consider clipping of spiral other than where to stop.

So to the code. (Updated)

// 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}
    );
};

Hope this helps. Sorry about the extra fruit but I had to test it so though I would just copy it all as the answer.

I have added a fiddle for those that want to see it running. PointA be is moved automatically (so looks a little strange as you move the mouse) as i could not be bothered with adding a proper interface.

UPDATE: I have updated the answer and fiddle attempting to find a better solution to the updated question. Unfortunately I was unable to match the new requirements, though from my analysis I find that the requirements present an unsolvable problem. Namely as the spiral angle nears zero the scale (in the solution) approaches infinity, the asymptote is somewhere near PI/4 but because this is only an approximation it all becomes meaningless. There is a set of locations for point A and B where the spiral can not be fitted. This is my interpretation and does not mean there is no solution as I have not provided a proof.

Fiddle (updated)

Upvotes: 15

Related Questions