components/RenderAnimator.js

  1. import StateMap from '../StateMap.js';
  2. import {arrayCache} from '../utils/array.js';
  3. import createComponentClass from '../factory.js';
  4. export default (function () {
  5. var createTest = function (testStates, animation) {
  6. if (testStates === 'default') {
  7. return defaultTest.bind(null, animation);
  8. } else {
  9. //TODO: Better clean-up: Create a lot of these without removing them later... DDD 2/5/2016
  10. return stateTest.bind(null, animation, StateMap.setUp(testStates));
  11. }
  12. },
  13. defaultTest = function (animation) {
  14. return animation;
  15. },
  16. methodPlay = function (animation, loop, restart) {
  17. this.component.playAnimation(animation, loop, restart);
  18. },
  19. methodStop = function (animation) {
  20. this.component.stopAnimation(animation);
  21. },
  22. stateTest = function (animation, states, ownerState) {
  23. if (ownerState.includes(states)) {
  24. return animation;
  25. }
  26. return false;
  27. },
  28. triggerPlay = function (animation, loop, restart) {
  29. /**
  30. * On entering a new animation-mapped state, this component triggers this event to play an animation.
  31. *
  32. * @event platypus.Entity#play-animation
  33. * @param animation {String} Describes the animation to play.
  34. * @param loop {Boolean} Whether to loop a playing animation.
  35. * @param restart {Boolean} Whether to restart a playing animation.
  36. */
  37. this.owner.triggerEvent('play-animation', animation, loop, restart);
  38. },
  39. triggerStop = function (animation) {
  40. /**
  41. * On attaining an animation-mapped state, this component triggers this event to stop a previous animation.
  42. *
  43. * @event platypus.Entity#stop-animation
  44. * @param animation {String} Describes the animation to stop.
  45. */
  46. this.owner.triggerEvent('stop-animation', animation);
  47. };
  48. return createComponentClass(/** @lends platypus.components.RenderAnimator.prototype */{
  49. id: 'RenderAnimator',
  50. properties: {
  51. /**
  52. * An object containg key-value pairs that define a mapping from entity states to the animation that should play. The list is processed from top to bottom, so the most important actions should be listed first (for example, a jumping animation might take precedence over an idle animation). If not specified, an 1-to-1 animation map is created from the list of animations in the sprite sheet definition using the animation names as the keys.
  53. *
  54. * "animationStates":{
  55. * "standing": "default-animation" // On receiving a "standing" event, or when this.owner.state.standing === true, the "default" animation will begin playing.
  56. * "ground,moving": "walking", // Comma separated values have a special meaning when evaluating "state-changed" messages. The above example will cause the "walking" animation to play ONLY if the entity's state includes both "moving" and "ground" equal to true.
  57. * "ground,striking": "swing!", // Putting an exclamation after an animation name causes this animation to complete before going to the next animation. This is useful for animations that would look poorly if interrupted.
  58. * "default": "default-animation" // Optional. "default" is a special property that matches all states. If none of the above states are valid for the entity, it will use the default animation listed here.
  59. * }
  60. *
  61. * @property animationStates
  62. * @type Object
  63. * @default null
  64. */
  65. animationStates: null,
  66. /**
  67. * An object containg key-value pairs that define a mapping from triggered events to the animation that should play.
  68. *
  69. * "animationEvents":{
  70. * "move": "walk-animation",
  71. * "jump": "jumping-animation"
  72. * }
  73. *
  74. * The above will create two event listeners on the entity, "move" and "jump", that will play their corresponding animations when the events are triggered.
  75. *
  76. * @property animationEvents
  77. * @type Object
  78. * @default null
  79. */
  80. animationEvents: null,
  81. /**
  82. * Sets a component that this component should be connected to.
  83. *
  84. * @property component
  85. * @type Component
  86. * @default null
  87. */
  88. component: null,
  89. /**
  90. * Optional. Forces animations to complete before starting a new animation. Defaults to `false`.
  91. *
  92. * @property forcePlayThrough
  93. * @type Boolean
  94. * @default false
  95. */
  96. forcePlayThrough: false,
  97. /**
  98. * Whether to restart a playing animation on event.
  99. *
  100. * @property restart
  101. * @type Boolean
  102. * @default true
  103. */
  104. restart: true,
  105. /**
  106. * Whether to loop a playing animation on event.
  107. *
  108. * @property loop
  109. * @type Boolean
  110. * @default false
  111. */
  112. loop: false
  113. },
  114. /**
  115. * This component is typically added to an entity automatically by a render component. It handles mapping entity states and events to playable animations.
  116. *
  117. * @memberof platypus.components
  118. * @uses platypus.Component
  119. * @constructs
  120. * @listens platypus.Entity#animation-ended
  121. * @listens platypus.Entity#state-changed
  122. * @listens platypus.Entity#update-animation
  123. * @fires platypus.Entity#play-animation
  124. * @fires platypus.Entity#stop-animation
  125. */
  126. initialize: (function () {
  127. const
  128. trigger = function (animation, loop, restart) {
  129. this.override = animation;
  130. this.owner.triggerEvent('play-animation', animation, loop, restart);
  131. },
  132. method = function (animation, loop, restart) {
  133. this.override = animation;
  134. this.playAnimation(animation, loop, restart);
  135. };
  136. return function () {
  137. const
  138. events = this.animationEvents,
  139. states = this.animationStates;
  140. //Handle Events:
  141. this.override = false;
  142. if (events) {
  143. for (const animation in events) {
  144. if (events.hasOwnProperty(animation)) {
  145. if (this.component) {
  146. this.addEventListener(animation, method.bind(this.component, events[animation], this.loop, this.restart));
  147. } else {
  148. this.addEventListener(animation, trigger.bind(this, events[animation], this.loop, this.restart));
  149. }
  150. }
  151. }
  152. }
  153. //Handle States:
  154. this.followThroughs = {};
  155. this.checkStates = arrayCache.setUp();
  156. this.state = this.owner.state;
  157. this.stateChange = true; //Check state against entity's prior state to update animation if necessary on instantiation.
  158. this.lastState = -1;
  159. if (states) {
  160. for (const anim in states) {
  161. if (states.hasOwnProperty(anim)) {
  162. const animation = states[anim];
  163. //TODO: Should probably find a cleaner way to accomplish this. Maybe in the animationMap definition? - DDD
  164. if (animation[animation.length - 1] === '!') {
  165. animation = animation.substring(0, animation.length - 1);
  166. this.followThroughs[animation] = true;
  167. } else {
  168. this.followThroughs[animation] = false;
  169. }
  170. this.checkStates.push(createTest(anim, animation));
  171. }
  172. }
  173. }
  174. this.waitingAnimation = false;
  175. this.waitingState = 0;
  176. this.playWaiting = false;
  177. this.animationFinished = false;
  178. if (this.component) {
  179. this.playAnimation = methodPlay;
  180. this.stopAnimation = methodStop;
  181. } else {
  182. this.playAnimation = triggerPlay;
  183. this.stopAnimation = triggerStop;
  184. }
  185. };
  186. } ()),
  187. events: {
  188. "state-changed": function () {
  189. this.stateChange = true;
  190. },
  191. "animation-ended": function (animation) {
  192. if (animation === this.currentAnimation) {
  193. if (this.override && (animation === this.override)) {
  194. this.stateChange = true;
  195. this.override = false;
  196. }
  197. if (this.waitingAnimation) {
  198. this.currentAnimation = this.waitingAnimation;
  199. this.waitingAnimation = false;
  200. this.lastState = this.waitingState;
  201. this.animationFinished = false;
  202. this.playAnimation(this.currentAnimation);
  203. } else {
  204. this.animationFinished = true;
  205. }
  206. }
  207. },
  208. "update-animation": function (playing) {
  209. var i = 0,
  210. testCase = false;
  211. if (this.stateChange && !this.override) {
  212. if (this.state.has('visible')) {
  213. this.visible = this.state.get('visible');
  214. }
  215. for (i = 0; i < this.checkStates.length; i++) {
  216. testCase = this.checkStates[i](this.state);
  217. if (testCase) {
  218. if (this.currentAnimation !== testCase) {
  219. if (!this.followThroughs[this.currentAnimation] && (!this.forcePlaythrough || (this.animationFinished || (this.lastState >= +i)))) {
  220. this.currentAnimation = testCase;
  221. this.lastState = +i;
  222. this.animationFinished = false;
  223. if (playing) {
  224. this.playAnimation(this.currentAnimation);
  225. } else {
  226. this.stopAnimation(this.currentAnimation);
  227. }
  228. } else {
  229. this.waitingAnimation = testCase;
  230. this.waitingState = +i;
  231. }
  232. } else if (this.waitingAnimation && !this.followThroughs[this.currentAnimation]) {// keep animating this animation since this animation has already overlapped the waiting animation.
  233. this.waitingAnimation = false;
  234. }
  235. break;
  236. }
  237. }
  238. this.stateChange = false;
  239. }
  240. }
  241. },
  242. methods: {
  243. toJSON: function () { // This component is added by another component, so it shouldn't be returned for reconstruction.
  244. return null;
  245. },
  246. destroy: function () {
  247. arrayCache.recycle(this.checkStates);
  248. this.checkStates = null;
  249. this.followThroughs = null;
  250. this.state = null;
  251. }
  252. }
  253. });
  254. }());