3
\$\begingroup\$

I decided to play around with doing my own super-basic animation. For a previous Javascript project, I needed the ability to flash an element. The code below is the result of me attempting to generalize the main animation loop function, and learn Typescript at the same time.

What I'm looking for suggestions on:

  1. The name of my main animation loop (doOverEveryThen). It's terrible, but basically describes what the function does. Any suggestions here would be appreciated.

  2. For the defaultable parameters, I had two options:

    1. Use the "JavaScript way", but putting something like thenF = thenF || function(){} at the start of the function.
    2. Opt for the Typescript way of default parameters.

    Going with Typescript's default parameters seemed like the better way to go, but it also bulked up an already long line.

  3. The first function, my "animation loop" is the heart of the two animations. For the job it needs to do, is it optimal? Can it be improved at all in terms of efficiency or neatness?

  4. This is the first code I've written in Typescript. Any suggestions specific to the language would be appreciated.

What I don't need suggestions on:

  1. I know I could have used jQuery for a much better result. That would have defeated the purpose though.

At the bottom, I put a Stack Snippet that uses the JavaScript that the Typescript compiles into.


/**
 * Calls the frameAction function once per frame, then executes the callback.
 *
 * @param durationMS How long it should run for
 * @param frameAction A function to be executed once per frame 
 * @param thenF The callback to run after it completes
 * @param frameDelayMS The delay between frames
 */
function doOverEveryThen(durationMS: number, frameAction: (percentageComplete: number) => void, thenF: () => void = function(){}, frameDelayMS: number = 33) {
    let percPerMS = 1 / durationMS;
    let changePerFrame = percPerMS * frameDelayMS;

    let currentPerc = 0;
    let animationInterval = setInterval(function () {
        frameAction(currentPerc);

        currentPerc += changePerFrame;

        if (currentPerc >= 1) {
            clearInterval(animationInterval);

            thenF();
        }
    }, frameDelayMS);
}

/**
 * Flashes the given element over the given duration.
 *
 * @param element The element to flash
 * @param duration The length of the flash
 * @param thenF THe callback to call when the flash completes
 */
function flashElementThen(element: HTMLElement, duration: number, thenF?: () => void) {
    let originalBackground = getComputedStyle(element).backgroundColor;

    doOverEveryThen(duration, (percComplete) => {
            let whitenPerc = percComplete <= 0.5 ?
                percComplete * 2
                : (1 - percComplete) * 2;

            let changedColor = shadeRGBColor(originalBackground, whitenPerc);

            element.style.backgroundColor = changedColor;
        },

        function () {
            element.style.backgroundColor = originalBackground;

            thenF();
        }
    );
}

/**
 * "Slides" the element from it's current position to the given end coordinates.
 *
 * @param element The element to move
 * @param duration How long the move should take
 * @param xFinal The final position in the x-dimension
 * @param yFinal The final position in the y-dimension
 * @param thenF The callback to call when it completes
 */
function slideElementThen(element: HTMLElement, duration: number, xFinal: number, yFinal: number, thenF: () => void) {
    var offset = element.getBoundingClientRect();

    let startingX = offset.left;
    let startingY = offset.top;

    let xDist = xFinal - startingX;
    let yDist = yFinal - startingY;

    doOverEveryThen(duration, (percComplete) => {
        let newX = xDist * percComplete;
        let newY = yDist * percComplete;

        element.style.left = newX + "px";
        element.style.top = newY + "px";
    }, thenF);
}

/**
 * Lightens the given rgb color by the given percent.
 * Stolen from http://stackoverflow.com/a/13542669/3000206
 * 
 * @param color The CSS string in the form "rgb(#,#,#)" to lighten
 * @param percent A decimal percentage of the amount to lighten the color by
 * @returns The lightened color
 */
function shadeRGBColor(color: string, percent: number) {
    var f = color.split(","), t = percent < 0 ? 0 : 255, p = percent < 0 ? percent * -1 : percent, R = parseInt(f[0].slice(4)), G = parseInt(f[1]), B = parseInt(f[2]);
    return "rgb(" + (Math.round((t - R) * p) + R) + "," + (Math.round((t - G) * p) + G) + "," + (Math.round((t - B) * p) + B) + ")";
}

Snippet:

var box = document.getElementById('box');

slideElementThen(box, 3000, 100, 100, function () {
  flashElementThen(box, 500, function () {
    box.innerHTML = "DONE!";
  });
});

flashElementThen(box, 3000);

function doOverEveryThen(durationMS, frameAction, thenF, frameDelayMS) {
    if (thenF === void 0) { thenF = function () { }; }
    if (frameDelayMS === void 0) { frameDelayMS = 33; }
    var percPerMS = 1 / durationMS;
    var changePerFrame = percPerMS * frameDelayMS;
    var currentPerc = 0;
    var animationInterval = setInterval(function () {
        frameAction(currentPerc);
        currentPerc += changePerFrame;
        if (currentPerc >= 1) {
            clearInterval(animationInterval);
            thenF();
        }
    }, frameDelayMS);
}

function flashElementThen(element, duration, thenF) {
    var originalBackground = getComputedStyle(element).backgroundColor;
    doOverEveryThen(duration, function (percComplete) {
        var whitenPerc = percComplete <= 0.5 ?
            percComplete * 2
            : (1 - percComplete) * 2;
        var changedColor = shadeRGBColor(originalBackground, whitenPerc);
        element.style.backgroundColor = changedColor;
    }, function () {
        element.style.backgroundColor = originalBackground;
        thenF();
    });
}

function slideElementThen(element, duration, xFinal, yFinal, thenF) {
    var offset = element.getBoundingClientRect();
    var startingX = offset.left;
    var startingY = offset.top;
    var xDist = xFinal - startingX;
    var yDist = yFinal - startingY;
    doOverEveryThen(duration, function (percComplete) {
        var newX = xDist * percComplete;
        var newY = yDist * percComplete;
        element.style.left = newX + "px";
        element.style.top = newY + "px";
    }, thenF);
}

function shadeRGBColor(color, percent) {
    var f = color.split(","), t = percent < 0 ? 0 : 255, p = percent < 0 ? percent * -1 : percent, R = parseInt(f[0].slice(4)), G = parseInt(f[1]), B = parseInt(f[2]);
    return "rgb(" + (Math.round((t - R) * p) + R) + "," + (Math.round((t - G) * p) + G) + "," + (Math.round((t - B) * p) + B) + ")";
}
#box {
  border: 1px groove green;

  width: 200px;
  height: 200px;

  background-color: darkblue;

  color: white;

  position: relative;
}
<div id="box"></div>

\$\endgroup\$
3
\$\begingroup\$

My eye is drawn to the number of parameters in your main animation loop function. I'd try to reduce that.

Since this function is essentially chaining callback functions and inserting delays, the opportunity I see is to use promises to more elegantly chain the callbacks (readability is one sure benefit).

\$\endgroup\$
  • 2
    \$\begingroup\$ Thanks. Given this was posted over a year ago, and I don't even write TS anymore, I suspect this is the best answer I'm going to get. \$\endgroup\$ – Carcigenicate Jul 21 '17 at 16:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.