How would I animate the process of a transformed canvas image going back into its original state?

前端 未结 2 1712
星月不相逢
星月不相逢 2021-01-28 04:31

I am at a transform of (1, 0, 0, -0.7, 0.4, 320, 70) and I want to gradually end up at (1, 0, 0, 1, 0, 0), how do I do this?

This is the code that transforms the image

相关标签:
2条回答
  • 2021-01-28 05:11

    Tweening.

    Most animations involve key frames. At its most simple you start at one state and over time you progress to the next.

    Simple tweening (AKA lerp)

    For example we have some key frames with a value and a time.

    var keys[  // time in seconds
       {value : 10, time : 0},  
       {value : 20, time : 30},
    }
    

    At some time we want what the value should be. So ignoring times outside the range we can write a simple function that gets the value for a given time. It first converts the time to a normalised time (0 to 1) between the keys, where 0 is time at first key and 1 is time at second key.

    function lerpKeys(time, fromKey, toKey){
        var relativeTime = time - fromKey.time;  
        var timeDiferance  = toKey.time - fromKey.time;
        var normalisedTime = relativeTime / timeDiferance;
        var valueDiferance = toKey.value - fromKey.value; 
        var currentValue = valueDiferance * normalisedTime + fromKey.value;
        return currentValue;
    }
    

    That is the detailed example and can be simplified to

    function lerpKeys (time, fromKey, toKey){
        var nt = (time - fromKey.time) / (toKey.time - fromKey.time); // normalised time
        return (toKey.value - fromKey.value) * nt + fromKey.value;
    }
    

    And easing

    The beauty of doing it this way is that the normalised time can also have one of many easing functions applied to it. A easing function takes a value from 0 to 1 and returns a new value from 0 to 1 but puts a curve in place of the linear line.

    An example of easing functions

    // ease in out
    function ease (value, strength) {  // strength is the amount of easing 1= no easing 
                                       //  1  < starts slow to fast then back to slow
                                       //  0 < 1 fast to slow to fast
        var v = Math.pow(Math.min(1, Math.max(0, value )), strength);
        return v / (v + Math.pow(1 - value, strength));
    }
    
    // linear (no easing just clamps the value)
    function linear (value){
        return Math.max(0, Math.min(1, value));
    }
    

    To use it (note that the ease function clamps the value to the range 0 to 1 so that the if the time is outside the range the animation stops

    // time in seconds
    // from and to keys
    // ease function to use
    function lerpKeysEase(time, fromKey, toKey, easeFunc){
        var nt = easeFunc((time - fromKey.time) / (toKey.time - fromKey.time), 2); // normalised time
        return (toKey.value - fromKey.value) * nt + fromKey.value;
    }
    var currentValue  = lerpKeysEase(time, keys[0], keys[1], ease);
    

    Most of the common easing function can be found at Github simple easing

    Update

    Oh I should have read the above github page before posting as the functions are just variations on the ease in out function in the snippet above. An excellent easing function page Easing examples and code and another page for a quick visual easing referance

    The Transform

    So that is the basics of tweening and is easy to adapt for things like positions and transforms.

    // keys with transforms
    var keys[  // time in seconds
       {value : [1, 0, -0.7, 0.4, 320, 70] , time : 0},  
       {value : [1, 0, 0, 1, 0, 0] , time : 30},
    }
    
    // lerp the transform
    function lerpTranformKeysEase(time, fromKey, toKey, easeFunc){
        var f = fromKey.value; // to save some typing
        var t = toKey.value;
        var i = 0;
        var nt = easeFunc((time - fromKey.time) / (toKey.time - fromKey.time), 2); // normalised time
        // return an array with the new transform
        return [
            (t[i] - f[i]) * nt + f[i++],
            (t[i] - f[i]) * nt + f[i++],
            (t[i] - f[i]) * nt + f[i++],
            (t[i] - f[i]) * nt + f[i++],
            (t[i] - f[i]) * nt + f[i++],
            (t[i] - f[i]) * nt + f[i++]
        ];
    }
    

    All rather simple realy... BUT....

    A better lerp (fixing the disfigured)

    The transformation matrix has some special properties and encodes many things like position, rotation, scale X/Y, skew, shearing and applying a simple lerp (tween) function will not get the correct results as the object being transformed will be deformed.

    What you need to do is decompose the transform into a more usable form. This simple means take a normal transform and return the various encoded values we want to animate. The transform has 3 basic parts, the X axis (first two values), Y axis (second two values), and origin (last two). The X,Y axis have a direction and a scale, and the origin is just a simple coordinate.

    So lets create a decompose function that returns an array that holds the x,y axis direction, x,y scales and the origin

    // NOTE Math.hypot is not supported on all browsers use 
    // Math.sqrt(matrix[1] * matrix[1] +  matrix[0] * matrix[0])
    // if needed
    function matDecompose(matrix){  // matrix as array
         return [
              Math.atan2(matrix[1], matrix[0]), // x axis direction
              Math.atan2(matrix[3], matrix[2]), // y axis direction
              Math.hypot(matrix[1], matrix[0]), // x axis scale
              Math.hypot(matrix[3], matrix[2]), // y axis scale
              matrix[4], matrix[5]   // origin
         ];  
    }
    

    Now that we have this function we can convert the transform to the decomposed values and put them in the keys

    var keys[  // time in seconds
       {value : matDecompose([1, 0, -0.7, 0.4, 320, 70]) , time : 0},  
       {value : matDecompose([1, 0, 0, 1, 0, 0]) , time : 30},
    }
    

    Then you just tween the keys as before, but this time the rotations, scales, and skewing will more accurately be tweened.

    Of course the decomposed matrix is of no use so we need to convert it back to a usable transformation matrix.

    // Returns a matrix as array from the decomposed matrix
    function reCompose(m) { // m is the decomposed matrix as array
        return [
           Math.cos(m[0]) * m[2],  // reconstruct X axis and scale
           Math.sin(m[0]) * m[2],
           Math.cos(m[1]) * m[3],  // reconstruct Y axis and scale
           Math.sin(m[1]) * m[3],
           m[4], m[5]  // origin
       ];
    }
    

    Now you can apply the tween to get the new (decomposed transform) and covert it back to the standard matrix to apply to the canvas

    var currentMatrix  = reCompose( lerpTranformKeysEase(time, keys[0], keys[1], ease));
    ctx.setTransform(
        currentMatrix[0],
        currentMatrix[1],
        currentMatrix[2],
        currentMatrix[3],
        currentMatrix[4],
        currentMatrix[5]
    );
    // Now render the object.
    

    So now you have the start of a very handy keyframe animation interface. With a little more logic for multiple key frames you can do any animation you wish, now the problem will be where to get the keyframes from ????

    0 讨论(0)
  • 2021-01-28 05:15

    I used some of your code and added an animation loop. You could use setTimeout in place of requestAnimationFrame. setTimeout(animationLoop, milliseconds);

    document.addEventListener("DOMContentLoaded", function(event) {
      image = new Image();
      image2 = new Image();
      image3 = new Image();
      image4 = new Image();
    
    
      window.onload = function() {
        //first image
        var width = image.width,
          height = image.height;
        canvas1 = document.getElementById("num1Canvas");
        bottomSlice = canvas1.getContext("2d");
        //second image
        var width2 = image2.width,
          height2 = image2.height;
        canvas2 = document.getElementById("num2Canvas");
        topSlice = canvas2.getContext("2d");
        //third image
        newCanvas1 = document.getElementById("newNum1Canvas");
        newBottomSlice = newCanvas1.getContext("2d");
        //fourth image
        newCanvas2 = document.getElementById("newNum2Canvas");
        newTopSlice = newCanvas2.getContext("2d");
    
        var i = 0;
        function animationLoop() {
          
          if (i > height / 2) {
            alert('done!');
            return;
          }
          
          //first image transform
          bottomSlice.setTransform(1, 0, -0.7, .4, 320, 70);
          bottomSlice.drawImage(image,
            0, height / 2 - i, width, 2,
            0, height / 2 - i, width, 2);
          bottomSlice.setTransform(1, 0, -0.7, 0.4, 320, 70);
          bottomSlice.drawImage(image2,
            0, height / 2 + i, width, 2,
            0, height / 2 + i, width, 2);
          //second image transform 
          topSlice.setTransform(1, 0, -0.7, .4, 320, 0.2);
          topSlice.drawImage(image3,
            0, height2 / 2 - i, width2, 2,
            0, height2 / 2 - i, width2, 2);
          topSlice.setTransform(1, 0, -0.7, 0.4, 320, 0.2);
          topSlice.drawImage(image4,
            0, height2 / 2 + i, width2, 2,
            0, height2 / 2 + i, width2, 2);
          
          i++;
          requestAnimationFrame(animationLoop);
        }
        animationLoop();
    
      };
      
      var can = document.createElement('canvas');
      var w = can.width=300;
      var h = can.height=300;
      var ctx = can.getContext('2d');
      
      ctx.fillStyle="red";
      ctx.fillRect(0,0,w,h);
      image.src = can.toDataURL("image/png");//"bottom.png";
      
      ctx.fillStyle="blue";
      ctx.fillRect(0,0,w,h);
      image2.src = can.toDataURL("image/png");//"top.png";
      
      ctx.fillStyle="green";
      ctx.fillRect(0,0,w,h);
      image3.src = can.toDataURL("image/png");//"bottom.png";
      
      ctx.fillStyle="black";
      ctx.fillRect(0,0,w,h);
      image4.src = can.toDataURL("image/png");//"top.png";
    });
    <canvas id="num1Canvas"></canvas>
    <canvas id="num2Canvas"></canvas>
    <canvas id="newNum1Canvas"></canvas>
    <canvas id="newNum2Canvas"></canvas>

    0 讨论(0)
提交回复
热议问题