1 /** 
  2  * @fileoverview
  3  * @class StyleTransition
  4  * @namespace APE.anim
  5  *<p>
  6  * For morphing an objects styles across a css-time-continuum.
  7  *</p>
  8  * @author Garrett Smith
  9  *
 10  * @requires APE.anim.Animation
 11  * @requires APE.color
 12  * @requires APE.dom.style-f 
 13  */
 14 
 15 /** 
 16  * @param {String} id of the element
 17  * @param {Object} styleObject in the form of {color: "#030", fontSize : "12px"}
 18  * @param {ufloat} [duration] optional number of seconds.
 19  * @param {Function} [transition] optional funtion that takes a float [0-1] and returns a float [0-1]
 20  * @extends APE.anim.Animation 
 21  * @constructor
 22  */
 23 APE.anim.StyleTransition = function(id, styleObject, duration, transition) {
 24 	APE.anim.Animation.call(this, duration); // invoke super constructor.
 25     if(id.id) id = id.id;
 26 	this.id = id;
 27 	this.adapters = [];
 28 	this.style = document.getElementById(this.id).style;
 29     if(transition)
 30     	this.transition = transition;
 31 	this.init(styleObject);
 32 };
 33 
 34 APE.extend(APE.anim.StyleTransition, APE.anim.Animation, {
 35 
 36 	inited : false,
 37     /**
 38      * @method run 
 39      * @memberOf APE.anim.StyleTransition
 40      * @description overrides (implements) <code>run()</code> in Animation. 
 41      * Runs the animation, getting the correct value  
 42      * from each ITransitionAdapter.
 43      * This run() method gets called by Animation.
 44      */
 45 	run : function run(rationalValue) {
 46     	var i = 0, 
 47             adapters = this.adapters, 
 48             len = adapters.length, 
 49             adapter;
 50     	while(i < len) {
 51         	adapter = adapters[i++];
 52             this.style[adapter.prop] = adapter.blendTo(rationalValue);
 53         }
 54     },
 55 
 56     /**@private */
 57 	init : function(styleObject) {
 58     	if(this.inited) return;
 59 
 60     	var el = document.getElementById(this.id),
 61             adapters = [],
 62             adapter,
 63             APE = window.APE,
 64             prop,
 65             units,
 66             fromValue, toValue,
 67             dom = APE.dom,
 68         	TransitionAdapterFactory = APE.anim.TransitionAdapterFactory,
 69             ThresholdTransitionAdapter = TransitionAdapterFactory.ThresholdTransitionAdapter,
 70             ImmediateThresholdTransitionAdapter = TransitionAdapterFactory.ImmediateThresholdTransitionAdapter
 71 
 72         // Loop through style object to find values.
 73     	for(prop in styleObject) {
 74         	toValue = styleObject[prop];
 75         	if(!toValue) continue; // CSSStyleRule.
 76 
 77         	if(prop == "opacity" && !("opacity"in this.style)
 78                                 && ("filter"in this.style)) {
 79             	prop = "alpha";
 80                 this.style.zoom = "1";
 81             	fromValue = dom.getFilterOpacity(el);
 82             }
 83         	else { 
 84                 if(prop == 'clip' && (!fromValue || fromValue.indexOf("auto") != -1)) {
 85                     fromValue = "rect(0px " + el.offsetWidth + "px " + el.offsetHeight + "px 0px)";
 86                 }
 87             	else {
 88                     units = dom.getStyleUnit(toValue);
 89                 	fromValue = dom.findInheritedStyle(el, prop, units);
 90                 }
 91             }
 92              // Get a ITransitionAdapter from the factory.
 93         	adapter = TransitionAdapterFactory.fromValues(prop, fromValue, toValue);
 94         	adapters.push( adapter );
 95         }
 96 
 97         // IE will not properly render visibility when 
 98         // 1) visibility is initially hidden
 99         // 2) alpha filter is applied
100         // 3) and visibility is then set to visible.
101         // after that, the element doesn't appear visible. 
102         // Workaround: first transition is visibility. 
103         adapters.sort(function(a,b){ 
104             return (a instanceof ImmediateThresholdTransitionAdapter ? -1 : 1);
105         });
106 
107         this.adapters = adapters;
108     },
109         
110     /** 
111      * @memberOf APE.anim.StyleTransition
112      * Helpful debugging info. */
113     toString : function() {
114         return "StyleTransitionAdapter : id=#"+this.id+"\n" + 
115             APE.anim.Animation.prototype.toString.call(this) +
116             "\nAdapters:\n  " +
117             this.adapters.join("\n  ");
118     }
119 
120 });
121 
122 /**
123  * Factory for APE.anim.TransitionAdapterFactory Interface.
124  * @return {ITransitionAdapter} ITransitionAdapter for a specific type of style setting during run.
125  * The {ITransitionAdapter} implements blendTo(rationaValue).
126  * @class
127  * @private Used internally.
128  */
129 APE.anim.TransitionAdapterFactory = {
130     // "1px", "1.1px", "-.1px" => ["-.1px", "-.1", "px"]
131 	lengthExp : /(^-?\d+|(?:-?\d*\.\d+))(px|em|ex|pt|pc|in|cm|mm|%)/i,
132 	colorExp : /color/i, 
133 	positiveLengthExp : /(?:width|height|padding|fontSize)$/ig, 
134 	filterExp : /alpha/,
135 	opacityExp : /^opacity/,
136 	intExp : /^\d+$/,
137     noVisibilityExp : /^(?:hidden|collapse)/,
138 
139 	fromValues : function(prop, fromValue, toValue) {
140 
141     	if(this.positiveLengthExp.test( prop )) {
142         	return new this.PositiveLengthTransitionAdapter(prop, fromValue, toValue);
143         }
144     	if(this.colorExp.test( prop )) 
145         	return new this.ColorTransitionAdapter(prop, fromValue, toValue);
146     	if(prop == 'clip')
147         	return new this.ClipTransitionAdapter(prop, fromValue, toValue);
148     	if(this.lengthExp.test( fromValue )) {
149         	return new this.LengthTransitionAdapter(prop, fromValue, toValue);
150         }
151     	if(this.filterExp.test( prop )) 
152         	return new this.FilterTransitionAdapter(prop, fromValue, toValue);
153     	if(this.opacityExp.test( prop )) 
154         	return new this.OpacityTransitionAdapter(prop, fromValue, toValue);
155     	if(prop == "fontWeight" && this.intExp.test( fromValue ) && this.intExp.test( toValue )) {
156         	return new this.FontWeightTransitionAdapter(prop, fromValue, toValue);
157         }
158         if(prop == "visibility" && this.noVisibilityExp.test(fromValue) 
159             || prop == "display" && fromValue == "none")
160             return new this.ImmediateThresholdTransitionAdapter(prop, fromValue, toValue);
161         // Return an object that sets toValue on completion.
162     	return new this.ThresholdTransitionAdapter(prop, fromValue, toValue);
163     }
164 };
165 
166 (function() {
167 
168 	var APE = window.APE,
169         ColorRGB = APE.color && APE.color.ColorRGB,
170 
171     Adapters = {
172         /** @augments APE.anim.TransitionAdapterFactory */
173     	PositiveLengthTransitionAdapter : PositiveLengthTransitionAdapter,
174     	ColorTransitionAdapter : ColorTransitionAdapter,
175     	LengthTransitionAdapter : LengthTransitionAdapter,
176     	FilterTransitionAdapter : FilterTransitionAdapter,
177     	OpacityTransitionAdapter : OpacityTransitionAdapter,
178     	FontWeightTransitionAdapter : FontWeightTransitionAdapter,
179     	ThresholdTransitionAdapter : ThresholdTransitionAdapter,
180         ImmediateThresholdTransitionAdapter : ImmediateThresholdTransitionAdapter,
181     	ClipTransitionAdapter : ClipTransitionAdapter
182     };
183 
184 	APE.mixin(APE.anim.TransitionAdapterFactory, Adapters);
185 
186     function TransitionAdapter(prop, fromValue, toValue, units) {
187         this.prop = prop;
188         this.fromValue = fromValue;
189         this.toValue = toValue;
190         if(units)
191             this.units = units;
192     }
193     TransitionAdapter.prototype.toString = function() {
194         var units = (this.units||'');
195         return APE.getFunctionName(this.constructor) + ": " + this.prop 
196             + ", " + this.fromValue.toString() + units
197             + " \u2014 " + this.toValue.toString() + units;
198     };
199 
200 	function ColorTransitionAdapter(prop, fromValue, toValue) {
201         if(!ColorRGB) ColorRGB = APE.color.ColorRGB;
202         var f = ColorRGB.fromString(fromValue),
203             t = toValue = ColorRGB.fromString(toValue);
204 
205     	TransitionAdapter.call(this, prop, f, t);
206 
207         // This is where we mix fromValue and toValue,
208         // to avoid the creation of new ColorRGB for each frame.
209     	this.currentValue = new ColorRGB();
210     }
211     
212 	APE.extend(ColorTransitionAdapter, TransitionAdapter, {
213 
214         /**
215          * Adapter/Strategy interface.
216          * @return {String} rgb string of the blended values.
217          */
218     	blendTo : function(rationalValue) {
219         	var c = ColorRGB.blend(this.fromValue, this.toValue, rationalValue, this.currentValue);
220         	return c.toString();
221         }
222     });
223 
224 	function LengthTransitionAdapter(prop, fromValue, toValue) {
225     	var lengthExp = APE.anim.TransitionAdapterFactory.lengthExp,
226             fromValues = lengthExp.exec(fromValue),
227             toValues = lengthExp.exec(toValue);
228 
229         TransitionAdapter.call(this, prop, parseFloat(fromValues[0]), 
230                             parseFloat(toValues[0]), fromValues[2]);
231     }
232 
233     APE.extend(LengthTransitionAdapter, TransitionAdapter);
234 
235 	LengthTransitionAdapter.prototype.blendTo = function(rationalValue) {
236         var inverse = 1-rationalValue;
237         return ((this.fromValue * inverse) + (this.toValue * rationalValue)) + this.units;
238     };
239 
240 	function PositiveLengthTransitionAdapter() {
241     	LengthTransitionAdapter.apply(this, arguments);
242     }
243 
244     /**@ignore
245      * extends LengthTransitionAdapter
246      */
247 	APE.extend(PositiveLengthTransitionAdapter, LengthTransitionAdapter);
248     
249 	PositiveLengthTransitionAdapter.prototype.blendTo = function(rationalValue) {
250     	var inverse = 1-rationalValue,
251             v = Math.max((this.fromValue * inverse) + (this.toValue * rationalValue), 0) + this.units;
252     	return v;
253     };
254 
255 	var	splitClipExp = /,?\s/,
256     	clipExp = /rect\(([^\)]+)\)/,
257     	zeroToPxExp = /0(\s|\))/g;
258 
259 	function ClipTransitionAdapter(prop, fromValue, toValue) {
260     	this.prop = "clip";
261 
262     	var fromString = clipExp.exec(fromValue.replace(zeroToPxExp, "0px$1"))[1],
263         	toString = clipExp.exec(toValue.replace(zeroToPxExp, "0px$1"))[1];
264 
265     	this.fromValues = fromString.split(splitClipExp);
266     	this.toValues = toString.split(splitClipExp);
267     	this.clips = [];
268     	this.values = [];
269     	this.init();
270     }
271 
272 	ClipTransitionAdapter.prototype = {
273     	init : function() {
274         	for(var i = 0, f, t; i < 4; i++) {
275                 f = this.fromValues[i],
276                 t =  this.toValues[i];
277                 if(f == "0") f = "0px";
278                 if(t == "0") t = "0px";
279             	this.clips[i] = new LengthTransitionAdapter(this.prop, f, t);
280             }
281         },
282 
283     	blendTo : function(rationalValue) {
284         	for(var i = 0; i < 4; i++)    
285             	this.values[i] = this.clips[i].blendTo(rationalValue);
286         	return "rect("+this.values.join(" ")+")";
287         },
288 
289         toString : function() {
290             return "ClipTransitionAdapter:    \n" + this.clips.join("    \n");
291         }
292     };
293 
294     /**@ignore*/
295 	function OpacityTransitionAdapter(prop, fromValue, toValue) {
296     	TransitionAdapter.call(this, prop, parseFloat(fromValue), parseFloat(toValue));
297     }
298 
299     APE.extend(OpacityTransitionAdapter, TransitionAdapter);     
300     OpacityTransitionAdapter.prototype.blendTo = function(rationalValue) {
301         var inverse = 1-rationalValue,
302             v = Math.max((this.fromValue * inverse) + (this.toValue * rationalValue), 0);
303         return v;
304     };
305 
306     /**@ignore*/
307 	function FilterTransitionAdapter(prop, fromValue, toValue) {
308         TransitionAdapter.call(this, "filter", fromValue, toValue);
309     }
310 
311     /**@ignore
312      * constructor FilterTransitionAdapter
313      * performs adapter service using IE filters crap. 
314      */
315 	APE.extend(FilterTransitionAdapter, TransitionAdapter);
316     FilterTransitionAdapter.prototype.blendTo = function(rationalValue) {
317         var inverse = 1-rationalValue,
318             v = Math.abs((this.fromValue * inverse) + (this.toValue * rationalValue), 0);
319         return"alpha(opacity=" + Math.abs(v * 100) + ")";
320     };
321 
322     /** Useful for z-index, font-weight */
323 	function FontWeightTransitionAdapter(prop, fromValue, toValue) {
324         TransitionAdapter.call(this, prop, parseInt(fromValue), parseInt(toValue));
325     }
326 
327     APE.extend(FontWeightTransitionAdapter, TransitionAdapter);
328 	FontWeightTransitionAdapter.prototype.blendTo = function(rationalValue) {
329         var inverse = 1-rationalValue,
330             v = (((this.fromValue * inverse) + (this.toValue * rationalValue))/100 << 0) * 100;
331         if(v < 100) return 100;
332         if(v > 900) return 900;
333         return v;
334     };
335 
336     /**@ignore
337      * constructor ThresholdTransitionAdapter
338      * This is sort of a Null Object. It does implement blendTo,
339      * setting the toValue at the very end.
340      */
341 	function ThresholdTransitionAdapter(prop, fromValue, toValue) {
342     	TransitionAdapter.call(this, prop, fromValue, toValue);
343     }
344     
345     APE.extend(ThresholdTransitionAdapter, TransitionAdapter);
346 	ThresholdTransitionAdapter.prototype.blendTo = function(rationalValue) {
347         if(rationalValue == 1) 
348             return this.toValue;
349         return this.fromValue;
350     };
351 
352     function ImmediateThresholdTransitionAdapter(prop, fromValue, toValue) {
353     	TransitionAdapter.call(this, prop, fromValue, toValue);
354     }
355 
356     APE.extend(ImmediateThresholdTransitionAdapter, TransitionAdapter);
357 	ImmediateThresholdTransitionAdapter.prototype.blendTo = function(rationalValue) {
358         if(rationalValue == 0) {
359             return this.fromValue;
360         }
361         return this.toValue;
362     };
363 })();