2

I have 2 divs witch I need to animate:

<div class="d"></div>
<div class="p"></div>

Width of first div should become 70% and width of second div should become 30%. But when I'am trying to animate one div after another, calling at first function for 70% and then function for 30%, width of both of them become 30%.

Javascript code:

Anim({
    target: document.getElementsByClassName('d')[0],
    drawFunc: (progress, element) => {
        element.style.width = (progress * 70) + '%';
    }
});

Anim({
    target: document.getElementsByClassName('p')[0],
    drawFunc: (progress, element) => {
        element.style.width = (progress * 30) + '%';
    }
});

I don't understand why this is happening and how to make both functions work correctly.

Code snippet if needed:

(() => {
  "use strict";

  const init = (params) => {
    const start = performance.now();
    const element = params.target || null;

    requestAnimationFrame(function animate(time) {
      let timeFraction = (time - start) / params.duration;

      if (timeFraction > 1) {
        timeFraction = 1;
      }

      const progress = params.timingFunc(timeFraction, params.timingArg);

      params.drawFunc(progress, element);

      if (timeFraction < 1) {
        requestAnimationFrame(animate);
      }

      if (params.callback) {
        if (timeFraction >= 1) {
          params.callback();
        }
      }
    });
  };
  const timingFunctions = {
    linear: (timeFraction) => {
      return timeFraction;
    }
  };
  const paces = {
    easeIn: (func) => {
      return timingFunctions[func];
    }
  };
  const defaultParams = {
    duration: 1000,
    timingFunc: paces.easeIn('linear'),
    timingArg: null,
    delay: null,
    callback: null
  };
  const makeParams = (def, add) => {
    let params = def;

    if (add) {
      for (let i in add) {
        if (Object.prototype.hasOwnProperty.call(add, i)) {
          params[i] = add[i];
        }
      }
    }

    return params;
  };

  function Anim(paramArgs) {
    const params = makeParams(defaultParams, paramArgs);

    if ('timingFunc' in paramArgs) {
      params.timingFunc = (typeof paramArgs.timingFunc === 'function') ? paramArgs.timingFunc : paces[paramArgs.timingFunc.pace](paramArgs.timingFunc.func);
    }

    if (!params.delay) {
      init(params);
    } else {
      setTimeout(() => {
        init(params);
      }, params.delay);
    }
  }

  window.Anim = Anim;
})();

Anim({
  target: document.getElementsByClassName('d')[0],
  drawFunc: (progress, element) => {
    element.style.width = (progress * 70) + '%';
  }
});

Anim({
  target: document.getElementsByClassName('p')[0],
  drawFunc: (progress, element) => {
    element.style.width = (progress * 30) + '%';
  }
});
.d, .p {
    background-color: red;
    height: 50px;
    width: 0;
    margin-top: 10px;
  }
<div class="d"></div>
<div class="p"></div>
alexDev
  • 23
  • 2
  • `console.log("p1:"+progress);` will give you the answer ;) – eapo Dec 11 '18 at 23:14
  • `if (Object.prototype.hasOwnProperty.call(add, i)) {` what happens here? is the `hasOwnProperty` method called here? why not simply use is directly? – messerbill Dec 11 '18 at 23:16
  • @messerbill Yes, the hasOwnProperty method called here. I need IE8 support and direct use doesn't work there. – alexDev Dec 11 '18 at 23:23
  • :'D The good old IE.....That was clear....If you see some kind of weird looking code think about it.....hm, must be an IE fix :D Tell your customer it's really time to upgrade their browsers – messerbill Dec 11 '18 at 23:25
  • @messerbill this link show very good explanation why I wrote Object.prototype.hasOwnProperty.call instead of direct use: https://stackoverflow.com/questions/12017693/why-use-object-prototype-hasownproperty-callmyobj-prop-instead-of-myobj-hasow – alexDev Dec 11 '18 at 23:38
  • `I need IE8 support` I assume you must be transpiling then, as `let` & `const` don't work with IE8.. `Object.assign` isn't going to work either, without a polyfill. – Keith Dec 12 '18 at 00:04

1 Answers1

4

The problem is that both Anim calls have the same params object. Both params objects have the same exact callback drawFunc.

Why? Because in makeParams you are doing this:

let params = def;

Then you assign to params which in turn alters the original defaultParams (aliased here as def). When the second function calls Anim, the callback drawFunc of this second call gets assigned to the defaultParams object. Since all params objects are basically a reference to defaultParams, they get altered too, and the callback of the last call to Anim gets assigned to all of them.

To fix this, just clone def using Object.assign:

let params = Object.assign({}, def);

Side note: The target property is also altered in the params object, but before it changes, it gets assigned to a new variable inside init:

const element = params.target || null;

Thus, even though it changes in the params object, you don't really notice because all subsequent code uses the variable element instead of params.target.

Working code:

(() => {
  "use strict";

  const init = (params) => {
    const start = performance.now();
    const element = params.target || null;

    requestAnimationFrame(function animate(time) {
      let timeFraction = (time - start) / params.duration;

      if (timeFraction > 1) {
        timeFraction = 1;
      }

      const progress = params.timingFunc(timeFraction, params.timingArg);

      params.drawFunc(progress, element);

      if (timeFraction < 1) {
        requestAnimationFrame(animate);
      }

      if (params.callback) {
        if (timeFraction >= 1) {
          params.callback();
        }
      }
    });
  };
  const timingFunctions = {
    linear: (timeFraction) => {
      return timeFraction;
    }
  };
  const paces = {
    easeIn: (func) => {
      return timingFunctions[func];
    }
  };
  const defaultParams = {
    duration: 1000,
    timingFunc: paces.easeIn('linear'),
    timingArg: null,
    delay: null,
    callback: null
  };
  const makeParams = (def, add) => {
    let params = Object.assign({}, def);

    if (add) {
      for (let i in add) {
        if (Object.prototype.hasOwnProperty.call(add, i)) {
          params[i] = add[i];
        }
      }
    }

    return params;
  };

  function Anim(paramArgs) {
    const params = makeParams(defaultParams, paramArgs);

    if ('timingFunc' in paramArgs) {
      params.timingFunc = (typeof paramArgs.timingFunc === 'function') ? paramArgs.timingFunc : paces[paramArgs.timingFunc.pace](paramArgs.timingFunc.func);
    }

    if (!params.delay) {
      init(params);
    } else {
      setTimeout(() => {
        init(params);
      }, params.delay);
    }
  }

  window.Anim = Anim;
})();

Anim({
  target: document.getElementsByClassName('d')[0],
  drawFunc: (progress, element) => {
    element.style.width = (progress * 70) + '%';
  }
});

Anim({
  target: document.getElementsByClassName('p')[0],
  drawFunc: (progress, element) => {
    element.style.width = (progress * 30) + '%';
  }
});
.d, .p {
    background-color: red;
    height: 50px;
    width: 0;
    margin-top: 10px;
  }
<div class="d"></div>
<div class="p"></div>

Related issue: How do I correctly clone a JavaScript object?

ibrahim mahrir
  • 31,174
  • 5
  • 48
  • 73
  • Thank you. You saved my day. – alexDev Dec 11 '18 at 23:50
  • @alexDev One little note, use `Object.assign` instead of `Object.create`, I'm have a headache so my thinking isn't clear, sorry! Check out the updated answer! `Object.create`, although works, is suitable for prototypal inheritance not cloning. – ibrahim mahrir Dec 11 '18 at 23:56