Make oscillations smoothly increase/decrease when frequency being changed dynamically - javascript

I'm modeling the resonance effect with html5 canvas to animate spring when its reach the resonance.
Also got jquery ui slider (max ranged) that changes frequency (w) of the oscillations dynamically during the animation. The problem is when its changed, for some reason sine wave brokes at some points and the animation is not smooth. This is only happens when changing frequency, with amplitude its much better.
my main function to render each frame is this:
function doSways() {
var spring = springs[0],
a = 0,
A = params.standParams.A,
w = params.standParams.w;
animIntervalId = setInterval(function() {
ctx.clearRect(0, 0, cnvW, cnvH);
A = params.standParams.A;
w = params.standParams.w;
/*if (w < params.standParams.w) { // with this expression and commented <w = params.standParams.w> it works a bit smoother but still some issues can be noticed, for example sharp increases just after the new change of the frequency (w) on the slider
w += 0.01;
}*/
stand.y = A*Math.sin(a*degToRad*w) + offsetY;
stand.draw();
spring.draw(stand.y, A, w);
if (a++ >= 360) { // avoid overflow
a = 0;
}
},
25);
}
here's how I change frequency(w) on the slider and assign it to params.standParams.w
$( "#standfreq_slider" ).slider({
range: "max",
min: 1,
max: 25,
step: 1,
value: 5,
slide: function( event, ui ) {
params.standParams.w = parseInt(ui.value);
}
});
);
That if expression in doSways function kinda work but it casues another problem, I need to know the direction of sliding to determine wether I need to += or -= 0.01..
How to make everything work ideal ?
problem illustration live
jsfiddle

based on the basic formula for sinusoids:
V = V0 + A * sin(2 * pi * f * t + phi)
Where:
V: current value
V0: center value
A: amplitude
f: frequency (Hz)
t: time (seconds)
phi: phase
We want the current value (V) to be the same before and after a frequency change. In order to do that we will need a frequency (f) and phase (phi) before and after the change.
First we can set the first equation equal the the second one:
V0 + A * sin(2 * pi * f1 * t + phi1) = V0 + A * sin(2 * pi * f2 * t + phi2)
do some cancelling:
2 * pi * f1 * t + phi1 = 2 * pi * f2 * t + phi2
solve for the new phase for the final formula:
phi2 = 2 * pi * t * (f1 - f2) + phi1
More mathy version:
edited:
check out modified fiddle: https://jsfiddle.net/potterjm/qy1s8395/1/

Well the pattern i explain below is one i use quite often.
( It might very well have a name - please S.O. people let me know if so-. )
Here's the idea : whenever you need to want a property evolving in a smooth manner, you must distinguish the target value, and the current value. If, for some reason, the target value changes, it will only affect the current value in the way you decided.
To get from the current value to the target value, you have many ways :
• most simple : just get it closer from a given ratio on every tick :
current += ratio * (target-current);
where ratio is in [0, 1], 0.1 might be ok. You see that, with ratio == 1, current == target on first tick.
Beware that this solution is frame-rate dependent, and that you might want to threshold the value to get the very same value at some point, expl :
var threshold = 0.01 ;
if (Math.abs(target-current)<threshold) current = target;
• You can also reach target at a given speed :
var sign = (target - current) > 0 ? 1 : -1;
current += sign * speedToReachTarget * dt;
Now we are not frame-rate dependent (you must handle frame time properly), but you will have 'bouncing' if you don't apply a min/max and a threshold also :
if (Math.abs(target-current)<0.01) {
current = target;
return;
}
var sign = (target - current) > 0 ? 1 : -1;
current += sign * speedToReachTarget * dt;
current = (( sign > 0) ? Math.min : Math.max )( target, current);
• and you might use many other type of interpolation/easing.

Related

Optimise javascript canvas for mass-drawing of tiny objects

