Reputation: 3641
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.
Upvotes: 18
Views: 5893
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.
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;
};
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;
};
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
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
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.
Upvotes: 15