Created
October 16, 2009 13:58
-
-
Save ialexi/211828 to your computer and use it in GitHub Desktop.
Provides CSS Transition-style Animation for SproutCore—even if the browser doesn't support CSS Transitions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ========================================================================== | |
| // Project: Animate | |
| // Copyright: ©2009 Alex Iskander | |
| // Now in fancy GitHub repository: http://github.com/ialexi/animate | |
| // ========================================================================== | |
| /*globals Animate */ | |
| /** @namespace | |
| A simple mixin called Animatable is provided. What does it do? | |
| It makes CSS transitions for you, and if they aren't available, | |
| implements them in JavaScript. | |
| Current flaws: | |
| - Likely slightly buggy. Haven't seen any. Please tell me. | |
| - Not very configurable. Should at LEAST allow (preset) interpolation | |
| functions. | |
| Current good things: | |
| - Seems to work! | |
| - Animates 300 SC.LabelViews acceptably with only JavaScript. Animates >500 | |
| just as well (if not better) with CSS transitions. | |
| - Automatically detects if CSS transitions are available. | |
| Example Usage: | |
| {{{ | |
| aView: SC.LabelView.design(Animate.Animatable, { | |
| transitionLayout: { | |
| left: {duration: 250}, | |
| top: {duration: 250} | |
| } | |
| }) | |
| }}} | |
| @extends SC.Object | |
| */ | |
| Animate = SC.Object.create( | |
| /** @scope Animate.prototype */ { | |
| NAMESPACE: 'Animate', | |
| VERSION: '0.1.0', | |
| Animatable: { | |
| transitionLayout: {}, | |
| concatenatedProperties: ["transitionLayout"], | |
| _animatableCSSTransitions: false, | |
| _cssTransitionFor: { | |
| "left": "left", "top": "top", "right": "right", "bottom": "bottom", | |
| "width": "width", "height": "height" | |
| }, | |
| initMixin: function() | |
| { | |
| // if transitionLayout was concatenated... | |
| if (SC.isArray(this.transitionLayout)) | |
| { | |
| var tl = {}; // prepare a new one mixed in | |
| for (var i = 0; i < this.transitionLayout.length; i++) | |
| { | |
| SC.mixin(tl, this.transitionLayout[i]); | |
| } | |
| this.transitionLayout = tl; | |
| } | |
| // live animators | |
| this._animators = {}; // keyAnimated => object describing it. | |
| this._animatableSetCSS = {}; | |
| }, | |
| /** | |
| Returns a starting hash with all terms in the target | |
| layout defined, for use in interpolation. | |
| */ | |
| _animatableStartLayoutHash: function(target) | |
| { | |
| // get our frame and parent's frame | |
| var f = this.get("frame"); | |
| var p = this.getPath("parentView.frame"); | |
| // prepare a new layout, empty. | |
| var l = {}; | |
| // loop through properties in target | |
| for (var i in target) | |
| { | |
| switch(i) | |
| { | |
| case "left": | |
| l[i] = f.x; break; | |
| case "top": | |
| l[i] = f.y; break; | |
| case "right": | |
| l[i] = p.width - f.x - f.width; break; | |
| case "bottom": | |
| l[i] = p.height - f.y - f.height; break; | |
| case "height": | |
| l[i] = f.height; break; | |
| case "width": | |
| l[i] = f.width; break; | |
| case "centerX": | |
| l[i] = f.x + (f.width / 2) - (p.width / 2); break; | |
| case "centerY": | |
| l[i] = f.y + (f.height / 2) - (p.height / 2); break; | |
| // cannot animate any others... so just set to target. | |
| default: | |
| l[i] = target[i]; | |
| } | |
| } | |
| return l; | |
| }, | |
| /** | |
| Overriden to support animation. | |
| Works by keeping a copy of the current layout, called animatableCurrentLayout. | |
| Whenever layout is changed, the new value is stored in "target", the "layout" | |
| property is reset to the animatableCurrentLayout, and animatable properties | |
| interpolate between the (original) animatableCurrentLayout and "target". | |
| Note that this interpolation updates animatableCurrentLayout; the start values | |
| for any interpolations are actually stored in the animator hash. | |
| */ | |
| updateLayout: function(context, firstTime) | |
| { | |
| var newLayout = this.get("layout"); | |
| // make sure we have a current layout, otherwise... nothing to animate! | |
| // also, if animation is disabled... | |
| if (!this._animatableCurrentLayout || firstTime) | |
| { | |
| sc_super(); | |
| this._animatableCurrentLayout = newLayout; | |
| return; | |
| } | |
| // don't animate if there is nothing to animate. | |
| if (SC.isEqual(newLayout, this._animatableCurrentLayout)) | |
| return; | |
| // reset layout | |
| this.set("layout", this._animatableCurrentLayout); | |
| // get normalized start | |
| var normalizedStart = this._animatableStartLayoutHash(newLayout); | |
| var cssTransitions = []; | |
| // stop any old animations | |
| for (var i in newLayout) | |
| { | |
| if (this._animators[i]) | |
| { | |
| this._animators[i].timer.invalidate(); | |
| this._animators[i].timer.destroy(); | |
| } | |
| // if it needs to be set right away since it is not animatable, it will | |
| // have been. But if we choose not to animate it... that's a different story. | |
| // just add it to normalized start so it will be set immediately. | |
| if (!this.transitionLayout[i] || newLayout[i] == normalizedStart[i]) | |
| { | |
| normalizedStart[i] = newLayout[i]; | |
| continue; | |
| } | |
| if (this._animatableCSSTransitions && this._cssTransitionFor[i]) | |
| { | |
| cssTransitions.push(this._cssTransitionFor[i] + " " + (this.transitionLayout[i].duration / 1000) + "s linear"); | |
| normalizedStart[i] = newLayout[i]; | |
| continue; | |
| } | |
| // well well well... looks like we need to animate. Prepare an animation structure. | |
| // (WHY ARE WE ALWAYS PREPARING?) | |
| var animator = { | |
| start: Date.now(), | |
| end: Date.now() + this.transitionLayout[i].duration, | |
| startValue: normalizedStart[i], | |
| endValue: newLayout[i], | |
| timer: undefined, | |
| property: i | |
| }; | |
| animator.timer = SC.Timer.schedule({ | |
| target: this, | |
| action: function(animator) { | |
| return function(){ this._animatableAnimationStep(animator); }; | |
| }(animator), | |
| interval: 10, | |
| repeats: YES, | |
| until: animator.end | |
| }); | |
| } | |
| // and update layout to the normalized start. | |
| var css = cssTransitions.join(","); | |
| this._animatableSetCSS = css; | |
| this.set("layout", normalizedStart); | |
| this._animatableLayoutUpdate(); | |
| // all our timers are scheduled, we should be good to go. YAY. | |
| return this; | |
| }, | |
| /** | |
| Manages a single step in a single animation. | |
| */ | |
| _animatableAnimationStep: function(a) | |
| { | |
| // prepare timing stuff | |
| var s = a.start, e = a.end; | |
| var d = e - s; | |
| // get current | |
| var c = Date.now() - s; | |
| var percent = c / d; | |
| // Now, interpolate between start layout and end layout | |
| var value = a.startValue + ((a.endValue - a.startValue) * percent); | |
| this.layout[a.property] = value; | |
| // trigger update | |
| this._animatableLayoutUpdate(); | |
| // update current layout | |
| this._animatableCurrentLayout = this.layout; | |
| }, | |
| /** | |
| Triggers a layout re-rendering. | |
| */ | |
| _animatableLayoutUpdate: function() | |
| { | |
| // set layout | |
| this.notifyPropertyChange("layoutStyle"); | |
| // notify of update | |
| var layer = this.get("layer"); | |
| if (layer) { | |
| var context = this.renderContext(layer); | |
| this.renderLayout(context); | |
| context.addStyle("-webkit-transition", this._animatableSetCSS); | |
| context.update(); | |
| } | |
| } | |
| } | |
| }); | |
| /* | |
| Test for CSS transition capability... | |
| */ | |
| (function(){ | |
| var test = function(){ | |
| // a test element | |
| var el = document.createElement("div"); | |
| // the css and javascript to test | |
| var css_browsers = ["-webkit"]; | |
| var test_browsers = ["moz", "Moz", "o", "ms", "webkit"]; | |
| // prepare css | |
| var css = ""; | |
| for (var i = 0; i < css_browsers.length; i++) | |
| css += css_browsers[i] + "-transition:all 1s linear;" | |
| // set css text | |
| el.style.cssText = css; | |
| // test | |
| for (var i = 0; i < test_browsers.length; i++) | |
| { | |
| if (el.style[test_browsers[i] + "TransitionProperty"] !== undefined) | |
| return true; | |
| } | |
| return false; | |
| } | |
| // test | |
| var testResult = test(); | |
| // console.error("Supports CSS transitions: " + testResult); | |
| // and apply what we found | |
| if (testResult) | |
| Animate.Animatable._animatableCSSTransitions = true; | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment