Search code examples
pythonmathpygame2d-games

How can I relatively scale something between two points?


This is more of a math question, and I couldn't seem to find any answers online.

So here is what I am trying to accomplish:

Lets say I have a circle, starting at Ay. Now as this circle moves towards By, I want it to scale proportionally to a certain size.

So for example, if the circle's diameter was 5 at Ay, how could I scale it to become 52.2 by the time it reaches By.

enter image description here

And bonus question: could I achieve this same thing with a square?


Solution

  • Tweening in terms of position.

    Keys and keyFrames

    In animation we define known positions and states as key frames, normally we index the key frames in terms of time.

    // an array of keys. The ? represents the applicable number value
    var keys = [{ 
             time : 0, // the state of an object at time 0
             pos : {x : ? , y : ? }, // position
             scale : ?,
             rotation : ?,
             colour : [?,?,?],       // rgb colour, just for the hell of it
             // and whatever else you may want to animate
         },{
             time : 100, // the state of the object at time 100
             pos : {x : ? , y : ? },
             scale : ?,
             rotation : ?,
             colour : [?,?,?],
             // and whatever else you may want to animate
         }
    ]
    

    Normalised time

    To get the state of an object at any time t between the key frames we find the normalised time ( a value from 0 to 1) between the times and multiply that to the difference between other states then add that to the beginning state.

    So say the time is 50 first we get the normalised time

    var currentTime = 50;
    var timeDif = currentTime - keys[0].time; // difference from start time to current
    // to get the normalised time divid by the differance
    var normTime = timeDif / (keys[1].time - keys[0].time); // divide by the differance in time between keys
    

    Now you have the normalised time you can easily calculate any of the states

    var scaleDif =  keys[1].scale - keys[0].scale; // get diff in scale
    var scaleChange = scaleDif * normTime;  // multiply by the normalised time
    var currentScale = keys[0].scale + scaleChange; // add to the starting scale
    

    That is all a little long winded but that is to ease you into what is happening. the complete keying function would look like.

    function tweenKeys(time,key1,key2){
        var nt = (time - key1.time) / (key2.time - key1.time); // get normalised time
        // because you can not divide by zero we need a little check. Javascript return infinity if we div by zero but we want the value 0 
        nt = nt < Infinity ? nt : 0; // zero if there was a divide by zero
        var ck = {}; // ck for current key. the key represents the state at time
        ck.scale = key1.scale + (key2.scale - key1.scale) * nt;
        ck.rotation = key1.rotation + (key2.rotation - key1.rotation ) * nt;
        ck.pos.x = key1.pos.x + (key2.pos.x- key1.pos.x) * nt;
        ck.pos.y = key1.pos.y + (key2.pos.y- key1.pos.y) * nt;
        ck.colour[0] = key1.colour[0] + (key2.colour[0] - key1.colour[0]) * nt;
        ck.colour[1] = key1.colour[1] + (key2.colour[1] - key1.colour[1]) * nt;
        ck.colour[2] = key1.colour[2] + (key2.colour[2] - key1.colour[2]) * nt;
        return ck; // return the newly create state
    }
    

    Thats the basics of keyframing and you can find more on it in this answer How would I animate... ?

    In space rather than time

    All good but for your problem this has not helped, you are not using time you are using position to determine the current state of the object. Well it does not matter what we use to find our current state, any of the values in the key frame can be used to determine that state of all the others. All we need to do is find the normalised difference and then apply that like we did normalised time to all the other values.

    Normalised position

    So lets look at position

    Consider two points p1 and p2, defined as

    var p1 = {x : ?, y : ?}; // ? represent some number value
    var p2 = {x : ?, y : ?}; // ? represent some number value
    

    And representing your positions A,B

    If we have a 3rd point C

    var c = {x : ?, y : ?}; // ? represent some number value
    

    somewhere on the 2D plane. We want a formula that will return a 0 when C is at point p1 and 1 when the point c is at point p2. This will be our normalised position used to get the current state.

    As position is 2d we need to involve both the x and y in the calculations. We get the distance from p1 to point c and the divide that by the distance between point p1 and p2. that will give us the value we want. To find the distance we use the pythag solution. root of the sum of the squares

    var dist = Math.sqrt( Math.pow( p2.x - p1.x, 2) + Math.pow( p2.y - p1.y, 2)); // for the twisted world of IE users and
    var dist = Math.hypot(p2.x - p1.x, p2.y - p1.y); // for all good browsers
    

    So the normalised distance is

    var normDist = Math.hypot(c.x - p1.x, c.y - p1.y) / Math.hypot(p2.x - p1.x, p2.y - p1.y); 
    // because you can not divide by zero we need a little check. Javascript returns infinity if we div by zero but we want the value 0 
    normDist = normDist < Infinity ? normDist : 0; // zero if there was a divide by zero
    

    Then apply that (normDist) to all the key states.

    var currentScale = (keys[1].scale - keys[0].scale) * normDist + keys[0].scale;
    

    Problems with positioning

    Ok you say thanks, sorry but that is not the solution, it would be if you knew that the point c is always on the line between p1, p2 but that is not always the case, and under a strict examination it is hardly ever because computers store digital information so there will be a little error in any calculation that requires very fine detail. Also the above method will return 1 for normalized distance for any point that is distance to p2 away from p1, that describes a circle around the point p1. We need to constrain this value a bit more. Also if c is befor the point p1 or after the point p2 it would be handy to know. Thus we can use the following to do so.

    // get the unit distance on the line p1,p2 of point c representing 
    // the distance along the line that is closest to c
    function unitDistOfPoint(p1,p2,c){
        var v1 = {}; // working vectors
        var v2 = {}; 
        v1.x = p2.x - p1.x; // vector between p1,p2
        v1.y = p2.y - p1.y;
        v2.x = c.x - p1.x;  // vector to c from p1
        v2.y = c.y - p1.y;
        // a little math magic. Divide the dot product of the vectors v2, v1
        // by the square of line length
        return (v2.x * v1.x + v2.y * v1.y) / (v1.y * v1.y + v1.x * v1.x);
    }
    

    Now we can do the tweening and get your scale

    // return the state for a object at point c in terms of key1, to key2
    function tweenKeysViaPos(c,key1,key2){
        // get the normalised distance of the point c between keys 1 and 2
        var nd = unitDistOfPoint(c, key1.pos, key2.pos); // nd for normalised distance
        // you may want to constrain the position to only between the points 
        // do that by clamping the value nd between 0 and 1 inclusive
        nd = Math.max(0, Math.min(1, nd)); // clamp the normalise distance
        var ck = {}; // ck for current key. the key represents the state at time
        ck.scale = key1.scale + (key2.scale - key1.scale) * nt;
        ck.rotation = key1.rotation + (key2.rotation - key1.rotation ) * nt;
        ck.pos.x = key1.pos.x + (key2.pos.x- key1.pos.x) * nt;
        ck.pos.y = key1.pos.y + (key2.pos.y- key1.pos.y) * nt;
        ck.colour[0] = key1.colour[0] + (key2.colour[0] - key1.colour[0]) * nt;
        ck.colour[1] = key1.colour[1] + (key2.colour[1] - key1.colour[1]) * nt;
        ck.colour[2] = key1.colour[2] + (key2.colour[2] - key1.colour[2]) * nt;
        return ck; // return the newly create state
    }
    

    That is the answer. As a side benefit if the point c does stray away from the line between the keys then the above function also return the position it should be.

    For more if needed

    You may want to extend this to adapt to many key frames. Normally for more than two key frames and using time it is easy to find the keys that we want by finding where time is greater than the first key and less than the next key. But this is not as simple if you are using the position to work out at which key you are at. So to help a more complex solution you will find this function handy

    // returns the distance point c is from the line p1,p2. If on the line
    // the the return value is 0. If befor point p1 or after p2 then the distance
    // is the distance to p1, or p2 respectively
    function distFromLine(p1,p2,c){
        var v1 = {}; // working vectors
        var v2 = {}; 
        v1.x = p2.x - p1.x; // vector between p1,p2
        v1.y = p2.y - p1.y;
        v2.x = c.x - p1.x;  // vector to c from p1
        v2.y = c.y - p1.y;
        // a little math magic. Divide the dot product of the vectors v2, v1
        // by the square of line length
        var u = (v2.x * v1.x + v2.y * v1.y) / (v1.y * v1.y + v1.x * v1.x);
        var v3 = {};
        if(u < 0){ // befor the start
            return Math.hypot(v2.x,v2.y); // distance to p1
        }
        if(u > 1){ // after end
            return Math.hypot(c.x - p2.x,c.y p2.y); // distance to p2
        }
        // get the point on the line that is closest
        v3.x = p1.x + v1.x * u;
        v3.y = p1.y + v1.y * u;
        // return the distance from that point to c
        return Math.hypot(c.x - v3.x,c.y - v3.y); // distance from line of c
    }
    

    You can then find the two keys you need by finding the keys that return the smallest distance from the line between them. You and then define a complicated line by defining many key frames and where ever you put an object you can calculate where it should be and in what state.

    Hope this helps and did not go over the top. If anything is unclear to anyone that reads please do say so in the comments and I will clarify.