[IMP] website: allow better animations thanks to requestAnimationFrame
This commit extends the behavior of the Animation class of the
"web_editor.snippets.animation" module.
Now, all animations can easily define an animation algo update without
much caring about performances. All that must be done is:
* If the "start"/"stop" method is/are defined, do not forget to call
the _super parent method (which will now initializes the animation
components).
* Define particulary-named method in your Animation class extension.
See in-code comment of the _prepareComponents method.
This extension should be used to define all animations. For example the
use of $(window).scroll(function () { /* do stuff */ }) is now the same
as defining the function with on_scroll: function () { /* do stuff */ }
except that the later one will be more efficient, smoother and allow
you to not care about throttling / event binding/unbinding / ...
This commit is contained in:
@@ -2,12 +2,279 @@ odoo.define('website.snippets.animation', function (require) {
|
||||
'use strict';
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var Class = require('web.Class');
|
||||
var core = require('web.core');
|
||||
var base = require('web_editor.base');
|
||||
var animation = require('web_editor.snippets.animation');
|
||||
|
||||
var qweb = core.qweb;
|
||||
|
||||
// Initialize fallbacks for the use of requestAnimationFrame, cancelAnimationFrame and performance.now()
|
||||
window.requestAnimationFrame = window.requestAnimationFrame
|
||||
|| window.webkitRequestAnimationFrame
|
||||
|| window.mozRequestAnimationFrame
|
||||
|| window.msRequestAnimationFrame
|
||||
|| window.oRequestAnimationFrame
|
||||
|| function (callback) { window.setTimeout(callback, 10); };
|
||||
|
||||
window.cancelAnimationFrame = window.cancelAnimationFrame
|
||||
|| window.webkitCancelAnimationFrame
|
||||
|| window.mozCancelAnimationFrame
|
||||
|| window.msCancelAnimationFrame
|
||||
|| window.oCancelAnimationFrame
|
||||
|| function (id) { window.clearTimeout(id); };
|
||||
|
||||
if (!window.performance || !window.performance.now) window.performance = {now: function () { return Date.now(); }};
|
||||
|
||||
/**
|
||||
* The AnimationComponent class in in charge of handling one animation loop using the requestAnimationFrame
|
||||
* feature. This is used by the Animation class below and should not be called directly by an end developer.
|
||||
* The component contains a simple API, it can be started, stopped, played and paused.
|
||||
*/
|
||||
var AnimationComponent = Class.extend({
|
||||
init: function (parent, updateCallback, startEvents, $startTarget, options) {
|
||||
this.parent = parent; // The Animation class instance which is using this component
|
||||
|
||||
options = options || {};
|
||||
this._minFrameTime = 1000 / (options.maxFPS || 100);
|
||||
|
||||
// Initialize the animation startEvents, startTarget, endEvents, endTarget and callbacks
|
||||
this._updateCallback = updateCallback || function () {};
|
||||
this.startEvents = startEvents || "scroll";
|
||||
this.$startTarget = $($startTarget || window);
|
||||
this._getStateCallback = options.getStateCallback
|
||||
|| ((this.startEvents === "scroll" && this.$startTarget[0] === window) ? function () { return window.pageYOffset; } : false)
|
||||
|| ((this.startEvents === "resize" && this.$startTarget[0] === window) ? function () { return {width: window.innerWidth, height: window.innerHeight}; } : false)
|
||||
|| function () { return undefined; };
|
||||
this.endEvents = options.endEvents || false;
|
||||
this.$endTarget = options.$endTarget ? $(options.$endTarget) : this.$startTarget;
|
||||
|
||||
// Add a namespace to events using the generated uid
|
||||
this._uid = "_animationComponent" + _.uniqueId();
|
||||
this.startEvents = _processEvents(this.startEvents, this._uid);
|
||||
if (this.endEvents) {
|
||||
this.endEvents = _processEvents(this.endEvents, this._uid);
|
||||
}
|
||||
|
||||
function _processEvents(events, namespace) {
|
||||
events = events.split(" ");
|
||||
return _.each(events, function (e, index) {
|
||||
events[index] += ("." + namespace);
|
||||
}).join(" ");
|
||||
}
|
||||
},
|
||||
/**
|
||||
* The start method initializes when the animation must be played and paused and initializes the
|
||||
* animation first frame.
|
||||
*/
|
||||
start: function () {
|
||||
// Initialize the animation first frame
|
||||
this._paused = false;
|
||||
this._rafID = window.requestAnimationFrame((function (t) {
|
||||
this._update(t);
|
||||
this._paused = true;
|
||||
}).bind(this));
|
||||
|
||||
// Initialize the animation play/pause events
|
||||
if (this.endEvents) {
|
||||
/**
|
||||
* If there are endEvents, the animation should begin playing when the startEvents are triggered
|
||||
* on the startTarget and pause when the endEvents are triggered on the endTarget.
|
||||
*/
|
||||
this.$startTarget.on(this.startEvents, (function (e) {
|
||||
if (this._paused) {
|
||||
_.defer(this.play.bind(this, e));
|
||||
}
|
||||
}).bind(this));
|
||||
this.$endTarget.on(this.endEvents, (function () {
|
||||
if (!this._paused) {
|
||||
_.defer(this.pause.bind(this));
|
||||
}
|
||||
}).bind(this));
|
||||
} else {
|
||||
/**
|
||||
* Else, if there is no endEvent, the animation should begin playing when the startEvents are
|
||||
* *continuously* triggered on the startTarget or fully played once. To achieve this, the animation
|
||||
* begins playing and is scheduled to pause after 2 seconds. If the startEvents are triggered during
|
||||
* that time, the schedule is delayed for another 2 seconds. This allows to describe an "effect"
|
||||
* animation (which lasts less than 2 seconds) or an animation which must be playing *during* an event
|
||||
* (scroll, mousemove, resize, repeated clicks, ...).
|
||||
*/
|
||||
var pauseTimer = null;
|
||||
this.$startTarget.on(this.startEvents, _.throttle((function (e) {
|
||||
this.play(e);
|
||||
|
||||
clearTimeout(pauseTimer);
|
||||
pauseTimer = _.delay((function () {
|
||||
this.pause();
|
||||
pauseTimer = null;
|
||||
}).bind(this), 2000);
|
||||
}).bind(this), 250, {trailing: false}));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* The stop method pauses the animation and destroys the attached events which trigger the
|
||||
* animation to be played or paused.
|
||||
*/
|
||||
stop: function () {
|
||||
this.$startTarget.off(this.startEvents);
|
||||
if (this.endEvents) {
|
||||
this.$endTarget.off(this.endEvents);
|
||||
}
|
||||
this.pause();
|
||||
},
|
||||
/**
|
||||
* The play method forces the requestAnimationFrame loop to start.
|
||||
* @param e -> the event which triggered the animation to play
|
||||
*/
|
||||
play: function (e) {
|
||||
this._newEvent = e;
|
||||
if (!this._paused) return;
|
||||
this._paused = false;
|
||||
this._rafID = window.requestAnimationFrame(this._update.bind(this));
|
||||
this._lastUpdateTimestamp = undefined;
|
||||
},
|
||||
/**
|
||||
* The pause method forces the requestAnimationFrame loop to stop.
|
||||
*/
|
||||
pause: function () {
|
||||
if (this._paused) return;
|
||||
this._paused = true;
|
||||
window.cancelAnimationFrame(this._rafID);
|
||||
this._lastUpdateTimestamp = undefined;
|
||||
},
|
||||
/**
|
||||
* The private _update method is the callback which is repeatedly called by the requestAnimationFrame
|
||||
* loop. It controls the max fps at which the animation is running and initializes the values that
|
||||
* the update callback needs to describe the animation (state, elapsedTime, triggered event).
|
||||
* @param timestamp -> the DOMHighResTimeStamp timestamp at which the function is called
|
||||
*/
|
||||
_update: function (timestamp) {
|
||||
if (this._paused) return;
|
||||
this._rafID = window.requestAnimationFrame(this._update.bind(this));
|
||||
|
||||
// Check the elapsed time since the last update callback call.
|
||||
// Consider it 0 if there is no info of last timestamp and leave this _update call if it was
|
||||
// called too soon (would overflow the set max FPS).
|
||||
var elapsedTime = 0;
|
||||
if (this._lastUpdateTimestamp) {
|
||||
elapsedTime = timestamp - this._lastUpdateTimestamp;
|
||||
if (elapsedTime < this._minFrameTime) return;
|
||||
}
|
||||
|
||||
// Check the new animation state thanks to the get state callback and store its new value.
|
||||
// If the state is the same as the previous one, leave this _update call, except if there
|
||||
// is an event which triggered the "play" method again.
|
||||
var animationState = this._getStateCallback.call(this.parent, elapsedTime, this._newEvent);
|
||||
if (!this._newEvent && animationState !== undefined && _.isEqual(animationState, this._animationLastState)) return;
|
||||
this._animationLastState = animationState;
|
||||
|
||||
// Call the update callback with frame parameters
|
||||
this._updateCallback.call(this.parent, this._animationLastState, elapsedTime, this._newEvent);
|
||||
this._lastUpdateTimestamp = timestamp; // Save the timestamp at which the update callback was really called
|
||||
this._newEvent = undefined; // Forget the event which triggered the last "play" call
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Extend the Animation class to be able to register AnimationComponent's automatically according to
|
||||
* the methods it defines. Also allows to register them in more conventional ways by extending the
|
||||
* _prepareComponents method and calling the _addComponent method.
|
||||
*/
|
||||
animation.Class.include({
|
||||
maxFPS: 100, // The max FPS at which all the automatic animation components will be running by default
|
||||
|
||||
/**
|
||||
* Extend the start method to prepare animation components and start them.
|
||||
*/
|
||||
start: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this._prepareComponents();
|
||||
_.each(this._animationComponents, function (component) {
|
||||
component.start();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Extend the start method to stop animation components and destroy them.
|
||||
*/
|
||||
stop: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
_.each(this._animationComponents, function (component) {
|
||||
component.stop();
|
||||
});
|
||||
this._animationComponents = [];
|
||||
},
|
||||
/**
|
||||
* The private _prepareComponents method is the place to register AnimationComponent instances.
|
||||
* This can be done by extending this method and calling the _addComponent method in it or by
|
||||
* defining particulary-named methods in the specialized Animation class.
|
||||
*
|
||||
* The automatic creation of components according to method names is working like this:
|
||||
* - Search for methods which match the pattern: on_startEvent[_startTarget[_off_endEvent[_endTarget]]] where:
|
||||
* - startEvent: the event name which triggers the animation to begin playing
|
||||
* - startTarget (optional): the selector (see description below) to find the target where to listen
|
||||
* for startEvent (if no selector, the window target will be used)
|
||||
* - endEvent (optional): the event name which triggers the end of the animation (if none is defined, the
|
||||
* animation will stop after a while, @see AnimationComponent.start)
|
||||
* - endTarget (optional): the selector (see description below) to find the target where to listen
|
||||
* for endEvent (if no selector, the startTarget will be used)
|
||||
* - For all of these methods, register the appropriate animation component with the method implementation used
|
||||
* as the update callback. The method receives 3 arguments: the animation state, the elapsedTime since last
|
||||
* update and the event which triggered the animation (undefined if just a new update call without trigger).
|
||||
* The animation state is undefined by default, the scroll offset for the particular "on_scroll" method and
|
||||
* and object with width and height for the particular "on_resize" method. There is the possibility to define
|
||||
* the getState callback of the animation component with this method. For the component created by the definition
|
||||
* of the "on_abc_def", define the "get_state_for_on_abc_def" (so "get_state_for_" followed by the method name).
|
||||
* This allows to improve performance even further in some cases.
|
||||
*
|
||||
* Selectors mentioned above can be:
|
||||
* - the "selector" string: this tells the system to use the Animation instance $target element as target for the
|
||||
* animation component
|
||||
* - an underscore-separated list of classnames: for example "leftPanel_playBtn", the system will search inside
|
||||
* the Animation instance $target element for a DOM which matches the selector ".leftPanel .playBtn"
|
||||
*/
|
||||
_prepareComponents: function () {
|
||||
this._animationComponents = [];
|
||||
|
||||
var self = this;
|
||||
_.each(this.__proto__, function (callback, key) {
|
||||
if (!_.isFunction(callback)) return;
|
||||
var m = key.match(/^on_([^_]+)(?:_(.+?))?(?:_off_([^_]+)(?:_(.+))?)?$/); // match the pattern described above
|
||||
if (!m) return;
|
||||
|
||||
self._addComponent(callback, m[1], _target_from_ugly_selector(m[2]), {
|
||||
getStateCallback: self["get_state_for_" + key] || undefined,
|
||||
endEvents: m[3] || undefined,
|
||||
$endTarget: _target_from_ugly_selector(m[4]),
|
||||
maxFPS: self.maxFPS,
|
||||
});
|
||||
|
||||
// Return the DOM element matching the selector in the form described above.
|
||||
function _target_from_ugly_selector(selector) {
|
||||
if (selector) {
|
||||
if (selector === "selector") {
|
||||
return self.$target;
|
||||
} else {
|
||||
return self.$(_.map(selector.split("_"), function (v) {
|
||||
return "." + v;
|
||||
}).join(" "));
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* The private _addComponent method registers a new AnimationComponent according to given parameters
|
||||
* @see AnimationComponent.init
|
||||
*/
|
||||
_addComponent: function (updateCallback, startEvents, $startTarget, options) {
|
||||
this._animationComponents.push(new AnimationComponent(this, updateCallback, startEvents, $startTarget, options));
|
||||
},
|
||||
});
|
||||
|
||||
function load_called_template () {
|
||||
var ids_or_xml_ids = _.uniq($("[data-oe-call]").map(function () {return $(this).data('oe-call');}).get());
|
||||
if (ids_or_xml_ids.length) {
|
||||
@@ -36,10 +303,12 @@ animation.registry.slider = animation.Class.extend({
|
||||
selector: ".carousel",
|
||||
start: function () {
|
||||
this.$target.carousel();
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
stop: function () {
|
||||
this.$target.carousel('pause');
|
||||
this.$target.removeData("bs.carousel");
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -116,6 +385,7 @@ animation.registry.share = animation.Class.extend({
|
||||
});
|
||||
}
|
||||
});
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -127,6 +397,7 @@ animation.registry.media_video = animation.Class.extend({
|
||||
var size = '<div class="media_iframe_video_size"> </div>';
|
||||
this.$target.html(editor+size+'<iframe src="'+_.escape(this.$target.data("src"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -138,12 +409,12 @@ animation.registry.ul = animation.Class.extend({
|
||||
$(this).closest('li').find('ul,ol').toggleClass('o_close');
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
this.$('.o_ul_toggle_next').off('click').on('click', function (event) {
|
||||
$(this).toggleClass('o_open');
|
||||
$(this).closest('li').next().toggleClass('o_close');
|
||||
event.preventDefault();
|
||||
});
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -158,6 +429,7 @@ animation.registry._fix_apple_collapse = animation.Class.extend({
|
||||
selector: ".s_faq_collapse [data-toggle='collapse']",
|
||||
start: function () {
|
||||
this.$target.off("click._fix_apple_collapse").on("click._fix_apple_collapse", function () {});
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -170,11 +442,10 @@ inside a gallery
|
||||
animation.registry.gallery = animation.Class.extend({
|
||||
selector: ".o_gallery:not(.o_slideshow)",
|
||||
start: function () {
|
||||
var self = this;
|
||||
this.$el.on("click", "img", this.click_handler);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
click_handler : function (event) {
|
||||
var self = this;
|
||||
var $cur = $(event.currentTarget);
|
||||
var edition_mode = ($cur.closest("[contenteditable='true']").size() !== 0);
|
||||
|
||||
@@ -202,7 +473,7 @@ animation.registry.gallery = animation.Class.extend({
|
||||
idx = urls.indexOf($img.attr("src"));
|
||||
|
||||
milliseconds = $cur.closest(".o_gallery").data("interval") || false;
|
||||
var params = {
|
||||
params = {
|
||||
srcs : urls,
|
||||
index: idx,
|
||||
dim : dimensions,
|
||||
@@ -232,7 +503,6 @@ animation.registry.gallery_slider = animation.Class.extend({
|
||||
selector: ".o_slideshow",
|
||||
start: function () {
|
||||
var $carousel = this.$target.is(".carousel") ? this.$target : this.$target.find(".carousel");
|
||||
var self = this;
|
||||
var $indicator = $carousel.find('.carousel-indicators');
|
||||
var $lis = $indicator.find('li:not(.fa)');
|
||||
var $prev = $indicator.find('li.fa:first');
|
||||
@@ -274,6 +544,8 @@ animation.registry.gallery_slider = animation.Class.extend({
|
||||
page = Math.floor(index / 10);
|
||||
hide();
|
||||
});
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user