Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Banana sprite js animation (forward/reverse and fade) with json data

I am working on a custom scroll animation framework. Where I can control the sequence via a blob of json data.

This code here uses some subscribers -- and although the forward/reverse animations are in place --- the fade in/out is not working well - where the fades malfunction.

Using json - I want to provide the skeleton for the block (classname, height, width, background), then the actions per start/end frames which relates to the scroll value.

how do I modify the code -- to fix fading.

So in this example.

-- when the scroll is at 0 -- or start of application - create the block. enter image description here

-- if the scroll is between a range 100-400 - the scroll is instructed to move right. enter image description here

-- if the scroll hits over 400 - destroy the block.

so the animation is to take hold in a forward direction, but I want to reverse the animations in the opposite direction - so the timeline can be moved forward, backward - dependent on the speed of the scroll - so a slowmo or speedup affect can take hold

--- this is the first generation code https://jsfiddle.net/d4053upt/1/

let data = [{
    "structure": {
      "name": "square",
      "height": 30,
      "width": 30,
      "x": 0,
      "y": 0,
      "background": 'url("https://i.pinimg.com/originals/74/f3/5d/74f35d5885e8eb858e6af6b5a7844379.jpg")'
    },
    "frames": [{
      "animation": "move",
      "start": 0,
      "stop": 300,
      "startPositionX": 0,
      "startPositionY": 0,
      "endPositionX": 90,
      "endPositionY": 0,
    }, {
      "animation": "move",
      "start": 301,
      "stop": 600,
      "startPositionX": 90,
      "startPositionY": 0,
      "endPositionX": 90,
      "endPositionY": 80,
    }, {
      "animation": "move",
      "start": 601,
      "stop": 900,
      "startPositionX": 90,
      "startPositionY": 80,
      "endPositionX": 0,
      "endPositionY": 0,
    }, {
      "animation": "show",
      "start": 601,
      "stop": 9999,
      "positionX": 0,
      "positionY": 0,
    }],
  },
  {
    "structure": {
      "name": "pear",
      "height": 30,
      "width": 30,
      "x": 90,
      "y": 80,
      "background": 'url("https://i.pinimg.com/originals/74/f3/5d/74f35d5885e8eb858e6af6b5a7844379.jpg")'
    },
    "frames": [{
      "animation": "move",
      "start": 0,
      "stop": 300,
      "startPositionX": 90,
      "startPositionY": 80,
      "endPositionX": 0,
      "endPositionY": 80,
    }, {
      "animation": "move",
      "start": 301,
      "stop": 600,
      "startPositionX": 0,
      "startPositionY": 80,
      "endPositionX": 0,
      "endPositionY": 0,
    }, {
      "animation": "move",
      "start": 601,
      "stop": 900,
      "startPositionX": 0,
      "startPositionY": 0,
      "endPositionX": 90,
      "endPositionY": 80,
    }, {
      "animation": "show",
      "start": 601,
      "stop": 9999,
      "positionX": 90,
      "positionY": 80,
    }],
  }
]

let animations = {
  setup: function($container) {
    this.$container = $container;
    this.viewportWidth = $container.width();
    this.viewportHeight = $container.height();
  },
  createBlock: function(blockSpec) {
    let $block = $('<div>');
    $block.addClass(blockSpec.name);
    $block.addClass("animatedblock");
    $block.css("height", blockSpec.height);
    $block.css("width", blockSpec.width);
    $block.css("background", blockSpec.background);
    $block.css("background-size", "cover");
    this.$container.append($block);
    this.setPosition($block, blockSpec.x, blockSpec.y)
    return $block;
  },
  setPosition($block, x, y) {
    $block.css({
      left: x / 100 * this.viewportWidth,
      top: y / 100 * this.viewportHeight,
    });
  },
  moveBlock($block, frame, scrollProgress) {
    let blockPositionX = frame.startPositionX + scrollProgress * (frame.endPositionX - frame.startPositionX);
    let blockPositionY = frame.startPositionY + scrollProgress * (frame.endPositionY - frame.startPositionY);
    this.setPosition($block, blockPositionX, blockPositionY);
  },
  showBlock: function($block, frame) {
    $block.show()
    this.setPosition($block, frame.positionX, frame.positionY);
  },
  hideBlock: function($block) {
    $block.hide()
  },
}

class ScrollObserver {
  constructor() {
    let $window = $(window);
    this.STOP_DISPATCH = 'STOP_DISPATCH';
    this.subscribers = [];
    $window.scroll(event => this.dispatch($window.scrollTop()));
  }
  subscribe(subscriberFn) {
    this.subscribers.push(subscriberFn);
  }
  dispatch(scrollPosition) {
    for (let subscriberFn of this.subscribers) {
      if (subscriberFn(scrollPosition) == this.STOP_DISPATCH) break;
    }
  }
}