I've been working on a game which requires thousands of very small images (20^20 px) to be rendered and rotated each frame. A sample snippet is provided.
I've used every trick I know to speed it up to increase frame rates but I suspect there are other things I can do to optimise this.
Current optimisations include:
Replacing save/restore with explicit transformations
Avoiding scale/size-transformations
Being explicit about destination sizes rather than letting the browser guess
requestAnimationFrame rather than set-interval
Tried but not present in example:
Rendering objects in batches to other offscreen canvases then compiling later (reduced performance)
Avoiding floating point locations (required due to placement precision)
Not using alpha on main canvas (not shown in snippet due to SO snippet rendering)
//initial canvas and context
var canvas = document.getElementById('canvas');
canvas.width = 800;
canvas.height = 800;
var ctx = canvas.getContext('2d');
//create an image (I) to render
let myImage = new OffscreenCanvas(10,10);
let myImageCtx = myImage.getContext('2d');
myImageCtx.fillRect(0,2.5,10,5);
myImageCtx.fillRect(0,0,2.5,10);
myImageCtx.fillRect(7.5,0,2.5,10);
//animation
let animation = requestAnimationFrame(frame);
//fill an initial array of [n] object positions and angles
let myObjects = [];
for (let i = 0; i <1500; i++){
myObjects.push({
x : Math.floor(Math.random() * 800),
y : Math.floor(Math.random() * 800),
angle : Math.floor(Math.random() * 360),
});
}
//render a specific frame
function frame(){
ctx.clearRect(0,0,canvas.width, canvas.height);
//draw each object and update its position
for (let i = 0, l = myObjects.length; i<l;i++){
drawImageNoReset(ctx, myImage, myObjects[i].x, myObjects[i].y, myObjects[i].angle);
myObjects[i].x += 1; if (myObjects[i].x > 800) {myObjects[i].x = 0}
myObjects[i].y += .5; if (myObjects[i].y > 800) {myObjects[i].y = 0}
myObjects[i].angle += .01; if (myObjects[i].angle > 360) {myObjects[i].angle = 0}
}
//reset the transform and call next frame
ctx.setTransform(1, 0, 0, 1, 0, 0);
requestAnimationFrame(frame);
}
//fastest transform draw method - no transform reset
function drawImageNoReset(myCtx, image, x, y, rotation) {
myCtx.setTransform(1, 0, 0, 1, x, y);
myCtx.rotate(rotation);
myCtx.drawImage(image, 0,0,image.width, image.height,-image.width / 2, -image.height / 2, image.width, image.height);
}
<canvas name = "canvas" id = "canvas"></canvas>
You are very close to the max throughput using the 2D API and a single thread, however there are some minor points that can improve performance.
WebGL2
First though, if you are after the best performance possible using javascript you must use WebGL
With WebGL2 you can draw 8 or more times as many 2D sprites than with the 2D API and have a larger range of FX (eg color, shadow, bump, single call smart tile maps...)
WebGL is VERY worth the effort
Performance related points
globalAlpha is applied every drawImage call, values other than 1 do not affect performance.
Avoid the call to rotate The two math calls (including a scale) are a tiny bit quicker than the rotate. eg ax = Math..cos(rot) * scale; ay = Math.sin(rot) * scale; ctx.setTransform(ax,ay,-ay,ax,x,y)
Rather than use many images, put all the images in a single image (sprite sheet). Not applicable in this case
Don`t litter the global scope. Keep object close as possible to functions scope and pass object by reference. Access to global scoped variable is MUCH slower the local scoped variables.
Best to use modules as they hove their own local scope
Use radians. Converting angles to deg and back is a waste of processing time. Learn to use radians Math.PI * 2 === 360 Math.PI === 180 and so on
For positive integers don't use Math.floor use a bit-wise operator as they automatically convert Doubles to Int32 eg Math.floor(Math.random() * 800) is faster as Math.random() * 800 | 0 ( | is OR )
Be aware of the Number type in use. Converting to an integer will cost cycles if every time you use it you convert it back to double.
Always Pre-calculate when ever possible. Eg each time you render an image you negate and divide both the width and height. These values can be pre calculated.
Avoid array lookup (indexing). Indexing an object in an array is slower than direct reference. Eg the main loop indexes myObject 11 times. Use a for of loop so there is only one array lookup per iteration and the counter is a more performant internal counter. (See example)
Though there is a performance penalty for this, if you separate update and render loops on slower rendering devices you will gain performance, by updating game state twice for every rendered frame. eg Slow render device drops to 30FPS and game slows to half speed, if you detect this update state twice, and render once. The game will still present at 30FPS but still play and normal speed (and may even save the occasional drooped frame as you have halved the rendering load)
Do not be tempted to use delta time, there are some negative performance overheads (Forces doubles for many values that can be Ints) and will actually reduce animation quality.
When ever possible avoid conditional branching, or use the more performant alternatives. EG in your example you loop object across boundaries using if statements. This can be done using the remainder operator % (see example)
You check rotation > 360. This is not needed as rotation is cyclic A value of 360 is the same as 44444160. (Math.PI * 2 is same rotation as Math.PI * 246912)
Non performance point.
Each animation call you are preparing a frame for the next (upcoming) display refresh. In your code you are displaying the game state then updating. That means your game state is one frame ahead of what the client sees. Always update state, then display.
Example
This example has added some additional load to the objects
can got in any direction
have individual speeds and rotations
don`t blink in and out at edges.
The example includes a utility that attempts to balance the frame rate by varying the number of objects.
Every 15 frames the (work) load is updated. Eventually it will reach a stable rate.
DON`T NOT gauge the performance by running this snippet, SO snippets sits under all the code that runs the page, the code is also modified and monitored (to protect against infinite loops). The code you see is not the code that runs in the snippet. Just moving the mouse can cause dozens of dropped frames in the SO snippet
For accurate results copy the code and run it alone on a page (remove any extensions that may be on the browser while testing)
Use this or similar to regularly test your code and help you gain experience in knowing what is good and bad for performance.
Meaning of rate text.
1 +/- Number Objects added or removed for next period
2 Total number of objects rendered per frame during previous period
3 Number Running mean of render time in ms (this is not frame rate)
4 Number FPS is best mean frame rate.
5 Number Frames dropped during period. A dropped frame is the length of the reported frame rate. I.E. "30fps 5dropped" the five drop frames are at 30fps, the total time of dropped frames is 5 * (1000 / 30)
const IMAGE_SIZE = 10;
const IMAGE_DIAGONAL = (IMAGE_SIZE ** 2 * 2) ** 0.5 / 2;
const DISPLAY_WIDTH = 800;
const DISPLAY_HEIGHT = 800;
const DISPLAY_OFFSET_WIDTH = DISPLAY_WIDTH + IMAGE_DIAGONAL * 2;
const DISPLAY_OFFSET_HEIGHT = DISPLAY_HEIGHT + IMAGE_DIAGONAL * 2;
const PERFORMANCE_SAMPLE_INTERVAL = 15; // rendered frames
const INIT_OBJ_COUNT = 500;
const MAX_CPU_COST = 8; // in ms
const MAX_ADD_OBJ = 10;
const MAX_REMOVE_OBJ = 5;
canvas.width = DISPLAY_WIDTH;
canvas.height = DISPLAY_HEIGHT;
requestAnimationFrame(start);
function createImage() {
const image = new OffscreenCanvas(IMAGE_SIZE,IMAGE_SIZE);
const ctx = image.getContext('2d');
ctx.fillRect(0, IMAGE_SIZE / 4, IMAGE_SIZE, IMAGE_SIZE / 2);
ctx.fillRect(0, 0, IMAGE_SIZE / 4, IMAGE_SIZE);
ctx.fillRect(IMAGE_SIZE * (3/4), 0, IMAGE_SIZE / 4, IMAGE_SIZE);
image.neg_half_width = -IMAGE_SIZE / 2; // snake case to ensure future proof (no name clash)
image.neg_half_height = -IMAGE_SIZE / 2; // use of Image API
return image;
}
function createObject() {
return {
x : Math.random() * DISPLAY_WIDTH,
y : Math.random() * DISPLAY_HEIGHT,
r : Math.random() * Math.PI * 2,
dx: (Math.random() - 0.5) * 2,
dy: (Math.random() - 0.5) * 2,
dr: (Math.random() - 0.5) * 0.1,
};
}
function createObjects() {
const objects = [];
var i = INIT_OBJ_COUNT;
while (i--) { objects.push(createObject()) }
return objects;
}
function update(objects){
for (const obj of objects) {
obj.x = ((obj.x + DISPLAY_OFFSET_WIDTH + obj.dx) % DISPLAY_OFFSET_WIDTH);
obj.y = ((obj.y + DISPLAY_OFFSET_HEIGHT + obj.dy) % DISPLAY_OFFSET_HEIGHT);
obj.r += obj.dr;
}
}
function render(ctx, img, objects){
for (const obj of objects) { drawImage(ctx, img, obj) }
}
function drawImage(ctx, image, {x, y, r}) {
const ax = Math.cos(r), ay = Math.sin(r);
ctx.setTransform(ax, ay, -ay, ax, x - IMAGE_DIAGONAL, y - IMAGE_DIAGONAL);
ctx.drawImage(image, image.neg_half_width, image.neg_half_height);
}
function timing(framesPerTick) { // creates a running mean frame time
const samples = [0,0,0,0,0,0,0,0,0,0];
const sCount = samples.length;
var samplePos = 0;
var now = performance.now();
const maxRate = framesPerTick * (1000 / 60);
const API = {
get FPS() {
var time = performance.now();
const FPS = 1000 / ((time - now) / framesPerTick);
const dropped = ((time - now) - maxRate) / (1000 / 60) | 0;
now = time;
if (FPS > 30) { return "60fps " + dropped + "dropped" };
if (FPS > 20) { return "30fps " + (dropped / 2 | 0) + "dropped" };
if (FPS > 15) { return "20fps " + (dropped / 3 | 0) + "dropped" };
if (FPS > 10) { return "15fps " + (dropped / 4 | 0) + "dropped" };
return "Too slow";
},
time(time) { samples[(samplePos++) % sCount] = time },
get mean() { return samples.reduce((total, val) => total += val, 0) / sCount },
};
return API;
}
function updateStats(CPUCost, objects) {
const fps = CPUCost.FPS;
const mean = CPUCost.mean;
const cost = mean / objects.length; // estimate per object CPU cost
const count = MAX_CPU_COST / cost | 0;
const objCount = objects.length;
var str = "0";
if (count < objects.length) {
var remove = Math.min(MAX_REMOVE_OBJ, objects.length - count);
str = "-" + remove;
objects.length -= remove;
} else if (count > objects.length + MAX_ADD_OBJ) {
let i = MAX_ADD_OBJ;
while (i--) {
objects.push(createObject());
}
str = "+" + MAX_ADD_OBJ;
}
info.textContent = str + ": " + objCount + " sprites " + mean.toFixed(3) + "ms " + fps;
}
function start() {
var frameCount = 0;
const CPUCost = timing(PERFORMANCE_SAMPLE_INTERVAL);
const ctx = canvas.getContext('2d');
const image = createImage();
const objects = createObjects();
function frame(time) {
frameCount ++;
const start = performance.now();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, DISPLAY_WIDTH, DISPLAY_WIDTH);
update(objects);
render(ctx, image, objects);
requestAnimationFrame(frame);
CPUCost.time(performance.now() - start);
if (frameCount % PERFORMANCE_SAMPLE_INTERVAL === 0) {
updateStats(CPUCost, objects);
}
}
requestAnimationFrame(frame);
}
#info {
position: absolute;
top: 10px;
left: 10px;
background: #DDD;
font-family: arial;
font-size: 18px;
}
<canvas name = "canvas" id = "canvas"></canvas>
<div id="info"></div>

Function Expression Parameter is not assigned a value, but has a value

I'm hoping someone can explain how the the below function expression works. The parameter 'p' has not been assigned a value, however it is used in the body of the function for calculations (defining the "angle" and "tail" variables) and as an argument object when draw() is called. When you step through the code, you can see that parameter 'p' does, indeed, have a value (see attached image)- but I don't understand where that value comes from. Please note that 'p' is not noted anywhere else upon review of the full code.
When you log 'p' to the console you see the following values, which continue to increase as the application runs:
Image of console.log(p);
Here is the full function expression and the function call:
var draw = function(p) { 
$.fillStyle = "hsla(38,5%,12%,.90)";
$.fillRect(0, 0, w, h); 
$.fillStyle = "hsla(38, 25%, 90%, 1)"; 
$.strokeStyle = "hsla(38, 25%, 90%, 1)";
for (var i = 0; i < numh; i++)
for (var j = 0; j < numw; j++) {
var diagnalW = j * spacing +
(i % 2 ? 0 : spacing / 2);
var diagnalH = i * spacing;
var arr = [position[0] - diagnalW,
position[1] - diagnalH
],
wave = Math.sqrt(arr[0] * arr[0] +
arr[1] * arr[1]),
arr = [arr[0] / wave, arr[1] / wave],
angle = 50 * (Math.cos(p / 360 - wave / 105) - 1);
$.beginPath();
$.arc(diagnalW + arr[0] * angle, diagnalH +
arr[1] * angle, 2.8, 0, 2 * Math.PI, false);
$.closePath();
$.fill();
for (var n = 0; n < 5; n++) {
var tail = 50 * (Math.cos((p - 50 * n) /
360 - wave / 105) - 1);
$.beginPath();
$.moveTo(diagnalW + arr[0] * angle, diagnalH +
arr[1] * angle);
$.lineWidth = 5 - n;
$.lineTo(diagnalW + arr[0] * tail, diagnalH + arr[1] * tail);
$.stroke()
}
}
};
var anim = function(p) {
window.requestAnimationFrame(anim);
draw(p);
};
anim();
I understand everything about this code with the exception of how 'p' obtains the values shown in the console. Also - in case it wasn't clear, this is an html5 canvas application.
(Side note: No, $ in the above isn't jQuery. It's just what the original author uses for her canvas context object.)
In the first call to draw, p will have the value undefined because anim() doesn't pass a value for p, and anim then uses p when calling draw.
After that, though, draw is called by the browser, not by that code, because it's being used as the callback for requestAnimationFrame. The browser will call it with a high-resolution timer value, which is what you're seeing in p.
p is a parameter of a callback for window.requestAnimationFrame
A parameter specifying a function to call when it's time to update your animation for the next repaint. The callback has one single argument, a DOMHighResTimeStamp, which indicates the current time (the time returned from performance.now()) for when requestAnimationFrame starts to fire callbacks.

Canvas jitters half my rendering

I was working on a fun project that implicates creating "imperfect" circles by drawing them with lines and animate their points to generate a pleasing effect.
The points should alternate between moving away and closer to the center of the circle, to illustrate:
I think I was able to accomplish that, the problem is when I try to render it in a canvas half the render jitters like crazy, you can see it in this demo.
You can see how it renders for me in this video. If you pay close attention the bottom right half of the render runs smoothly while the top left just..doesn't.
This is how I create the points:
for (var i = 0; i < q; i++) {
var a = toRad(aDiv * i);
var e = rand(this.e, 1);
var x = Math.cos(a) * (this.r * e) + this.x;
var y = Math.sin(a) * (this.r * e) + this.y;
this.points.push({
x: x,
y: y,
initX: x,
initY: y,
reverseX: false,
reverseY: false,
finalX: x + 5 * Math.cos(a),
finalY: y + 5 * Math.sin(a)
});
}
Each point in the imperfect circle is calculated using an angle and a random distance that it's not particularly relevant (it relies on a few parameters).
I think it's starts to mess up when I assign the final values (finalX,finalY), the animation is supposed to alternate between those and their initial values, but only half of the render accomplishes it.
Is the math wrong? Is the code wrong? Or is it just that my computer can't handle the rendering?
I can't figure it out, thanks in advance!
Is the math wrong? Is the code wrong? Or is it just that my computer can't handle the rendering?
I Think that your animation function has not care about the elapsed time. Simply the animation occurs very fast. The number of requestAnimationFrame callbacks is usually 60 times per second, So Happens just what is expected to happen.
I made some fixes in this fiddle. This animate function take care about timestamp. Also I made a gradient in the animation to alternate between their final and initial positions smoothly.
ImperfectCircle.prototype.animate = function (timestamp) {
var factor = 4;
var stepTime = 400;
for (var i = 0, l = this.points.length; i < l; i++) {
var point = this.points[i];
var direction = Math.floor(timestamp/stepTime)%2;
var stepProgress = timestamp % stepTime * 100 / stepTime;
stepProgress = (direction == 0 ? stepProgress: 100 -stepProgress);
point.x = point.initX + (Math.cos(point.angle) * stepProgress/100 * factor);
point.y = point.initY + (Math.sin(point.angle) * stepProgress/100 * factor);
}
}
Step by Step:
based on comments
// 1. Calculates the steps as int: Math.floor(timestamp/stepTime)
// 2. Modulo to know if even step or odd step: %2
var direction = Math.floor(timestamp/stepTime)%2;
// 1. Calculates the step progress: timestamp % stepTime
// 2. Convert it to a percentage: * 100 / stepTime
var stepProgress = timestamp % stepTime * 100 / stepTime;
// if odd invert the percentage.
stepProgress = (direction == 0 ? stepProgress: 100 -stepProgress);
// recompute position based on step percentage
// factor is for fine adjustment.
point.x = point.initX + (Math.cos(point.angle) * stepProgress/100 * factor);
point.y = point.initY + (Math.sin(point.angle) * stepProgress/100 * factor);

Non-linear interpolation of a range to another range

I'm struggling with creating a function, that interpolates one range to another. I already have a functioning linear interpolation, but getting more 'curved' interpolations, I am stumped.
F.ex. I want to interpolate the range [0,100] to [0,1000] in a way, so target-values near 0 is more likely than target-values near 1000 (or vice-versa).
One approach I got working was using logarithmic interpolation, which has some annoying drawbacks:
I used a crude hack to deal with negative numbers (f.ex. [-70,50]), by offsetting the range before calculating
The 'slope' is not adjustable (f.ex. if I want the target-value to be just a bit more likely or a lot more likely than a linear interpolation)
function logPol (value, s1, s2, t1, t2) {
var f = (value - s1) / ((value - s1) + (s2 - value));
var add = 0;
if(t1 <= 0 || t2 <= 0) {
add = t1 >= t2? 2 * Math.abs(t2) + 1 : 2 * Math.abs(t1) + 1;
t1 += add;
t2 += add;
}
var interpolated = Math.pow(t2,f) * Math.pow(t1, 1-f) - add;
return interpolated;
};
I've read a lot of articles on quadratic equations, thinking that would solve my problem, but never ended up with a working solution. If I need to explain something further, please let me know.
After reading a ton of different approaches, I finally solved my problem, and ended up using Bezier curves to handle my interpolation. Here's my resulting function:
/**
* Returns a bezier interpolated value, using the given ranges
* #param {number} value Value to be interpolated
* #param {number} s1 Source range start
* #param {number} s2 Source range end
* #param {number} t1 Target range start
* #param {number} t2 Target range end
* #param {number} [slope] Weight of the curve (0.5 = linear, 0.1 = weighted near target start, 0.9 = weighted near target end)
* #returns {number} Interpolated value
*/
var interpolate = function (value, s1, s2, t1, t2, slope) {
//Default to linear interpolation
slope = slope || 0.5;
//If the value is out of the source range, floor to min/max target values
if(value < Math.min(s1, s2)) {
return Math.min(s1, s2) === s1 ? t1 : t2;
}
if(value > Math.max(s1, s2)) {
return Math.max(s1, s2) === s1 ? t1 : t2;
}
//Reverse the value, to make it correspond to the target range (this is a side-effect of the bezier calculation)
value = s2-value;
var C1 = {x: s1, y:t1}; //Start of bezier curve
var C3 = {x: s2, y:t2}; //End of bezier curve
var C2 = { //Control point
x: C3.x,
y: C1.y + Math.abs(slope) * (C3.y - C1.y)
};
//Find out how far the value is on the curve
var percent = value / (C3.x-C1.x);
return C1.y*b1(percent) + C2.y*b2(percent) + C3.y*b3(percent);
function b1(t) { return t*t }
function b2(t) { return 2*t*(1 - t) }
function b3(t) { return (1 - t)*(1 - t) }
};

2D game algorithm to calculate a bullet's needed speed to hit target?

I have a rather simple bird's-view 2D game where tower sprites defend against incoming moving sprites by shooting a bullet at them. My question: How do I calculate the needed bullet speed for the bullet to reach its moving target, provided that the bullet will always have the same defined speed?
I'm using JavaScript and have these sprite variables (among others):
sprite.x, sprite.y, sprite.width, sprite.height, sprite.speedX (i.e. velocity), sprite.speedY... so I have the objects originSprite, targetSprite and bulletSprite, all with these type of values, and I need to set the right bulletSprite speed values.
Probably for it to look good, the bullet would start at the outside of the originSprite (or some defined radius, though I guess starting from the originSprite center would also work), but its bullet center would try hit into the center of the targetSprite or so. Note there's no gravity or anything in this world. (Perhaps I should have my sprites variables using angle and velocity but right now I'm using speedX and speedY...)
Thanks so much!
Treat the targets sprite as a straight line in a 2 dimensional room where:
A(time) = (sprite.positionX + sprite.speedX * time, sprite.positionX + sprite.speedX * time)
As your bullet have constant speed you also know:
bullet.speedX^2 + bullet.speedY^2 = bullet.definedSpeed^2
Then you can also calculate a straight line for the bullet:
B(time) = (bullet.positionX + bullet.speedX * time, bullet.positionX + bullet.speedX * time)
And you know that both lines interset somewhere:
A(time) = B(time)
Then it's up to you to solve those equations with your given values and seek a minimum for time.
Some physical insight
1 ) For the target being a "Point Object"
So you have to solve the VECTOR equation
Positionbullet [ time=t1 > t0 ] == Positiontarget [ time=t1 > t0 ] -- (Eq 1)
Where the positions are given by the motion (also VECTOR) equations
Positionobject [ t ] = Positionobject [ t0 ] + Speedobject * ( t - t0 )
Now, the condition for the bullet to be able to reach the target is that the Eq 1 has solutions for x and y. Let's write down the equation for x:
Xbullet [ t0 ] + SpeedXbullet * ( t - t0 ) = Xtarget [ t0 ] + SpeedXtarget * ( t - t0 )
So for the collision time we have
( tCollision - t0 ) = (xtarget [ t 0 ] - xbullet [ t0 ] ) / (SpeedXbullet - SpeedXtarget) -- (Eq 2)
As we need solutions with t > t0, that means that for having an intercept is enough that>
Sign ( xtarget[ t0 ] - xbullet[ t0 ] ) = Sign ( SpeedXbullet - SpeedXtarget ) -- (Eq 3)
Which tells us the evident fact that if an object is moving faster than the other, and in the same direction, they will eventually collide.
From Eq 2, you can see that for a given SpeedXtarget there exist infinite solutions (as already pointed out in other answers) for t and SpeedXbullet, so I think your specifications are not complete.
I guess (as stated in a commentary I made in another answer) thinking in a "tower defense" kind of game, that your bullets have a limited range.
So you need also another constraint:
Distance [ Positiontarget [ tCollision - t0 ] - Positionbullet [ t0 ] ] < BulletRange -- (Eq 4)
Which still permits infinite solutions, but bounded by an upper value for the Collision time, given by the fact that the target may abandon the range.
Further, the distance is given by
Distance[v,u]= +Sqrt[ (Vx-Ux)^2 + (Vx-Vy)^2 ]
So, Eq 4 becomes,
(Xtarget[tCollision - t0] - Xbullet[t0])2 + (Ytarget[tCollision - t0] - Ybullet[t0])2 < BulletRange2 -- (Eq 5)
Note that { Xbullet[t0] , Ybullet[t0} is the tower position.
Now, replacing in Eq 5 the values for the target position:
(Xtarget[t0] + SpeedXtarget * (t-t0) - Xbullet[t0])2 + (Ytarget[t0] + SpeedYtarget * (t-t0) - Ybullet[t0])2 < BulletRange2 -- (Eq 6)
Calling the initial distances:
Dxt0 = Xtarget[t0] - Xbullet[t0]
and
Dyt0 = Ytarget[t0] - Ybullet[t0]
Equation 6 becomes
(Dtx0 + SpeedXtarget * (t-t0) )2 + (Dty0 + SpeedYtarget * (t-t0))2 < BulletRange2 -- (Eq 7)
Which is a quadratic equation to be solved in t-t0. The positive solution will give us the largest time allowed for the collision. Afterwards the target will be out of range.
Now calling
Speedtarget 2 = SpeedXtarget 2 + SpeedYtarget 2
and
H = Dtx0 * SpeedXtarget + Dty0 * SpeedYtarget
TCollision Max = t0 - ( H
+/- Sqrt ( BulletRange2 * Speedtarget 2 - H2 ) ) / Speedtarget 2
So you need to produce the collision BEFORE this time. The sign of the
square root should be taken such as the time is greater than t0
After you select an appropriate flying time for your bullet from the visual
effects point of view, you can calculate the SpeedX and SpeedY for the bullet
from
SpeedXbullet = ( Xtarget [ t0 ] - Xbullet [ t0 ] ) / ( tCollision - t0 ) + SpeedXtarget
and
SpeedYbullet = ( Ytarget [ t0 ] - Ybullet [ t0 ] ) / ( tCollision - t0 ) + SpeedYtarget
2 ) For the target and tower being "Extensive Objects"
Now, it is trivial to generalize for the case of the target being a circle of radius R. What you get, is the equivalent of an "extended range" for the bullets. That extension is just R.
So, replacing BulletRange by (BulletRange + R) you get the new equations for the maximum allowed collision time.
If you also want to consider a radius for the cannons, the same considerations apply, giving a "double extended range
NewBulletRange = BulletRange + RTarget + RTower
Unlimited Range Bullets
In the case that you decide that some special bullets should not have range (and detection) limitations, there is still the screen border constraint. But it is a little more difficult to tackle. Should you need this kind of projectile, leave a comment and I'll try to do some math.
Using vectors can make the math around this seem a little simpler. Sylvester seems to be a promising implementation of vectors in JavaScript, but for the purpose of my example, I'll write my own vector functions. I'm also going to assume .x / .y are measured top/left corner.
// this is a "constant" - representing 10px motion per "time unit"
var bulletSpeed = 10;
// calculate the vector from our center to their center
var enemyVec = vec_sub(targetSprite.getCenter(), originSprite.getCenter());
// measure the "distance" the bullet will travel
var dist = vec_mag(enemyVec);
// adjust for target position based on the amount of "time units" to travel "dist"
// and the targets speed vector
enemyVec = vec_add(enemyVec, vec_mul(targetSprite.getSpeed(), dist/bulletSpeed));
// calculate trajectory of bullet
var bulletTrajectory = vec_mul(vec_normal(enemyVec), bulletSpeed);
// assign values
bulletSprite.speedX = bulletTrajectory.x;
bulletSprite.speedY = bulletTrajectory.y;
// functions used in the above example:
// getCenter and getSpeed return "vectors"
sprite.prototype.getCenter = function() {
return {
x: this.x+(this.width/2),
y: this.y+(this.height/2)
};
};
sprite.prototype.getSpeed = function() {
return {
x: this.speedX,
y: this.speedY
};
};
function vec_mag(vec) { // get the magnitude of the vector
return Math.sqrt( vec.x * vec.x + vec.y * vec.y);
}
function vec_sub(a,b) { // subtract two vectors
return { x: a.x-b.x, y: a.y-b.y };
}
function vec_add(a,b) { // add two vectors
return { x: a.x + b.x, y: a.y + b.y };
}
function vec_mul(a,c) { // multiply a vector by a scalar
return { x: a.x * c, y: a.y * c };
}
function vec_div(a,c) { // divide == multiply by 1/c
return vec_mul(a, 1.0/c);
}
function vec_normal(a) { // normalize vector
return vec_div(a, vec_mag(a));
}
Compute the distance between shooter and target: dist = sqrt((xt - xs)^2 + (yt - ys)^2)
Divide the x and y distances by the above one: nx = (xt - xs)/dist; ny = (yt - ys)/dist; (normalization of the vector)
Multiply the results by a factor to get n pixels per time unit, ie. a speed in each direction. It should give a constant speed in the wanted direction.
I assume that the target will move on a straight line with constant velocity.
If both the direction and the speed of the bullet are variable (i.e. you try to calculation speedX and speedY for the bullet), there are infinitely many solutions.
If you set a fixed direction, you simply intersect the two lines of the bullet and the target. From the distance between the current point of the target and the intersection point (and the target's speed) you can calculate the time the target will take to reach this intersection point.
From the distance between the origin of the bullet and the intersection point (and the previously calculated time) you can calculate the speed of the bullet.

Categories

Resources