I am doing a small JavaScript animation hoping that the little div can move along a sine wave, and for the moment the horizontal path works fine (just straight line). I am almost sure that my math formula for the Y axis is wrong. I have tried to correct it with some examples I found, but none of them worked for me. In all the possibilities I tried, the Y axis is ignored and the little box just moves in straight line horizontally.
How can I fix this, so the movement goes along a sine wave? I know that it's possible to do it easier with jQuery or using html 5, but I just got wondering what is wrong in my original code... I would prefer to fix this if possible.
function animate() {
xnow = item.style.left;
item.style.left = parseInt(xnow)+1+'px';
ynow = item.style.top;
item.style.top = ynow + (Math.sin((2*Math.PI*xnow)/document.width))*document.heigth;
setTimeout(animate,20);
}
The complete code here:
JSFiddle
I see several problems with your code:
xnow contains a string in this format: ###px You cannot multiply it, so use parseInt() in your Math.sin() call.
Same goes for your code to grab ynow, it needs parseInt().
Better is to use other (global) variables to store the x and y coordinates as numbers. And add px when you update coordinates of the div-element.
When you multiply 2*Math.PI with xnow (which contains only integer numbers), the sin() function will always return 0. So you won't get a sine-like movement. You need to divide xnow by the number of x-steps you want to use to do a complete sine-like movement
Math.sin() returns a value between -1 and +1, so you need to multiply it by an amplitude to see a (more clear) effect.
To keep it as much as you designed it, it would become something like this (takes 50 x-movement steps to do a complete sine and uses an amplitude of 10 pixels):
function animate() {
xnow = parseInt(item.style.left);
item.style.left = (xnow+1)+'px';
ynow = parseInt(item.style.top);
item.style.top = (ynow+Math.sin(2*Math.PI*(xnow/50))*10) + "px";
setTimeout(animate,20);
}
As mentioned: it is much better to use some global variables containing the values (instead of using parseInt() all the time)
See my updated JSFiddle example.
A sin function is in the form of y = a * sin(b*x) + c, where c is the y-midpoint of the function (or the horizontal line across which the function oscillates), where a is the amplitude (maximal y-offset from y = c) and b is the period (number of x = 2*pi segments per wave).
Given that, and that we know a sin wave oscillates from -a to +a, we know our offset (c) should 1) be constant and 2) halfway between our upper and lower bounds. For this we can use
c = document.height / 2;
Your amplitude will be the same value as c, if you want the object to traverse the entire screen. On testing you will find that this makes it go past the bottom of the page, so let's just make it 90%.
a = 0.9 * c;
For a period of 1 for the entire page, you'll need to make b multiply x by a factor such that it will be the fraction of 2*pi. In this case
b = 2*Math.PI/document.width;
On each iteration, there is no need to get the value of ynow, it is a function of xnow. You can do something along the lines of
xnow = parseInt(item.top.left) + 5;
Then calculate the new y with
ynow = c + a * Math.sin(b * xnow);.
Then set the style of the item.
item.style.left = xnow + 'px';
item.style.top = ynow + 'px';
Let me know if anything was unclear. Regards.
You need to use parseInt() on xnow. You also need to add 'px' to the end of the of the number to make into a correctly formatted string.
This code works:
function animate() {
xnow = parseInt(item.style.left);
item.style.left = xnow+1+'px';
item.style.top = 200 + (Math.sin((2*Math.PI*xnow)/200))*document.height+'px';
setTimeout(animate,20);
}
There are a couple errors:
height is misspelled
Parse xnow as an int before taking the sine
Parse ynow as an int before adding it (though it's not actually necessary, see below)
Add "px" to the end of the assignment to item.style.top
The equation could use some tweaking:
I suggest starting with (400 + Math.sin(2*Math.PI*xnow/document.width) * 200) + "px" and then playing around with it. The 400 is the horizontal axis to base the sine wave on. If you use ynow instead of a constant, you get cumulative effects (the wave will be much taller than you intend or the horizontal axis will change over time).
document.width is the width of one full period. The 200 is the peak amplitude (distance from the horizontal to a peak - document.height would push the box off screen in both directions). Plug in this function in place of the current one and then you can play around with the numbers:
function animate() {
xnow = parseInt(item.style.left);
item.style.left = xnow+1+'px';
item.style.top = (400 + Math.sin(2*Math.PI*xnow/document.width) * 200) + "px";
setTimeout(animate,20);
}
Related
I originally wanted to use four points (as a bezier Curve is defined with 4 points), but that forces me to brute force the position, so I tried a different approach i now need help with:
I have a start point P0, an end point P1 and slopes m0 and m1 which are supposed to give me the start/end slope to calculate a Bezier Curve inbetween them.
The Curve is supposed to be in the form of a function (3rd degree), since I need to get the height y of a given point x.
Using the HTML5Canvas i can draw a bezier curve no problem and using this function
that allows me to calculate any given point given a percentage of the way i can get the center point of the curve. But I don't need it depending on t but rather the y depending on x, so not halfway of the curve but halfway of the x distance between P0 and P1.
Image to visualize:
Left is what i can calculate, right is what i need.
I've been trying to calculate the cubic function given the two points P0, P1 as well as the slopes m0, m1, which results into four equations which i can't seem to be able to solve with only variable inputs. I've also tried to use the above function to calculate the t using the x value (which is known), but no dice there either.
I need to avoid using approximations or costly loops for these calculations as they are performed many times a second for many objects, thus this answer is not feasible for me.
Any help is appreciated.
I've encountered the same problem in a project I'm working on. I don't know of a formula to get the y coordinate from the x, and I suspect you'll have trouble with that route because a bezier curve can have up to 3 points that all have the same x value.
I would recommend using the library BezierEasing, which was designed for this use case and uses various performance enhancing techniques to make lookups as fast as possible: https://github.com/gre/bezier-easing
To solve this problem, you need to rewrite Bezier equation in power polynomial form
X(t) = t^3 * (P3.X-3*P2.X+3*P1.X-P0.X) +
t^2 * (3*P0.X + 6*P1.X+3*P2.X) +
t * (3*P1.X - 3P2.X) +
P0.X
if X(t) = P0.X*(1-ratio) + P3.X*ratio
then
let d = ratio * (P0.X - P3.X)
and solve cubic equation for unknown t
a*t^3 + b*t^2 + c*t + d = 0
JS code here
Then apply calculated t parameter (there might be upto three solutions) to Y-component and get point coordinates. Note that formulas are close (no loops) and should work fast enough
Thank you to everyone that answered before, those are generally great solutions.
In my case I can be 100% sure that I can convert the curve into a cubic function, which serves as the approximation of the bezier curve using the result of this calculation.
Since i have control over my points in my case, I can force the P0 to be on x=0, which simplifies the linear system calculations and thus allows me to calculate the cubic function much easier like this:
let startPoint: Utils.Vector2 = new Utils.Vector2(0, 100);
let endPoint: Utils.Vector2 = new Utils.Vector2(100, 100);
let a: number, b: number, c: number, d: number;
function calculateFunction() {
let m0: number = 0;
let m1: number = 0;
a = (-endPoint.x * (m0 + m1) - 2 * startPoint.y + 2 * endPoint.y) / -Math.pow(endPoint.x, 3);
b = (m1 - m0 - 3 * a * Math.pow(endPoint.x, 2)) / (2 * endPoint.x);
c = m0;
d = startPoint.y;
}
I'm trying to plot this type of "binary matrix" graphic:
Disregard the two colors from the sample image; I want to either color a dot blue for, let's say, "complete" values or leave it uncolored/gray for "incomplete" values as a way to track daily task completion for a certain amount of dots/days. The dots represent a day where a task was completed or not completed. Showing the full amount of dots/days gives perspective on % of completion as days go by.
I would like to use a combination of HTML/Javascript and PHP + MySQL. But the hardest part for me is figuring out a good algorithm to render this visualization. Thanks for your help.
Just treat each dot like it's a pixel. Also, imagine that the image has been rotated 90° CCW. Then, you simply draw a square that takes up less room that is allocated to it - this way, you get the separating lines.
Here'e a quick something to have a play with.
A few notes:
0) I just halved your image dimensions
1) 4 pixels and 5 pixels were chosen arbitrarily
2) I didn't bother with setting the colour of the dot - you can
easily do this.
3) I've simply treated the drawing area like a normal top-bottom
bitmap, while your image seems to show that all of the Y values will
be used before the next X value is needed. (This is like a 90° CCW
rotation).
4) I'm addressing the pixels with an X and a Y - perhaps you'd be
more interested in addressing them with a single number? If so, you
could easily write a function that would map two coords to a single
number - the pixels index, if you like.
I.e if an image is 100 x 100, there are 10,000 pixels. You could address them by specifying a number from 0 - 9,999
E.g
function 10k_to_100x100(index)
{
var x = index % 100;
var y = (index / 100).toFixed(0);
plotPixelDot(x, y);
}
X is simply the remainder when dividing by the width
Y is the whole number answer when dividing by the width
Here's a snippet you can try right here on the page:
function byId(id){return document.getElementById(id);}
window.addEventListener('load', onDocLoaded, false);
function onDocLoaded()
{
var x, y;
for (y=0; y<20; y++)
{
for (x=0; x<100; x++)
{
drawDot(x, y, 'output');
}
}
}
function drawDot(xPos, yPos, canvasId)
{
var actualX=xPos*5, actualY=yPos*5;
var ctx = byId(canvasId).getContext('2d');
ctx.fillRect(actualX, actualY, 4, 4);
}
<canvas width=558 height=122 id='output'></canvas>
I have a circle in my canvas. The mouse position is calculated in relation to the canvas. I want the circle to move when the mouse is at <=100px distance from it. The minimum distance to start moving is 100px, at 0.5px/tick. It goes up to 2px/tick at 20px distance.
Basically, the closer the mouse is to the circle, the faster the circle should move.
What I have so far moves the circle when distance is less or equal to 100 -- (I'm using easeljs library)
function handleTick() {
distance = calculateDistance(circle, mX, mY);
if (distance<=100) {
circle.x += 0.3;
stage.update();
}
}
What I want
function handleTick() {
distance = calculateDistance(circle, mX, mY);
if (distance<=100) {
circleSpeed = // equation that takes distance and outputs velocity px/tick.
circle.x += circleSpeed;
stage.update();
}
}
So I thought this was a mathmatical problem and posted it on math exchange, but so far no answers. I tried googling several topics like: "how to come up with an equation for a relation" since I have the domain (100, 20) and the range (0.5, 2). What function can relate them?
Thing is I'm bad at math, and these numbers might not even have a relation - I'm not sure what I'm looking for here.
Should I write a random algorithm "circleSpeed = 2x + 5x;" and hope it does what I want? Or is it possible to do as I did - "I want these to be the minimum and maximum values, now I need to come up with an equation for it"?
A pointer in the right direction would be great because so far I'm shooting in the dark.
If I understand it correctly, you want circleSpeed to be a function of distance, such that
circleSpeed is 0.5 when distance is 100.
circleSpeed is 2 when distance is 20.
There are infinity functions which fulfill that, so I will assume linearity.
The equation of the line with slope m and which contains the point (x₀,y₀) is
y = m (x-x₀) + y₀
But in this case you have two points, (x₁,y₁) and (x₂,y₂), so you can calculate the slope with
y₂ - y₁
m = ───────
x₂ - x₁
So the equation of the line is
y₂ - y₁
y = ─────── (x - x₁) + y₁
x₂ - x₁
With your data,
0.5 - 2
y = ──────── (x - 20) + 2 = -0.01875 x + 2.375
100 - 20
Therefore,
circleSpeed = -0.01875 * distance + 2.375
I assume you want a linear relation between the distance and speed?
If so, you could do something like circleSpeed = (2.5 - 0.5(distance/20)).
That would, however set the speed linearly from 0 to 2.5 on the range (100 to 0), but by using another if like this if (distance < 20) circleSpeed = 2 you would limit the speed to 2.0 at 20 range.
It's not 100% accurate to what you asked for, but pretty close and it should look ok I guess. It could possibly also be tweaked to get closer.
However if you want to make the circle move away from the mouse, you also need to do something to calculate the correct direction of movement as well, and your problem gets a tiny bit more complex as you need to calculate speed_x and speed_y
Here is a simple snippet to animate the speed linearly, what that means is that is the acceleration of the circle will be constant.
if distance > 100:
print 0
elseif distance < 20:
print 2
else:
print 2 - (distance -20 ) * 0.01875
Yet other relationships are possible, (other easings you might call them) but they will be more complicated, hehe.
EDIT: Whoops, I’d made a mistake.
I want to move the object in my case its a plane along the shown curve on page scroll step by step taking into consideration the amount of scroll value.firstly the object moves in straight line and then after a point it changes its direction and move in that direction.How to calculate those co-ordinates?
There are two ways you could get this one. I'll try to explain both in detail.
Scenario 1: Simple path like in the question.
Scenario 2: Arbitrary complex path.
Scenario 1:
In this case you can use a simple formula. Let's go with y = -x^2. This will yield a parabola, which has a similar shape as the path in the question. Here are the steps for what to do next (we assume your scrolling element is the body tag and I assume you have jquery):
Get the "y" value of the body using the following code:
var y = $("body").scrollTop();
Plug this value into the formula. I will give 3 examples where y is 0, 100 and 225 respectively.
//y=0
0 = -x^2
-x = sqrt(0)
x = +/- 0
So if we scroll and we are at the top of the page, then x will be zero.
//y=100
100 = -x^2
-x = sqrt(100)
x = +/- 10
The equation yieldsx as either positive of negative x but we only want positive so be sure to Math.abs() the result.
//y=225
225= -x^2
-x = sqrt(225)
x = +/- 15
From this you can see that the further we scroll down the more the object moves to the right.
Now set the "left" css of your object to the calculated value. This should be enough for this method.
Scenario 2
For more complex paths (or even random paths) you should rather put the x-values into an array ahead of time. Lets say you generate the array randomly and you end up with the following x-values:
var xvals = [0, 0.5, 1, 0.5, 0];
We use normalized x-values so that we can later calculate the position independent from screen size. This particular series of values will cause the object to zig-zag across the screen from left to right, then back to left.
The next step is to determine where our scroll position is at relative to the total scroll possibility. Lets say our page is 1000px in height. So if the scoll position is at zero then x = 0. If scroll = 500 then x = screenWidth. If scroll = 250 then x = 0.5 * screenWidth etc.
In the example I won't multiply with screen width for the sake of simplicity. But given the x value this should be simple.
The first thing you might want to get ready now is a lerping function. There is plenty of example code and so on so I trust that part to you. Basically it is a function that looks like this:
function lerp(from, to, prog);
Where from and to are any values imaginable and prog is a value between 0 and 1. If from is 100 and to is 200, a prog value of 0.5 will yield a return of 150.
So from here we proceed:
Get the scroll value as a normalized value
// scrollval = 200
var totalScroll = 1000;
var normScroll = scrollval/totalScroll; // answer is 0.2
Before we get to lerp we first need to get the x-values to lerp from and to. To do this we have to do a sort of lerping to get the correct index for the xvals array:
// normScroll = 0.2
var len = xvals.length; // 5
var indexMax = Math.ceil((len-1) * normScroll); // index = 1
var indexMin = Math.floor((len-1) * normScroll); // index = 0
Now we know the 2 x values to lerp between. They are xvals[0] which is 0, and xvals[1] which is 0.5;
But this is still not enough information. We also need the exact lerping "prog" value:
// We continue from the x indeces
scrollRange.x = totalScroll / len * indexMin; // 0
scrollRange.y = totalScroll / len * indexMax; // 250
var lerpingvalue = (scrollVal - scrollRange.x) / (scrollRange.y - scrollRange.x);// 0.8
Now we finally have everything we need. Now we know we need a value between xvals[0] and xvals[1] and that this value lies at 80% between these two values.
So finally we lerp:
var finalX = lerp(xvals[0], xvals[1], lerpingvalue);// 0.4
Now we know that the x coordinate is at 0.4 of the total screen size.
Trigger these calculations on each scroll event and you should be on your way.
I hope this was clear enough. If you try and try and can't get results and can show how hard you tried then I'll be happy to write a complete index.html sample for you to work from. Good luck!
EDIT: I made a mistake with the lerpingval calculation. Fixed now.
I'm building an image preload animation, that is a circle/pie that gets drawn. Each 'slice' is totalImages / imagesLoaded. So if there are four images and 2 have loaded, it should draw to 180 over time.
I'm using requestAnimFrame, which is working great, and I've got a deltaTime setup to restrict animation to time, however I'm having trouble getting my head around the maths. The closest I can get is that it animates and eases to near where it's supposed to be, but then the value increments become smaller and smaller. Essentially it will never reach the completed value. (90 degrees, if one image has loaded, as an example).
var totalImages = 4;
var imagesLoaded = 1;
var currentArc = 0;
function drawPie(){
var newArc = 360 / totalImages * this.imagesLoaded // Get the value in degrees that we should animate to
var percentage = (isNaN(currentArc / newArc) || !isFinite(currentArc / newArc)) || (currentArc / newArc) > 1 ? 1 : (currentArc / newArc); // Dividing these two numbers sometimes returned NaN or Infinity/-Infinity, so this is a fallback
var easeValue = easeInOutExpo(percentage, 0, newArc, 1);
//This animates continuously (Obviously), because it's just constantly adding to itself:
currentArc += easedValue * this.time.delta;
OR
//This never reaches the full amount, as increments get infinitely smaller
currentArc += (newArc - easedValue) * this.time.delta;
}
function easeInOutExpo(t, b, c, d){
return c*((t=t/d-1)*t*t + 1) + b;
}
I feel like I've got all the right elements and values. I'm just putting them together incorrectly.
Any and all help appreciated.
You've got the idea of easing. The reality is that at some point you cap the value.
If you're up for a little learning, you can brush up on Zeno's paradoxes (the appropriate one here being Achilles and the Tortoise) -- it's really short... The Dichotomy Paradox is the other side of the same coin.
Basically, you're only ever half-way there, regardless of where "here" or "there" may be, and thus you can never take a "final"-step.
And when dealing with fractions, such as with easing, that's very true. You can always get smaller.
So the solution is just to clamp it. Set a minimum amount that you can move in an update (2px or 1px, or 0.5px... or play around).
The alternative (which ends up being similar, but perhaps a bit jumpier), is to set a threshold distance. To say "As soon as it's within 4px of home, snap to the end", or switch to a linear model, rather than continuing the easing.