jQuery(function($) {
  animations.setup($('.container'));
  $(window).resize(event => animations.setup($('.container')));
  for (let obj of data) {
    let scrollObserver = new ScrollObserver();
    let blockSpec = obj.structure;
    let $block = animations.createBlock(blockSpec);
    for (let frame of obj.frames) {
      scrollObserver.subscribe(scrollPosition => {
        if (scrollPosition >= frame.start && scrollPosition <= frame.stop) {
          let scrollProgress = (scrollPosition - frame.start) / (frame.stop - frame.start);
          switch (frame.animation) {
            case 'move':
              animations.moveBlock($block, frame, scrollProgress);
              break;
            case 'show':
              animations.showBlock($block, frame);
          }
          return scrollObserver.STOP_DISPATCH;
        }
      });
    }
  }
});
body {
  height: 1500px;
}

.container {
  background: grey;
  position: fixed;
  top: 0;
  left: 0;
  height: 100vh;
  width: 100vw;
  box-sizing: content-box;
}

.animatedblock {
  position: absolute;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="container"></div>

-- this is the second generation code with current fade malfunction https://jsfiddle.net/26jkLnup/1/

let data = [{
"structure": {
  "name": "square",
  "height": 30,
  "width": 30,
  "x": 0,
  "y": 0,
  "background": 'url("https://i.pinimg.com/originals/74/f3/5d/74f35d5885e8eb858e6af6b5a7844379.jpg")'
},
"frames": [{
  "animation": "fadein",
  "start": 0,
  "stop": 300,
  "startPositionX": 0,
  "startPositionY": 0,
  "endPositionX": 90,
  "endPositionY": 0,
}, {
  "animation": "move",
  "start": 301,
  "stop": 600,
  "startPositionX": 90,
  "startPositionY": 0,
  "endPositionX": 90,
  "endPositionY": 80,
}, {
  "animation": "fadeout",
  "start": 601,
  "stop": 900,
  "positionX": 0,
  "positionY": 0,
}],
  }/*,
  {
"structure": {
  "name": "pear",
  "height": 30,
  "width": 30,
  "x": 90,
  "y": 80,
  "background": 'url("https://image.flaticon.com/icons/svg/272/272135.svg")'
},
"frames": [{
  "animation": "move",
  "start": 0,
  "stop": 300,
  "startPositionX": 90,
  "startPositionY": 80,
  "endPositionX": 0,
  "endPositionY": 80,
}, {
  "animation": "move",
  "start": 301,
  "stop": 600,
  "startPositionX": 0,
  "startPositionY": 80,
  "endPositionX": 0,
  "endPositionY": 0,
}, {
  "animation": "move",
  "start": 601,
  "stop": 900,
  "startPositionX": 0,
  "startPositionY": 0,
  "endPositionX": 90,
  "endPositionY": 80,
}, {
  "animation": "show",
  "start": 601,
  "stop": 9999,
  "positionX": 90,
  "positionY": 80,
}],
  }*/
]

let animations = {
  setup: function($container) {
this.$container = $container;
this.viewportWidth = $container.width();
this.viewportHeight = $container.height();
  },
  createBlock: function(blockSpec) {
let $block = $('<div>');
$block.addClass(blockSpec.name);
$block.addClass("animatedblock");
$block.css("height", blockSpec.height);
$block.css("width", blockSpec.width);
$block.css("background", blockSpec.background);
$block.css("background-size", "cover");
this.$container.append($block);
this.setPosition($block, blockSpec.x, blockSpec.y)
return $block;
  },
  setPosition($block, x, y) {
$block.css({
  left: x / 100 * this.viewportWidth,
  top: y / 100 * this.viewportHeight,
});
  },
  moveBlock($block, frame, scrollProgress) {
let blockPositionX = frame.startPositionX + scrollProgress * (frame.endPositionX - frame.startPositionX);
let blockPositionY = frame.startPositionY + scrollProgress * (frame.endPositionY - frame.startPositionY);
this.setPosition($block, blockPositionX, blockPositionY);
  },
  showBlock: function($block, frame) {
$block.show()
this.setPosition($block, frame.positionX, frame.positionY);
  },
  hideBlock: function($block) {
$block.hide()
  },
  fadeinBlock: function($block, frame, scrollProgress) {
 //console.log("scrollProgress", scrollProgress)

$block.css({
  opacity: 1 * scrollProgress
})
 
 
/*
$block.css({
	opacity: frame.startPositionY / 100 * this.viewportHeight
})*/
  },
  fadeoutBlock: function($block, frame, scrollProgress) {
//console.log("scrollProgress22222",scrollProgress)
/*
$block.css({
	opacity: frame.startPositionY / 100 * this.viewportHeight
})*/
$block.css({
  opacity: 1 * (1-scrollProgress)
})

  },
}

class ScrollObserver {
  constructor() {
let $window = $(window);
this.STOP_DISPATCH = 'STOP_DISPATCH';
this.subscribers = [];
$window.scroll(event => this.dispatch($window.scrollTop()));
  }
  subscribe(subscriberFn) {
this.subscribers.push(subscriberFn);
  }
  dispatch(scrollPosition) {
for (let subscriberFn of this.subscribers) {
  if (subscriberFn(scrollPosition) == this.STOP_DISPATCH) break;
}
  }
}

jQuery(function($) {
  animations.setup($('.animationcontainer'));
  $(window).resize(event => animations.setup($('.animationcontainer')));
  for (let obj of data) {
let scrollObserver = new ScrollObserver();
let blockSpec = obj.structure;
let $block = animations.createBlock(blockSpec);
for (let frame of obj.frames) {
  scrollObserver.subscribe(scrollPosition => {
    if (scrollPosition >= frame.start && scrollPosition <= frame.stop) {
      let scrollProgress = (scrollPosition - frame.start) / (frame.stop - frame.start);
      switch (frame.animation) {
        case 'move':
          animations.moveBlock($block, frame, scrollProgress);
          break;
        case 'show':
          animations.showBlock($block, frame);
          break;
        case 'fadein':
          animations.fadeinBlock($block, frame, scrollProgress);
          break;
        case 'fadeout':
          animations.fadeoutBlock($block, frame, scrollProgress);
          break;
      }
      return scrollObserver.STOP_DISPATCH;
    }
  });
}
  }
});
body {
  height: 1500px;
}

.animationcontainer {
  background: grey;
  border: 1px solid pink;
  position: fixed;
  top: 0;
  left: 0;
  height: 100vh;
  width: 100vw;
  box-sizing: content-box;
}

.animatedblock {
  position: absolute;
}

@media only screen and (min-width: 600px) {
  body {
background-color: lightblue;
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>


<body>
<div class="animationcontainer"></div>
<div class="animationcontainer"></div>
</body>
like image 244
The Old County Avatar asked Nov 06 '22 10:11

The Old County


1 Answers

There is a bit of a conceptual problem IMHO: you have fadein and fadeout animations that move the object. Not that it's impossible and I made the changes to "fix" the animation to the best of my understanding of what exactly you wanted to achieve, but that makes move animation operation kind of redundant. API-wise it seems to make more sense to encode all the things you animate into a single transition descriptor, for example first one would be:

"frames": [{
  "start": 0,
  "stop": 300,
  "startPositionX": 0,
  "startPositionY": 0,
  "endPositionX": 90,
  "endPositionY": 0,
  "startOpacity": 0,
  "endOpacity": 1
}]

I didn't change that but in order to move while fading in, I had to apply move after both fadein and fadeout. That makes them effectively fadeinandmove and fadeoutandmove.

Another change I made is initial opacity for the setup, as otherwise, you start fading in when at 100% opacity already.

Here is a fork of your snippet: https://jsfiddle.net/5hxg02d3/

Good luck with your framework!

EDIT: as you rightly noticed quickly moving back and forth breaks the sequence. Fade does hit zero for me if I drag slowly, rather than scroll back and forth, so I believe that you are referring to the same effect. That happens due to the API consistency problems I brought up earlier. Quick scroll movement allows the animation to jump from frame 1 to 3 or v.v. from 3 to 1 or even go out of the animation scope (below 0 or above 900) and as a result position is inconsistent with your expectation as updates are not called. I updated the snippet as per my initial suggestion in this fiddle: https://jsfiddle.net/twkq9jyf/1/ and will update the snippet below with it. My initial snippet is in the fiddle above for your reference.

let data = [{
"structure": {
  "name": "square",
  "height": 30,
  "width": 30,
  "x":0,
  "y":0,
  "opacity": 0,
  "background": 'url("https://i.pinimg.com/originals/74/f3/5d/74f35d5885e8eb858e6af6b5a7844379.jpg")'
},
"frames": [{
  "start": 0,
  "stop": 300,
  "startPositionX": 0,
  "startPositionY": 0,
  "endPositionX": 90,
  "endPositionY": 0,
  "startOpacity": 0,
  "endOpacity": 1
}, {
  "start": 301,
  "stop": 600,
  "startPositionX": 90,
  "startPositionY": 0,
  "endPositionX": 90,
  "endPositionY": 80,
  "startOpacity": 1,
  "endOpacity": 1
}, {
  "start": 601,
  "stop": 900,
  "startPositionX": 90,
  "startPositionY": 80,
  "endPositionX": 90,
  "endPositionY": 80,
  "startOpacity": 1,
  "endOpacity": 0
}],
  }
  ]

let animations = {
  setup: function($container) {
this.$container = $container;
this.viewportWidth = $container.width();
this.viewportHeight = $container.height();
  },
  createBlock: function(blockSpec) {
let $block = $('<div>');
$block.addClass(blockSpec.name);
$block.addClass("animatedblock");
$block.css("height", blockSpec.height);
$block.css("width", blockSpec.width);
$block.css("background", blockSpec.background);
$block.css("background-size", "cover");
$block.css("opacity", blockSpec.opacity);
this.$container.append($block);
return $block;
  },
  setPosition($block, x, y) {
$block.css({
  left: x / 100 * this.viewportWidth,
  top: y / 100 * this.viewportHeight,
});
  },
  updatePosition: function($block, frame, scrollProgress) {
let blockPositionX = frame.startPositionX + scrollProgress * (frame.endPositionX - frame.startPositionX);
let blockPositionY = frame.startPositionY + scrollProgress * (frame.endPositionY - frame.startPositionY);
this.setPosition($block, blockPositionX, blockPositionY);
  },
  updateOpacity: function($block, frame, scrollProgress) {
$block.css({
  opacity: frame.startOpacity + scrollProgress * (frame.endOpacity - frame.startOpacity)
})
  },
}

class ScrollObserver {
  constructor() {
let $window = $(window);
this.STOP_DISPATCH = 'STOP_DISPATCH';
this.subscribers = [];
$window.scroll(event => this.dispatch($window.scrollTop()));
  }
  subscribe(subscriberFn) {
this.subscribers.push(subscriberFn);
  }
  dispatch(scrollPosition) {
for (let subscriberFn of this.subscribers) {
  if (subscriberFn(scrollPosition) == this.STOP_DISPATCH) break;
}
  }
}

jQuery(function($) {
  animations.setup($('.animationcontainer'));
  $(window).resize(event => animations.setup($('.animationcontainer')));
  for (let obj of data) {
let scrollObserver = new ScrollObserver();
let blockSpec = obj.structure;
let $block = animations.createBlock(blockSpec);
for (let frame of obj.frames) {
  scrollObserver.subscribe(scrollPosition => {
    if ( (scrollPosition >= frame.start || frame.start == 0) &&
         (scrollPosition <= frame.stop || frame.stop == 900) ) {
      let scrollProgress = (scrollPosition - frame.start) / (frame.stop - frame.start);
      animations.updatePosition($block, frame, scrollProgress);
      animations.updateOpacity($block, frame, scrollProgress);
      return scrollObserver.STOP_DISPATCH;
    }
  });
}
  }
});
body {
  height: 1500px;
}

.animationcontainer {
  background: grey;
  border: 1px solid pink;
  position: fixed;
  top: 0;
  left: 0;
  height: 100vh;
  width: 100vw;
  box-sizing: content-box;
}

.animatedblock {
  position: absolute;
}

@media only screen and (min-width: 600px) {
  body {
background-color: lightblue;
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>


<body>
<div class="animationcontainer"></div>
<div class="animationcontainer"></div>
</body>

MORE: The subscriber is triggered on the scrolling action with the current scroll position, finds the first frame that matches the position, calculates the scrollProgress (0..1 parameter within the frame found) and triggers the updates of position and opacity. Besides the problem which I fixed this approach does not guarantee data correctness (there might be multiple frames covering the same scroll position and consequent frames might have an unequal end of the frame with the start of the next one). To address this, you can consider instead of storing frame with a startPosition and stopPosition to store the keyframes with the description as follows:

"frames": [{
  "key": 0,
  "x": 0,
  "y": 90,
  "opacity": 0
},
{
  "key": 300,
  "x": 90,
  "y": 0,
  "opacity": 1
}, ...]

instead of frame

"frames": [{
  "start": 0,
  "stop": 300,
  "startPositionX": 0,
  "startPositionY": 0,
  "endPositionX": 90,
  "endPositionY": 0,
  "startOpacity": 0,
  "endOpacity": 1
}, ... ]

that ensures that consequent frames have matching beginning and end states (keyframes). The number of keyframes will be equal to your number of frames+1. I suggest trying to implement this on your own as it is a simple exercise which will help to improve your understanding.

like image 159
isp-zax Avatar answered Nov 11 '22 16:11

isp-zax