]> dev.renevier.net Git - syp.git/blob - openlayers/examples/animator.js
fixes notices
[syp.git] / openlayers / examples / animator.js
1 /*  
2         Animator.js 1.1.9
3         
4         This library is released under the BSD license:
5
6         Copyright (c) 2006, Bernard Sumption. All rights reserved.
7         
8         Redistribution and use in source and binary forms, with or without
9         modification, are permitted provided that the following conditions are met:
10         
11         Redistributions of source code must retain the above copyright notice, this
12         list of conditions and the following disclaimer. Redistributions in binary
13         form must reproduce the above copyright notice, this list of conditions and
14         the following disclaimer in the documentation and/or other materials
15         provided with the distribution. Neither the name BernieCode nor
16         the names of its contributors may be used to endorse or promote products
17         derived from this software without specific prior written permission. 
18         
19         THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20         AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21         IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22         ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
23         ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24         DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25         SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26         CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27         LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28         OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
29         DAMAGE.
30
31 */
32
33
34 // Applies a sequence of numbers between 0 and 1 to a number of subjects
35 // construct - see setOptions for parameters
36 function Animator(options) {
37         this.setOptions(options);
38         var _this = this;
39         this.timerDelegate = function(){_this.onTimerEvent()};
40         this.subjects = [];
41         this.target = 0;
42         this.state = 0;
43         this.lastTime = null;
44 };
45 Animator.prototype = {
46         // apply defaults
47         setOptions: function(options) {
48                 this.options = Animator.applyDefaults({
49                         interval: 20,  // time between animation frames
50                         duration: 400, // length of animation
51                         onComplete: function(){},
52                         onStep: function(){},
53                         transition: Animator.tx.easeInOut
54                 }, options);
55         },
56         // animate from the current state to provided value
57         seekTo: function(to) {
58                 this.seekFromTo(this.state, to);
59         },
60         // animate from the current state to provided value
61         seekFromTo: function(from, to) {
62                 this.target = Math.max(0, Math.min(1, to));
63                 this.state = Math.max(0, Math.min(1, from));
64                 this.lastTime = new Date().getTime();
65                 if (!this.intervalId) {
66                         this.intervalId = window.setInterval(this.timerDelegate, this.options.interval);
67                 }
68         },
69         // animate from the current state to provided value
70         jumpTo: function(to) {
71                 this.target = this.state = Math.max(0, Math.min(1, to));
72                 this.propagate();
73         },
74         // seek to the opposite of the current target
75         toggle: function() {
76                 this.seekTo(1 - this.target);
77         },
78         // add a function or an object with a method setState(state) that will be called with a number
79         // between 0 and 1 on each frame of the animation
80         addSubject: function(subject) {
81                 this.subjects[this.subjects.length] = subject;
82                 return this;
83         },
84         // remove all subjects
85         clearSubjects: function() {
86                 this.subjects = [];
87         },
88         // forward the current state to the animation subjects
89         propagate: function() {
90                 var value = this.options.transition(this.state);
91                 for (var i=0; i<this.subjects.length; i++) {
92                         if (this.subjects[i].setState) {
93                                 this.subjects[i].setState(value);
94                         } else {
95                                 this.subjects[i](value);
96                         }
97                 }
98         },
99         // called once per frame to update the current state
100         onTimerEvent: function() {
101                 var now = new Date().getTime();
102                 var timePassed = now - this.lastTime;
103                 this.lastTime = now;
104                 var movement = (timePassed / this.options.duration) * (this.state < this.target ? 1 : -1);
105                 if (Math.abs(movement) >= Math.abs(this.state - this.target)) {
106                         this.state = this.target;
107                 } else {
108                         this.state += movement;
109                 }
110                 
111                 try {
112                         this.propagate();
113                 } finally {
114                         this.options.onStep.call(this);
115                         if (this.target == this.state) {
116                                 window.clearInterval(this.intervalId);
117                                 this.intervalId = null;
118                                 this.options.onComplete.call(this);
119                         }
120                 }
121         },
122         // shortcuts
123         play: function() {this.seekFromTo(0, 1)},
124         reverse: function() {this.seekFromTo(1, 0)},
125         // return a string describing this Animator, for debugging
126         inspect: function() {
127                 var str = "#<Animator:\n";
128                 for (var i=0; i<this.subjects.length; i++) {
129                         str += this.subjects[i].inspect();
130                 }
131                 str += ">";
132                 return str;
133         }
134 }
135 // merge the properties of two objects
136 Animator.applyDefaults = function(defaults, prefs) {
137         prefs = prefs || {};
138         var prop, result = {};
139         for (prop in defaults) result[prop] = prefs[prop] !== undefined ? prefs[prop] : defaults[prop];
140         return result;
141 }
142 // make an array from any object
143 Animator.makeArray = function(o) {
144         if (o == null) return [];
145         if (!o.length) return [o];
146         var result = [];
147         for (var i=0; i<o.length; i++) result[i] = o[i];
148         return result;
149 }
150 // convert a dash-delimited-property to a camelCaseProperty (c/o Prototype, thanks Sam!)
151 Animator.camelize = function(string) {
152         var oStringList = string.split('-');
153         if (oStringList.length == 1) return oStringList[0];
154         
155         var camelizedString = string.indexOf('-') == 0
156                 ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
157                 : oStringList[0];
158         
159         for (var i = 1, len = oStringList.length; i < len; i++) {
160                 var s = oStringList[i];
161                 camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
162         }
163         return camelizedString;
164 }
165 // syntactic sugar for creating CSSStyleSubjects
166 Animator.apply = function(el, style, options) {
167         if (style instanceof Array) {
168                 return new Animator(options).addSubject(new CSSStyleSubject(el, style[0], style[1]));
169         }
170         return new Animator(options).addSubject(new CSSStyleSubject(el, style));
171 }
172 // make a transition function that gradually accelerates. pass a=1 for smooth
173 // gravitational acceleration, higher values for an exaggerated effect
174 Animator.makeEaseIn = function(a) {
175         return function(state) {
176                 return Math.pow(state, a*2); 
177         }
178 }
179 // as makeEaseIn but for deceleration
180 Animator.makeEaseOut = function(a) {
181         return function(state) {
182                 return 1 - Math.pow(1 - state, a*2); 
183         }
184 }
185 // make a transition function that, like an object with momentum being attracted to a point,
186 // goes past the target then returns
187 Animator.makeElastic = function(bounces) {
188         return function(state) {
189                 state = Animator.tx.easeInOut(state);
190                 return ((1-Math.cos(state * Math.PI * bounces)) * (1 - state)) + state; 
191         }
192 }
193 // make an Attack Decay Sustain Release envelope that starts and finishes on the same level
194 // 
195 Animator.makeADSR = function(attackEnd, decayEnd, sustainEnd, sustainLevel) {
196         if (sustainLevel == null) sustainLevel = 0.5;
197         return function(state) {
198                 if (state < attackEnd) {
199                         return state / attackEnd;
200                 }
201                 if (state < decayEnd) {
202                         return 1 - ((state - attackEnd) / (decayEnd - attackEnd) * (1 - sustainLevel));
203                 }
204                 if (state < sustainEnd) {
205                         return sustainLevel;
206                 }
207                 return sustainLevel * (1 - ((state - sustainEnd) / (1 - sustainEnd)));
208         }
209 }
210 // make a transition function that, like a ball falling to floor, reaches the target and/
211 // bounces back again
212 Animator.makeBounce = function(bounces) {
213         var fn = Animator.makeElastic(bounces);
214         return function(state) {
215                 state = fn(state); 
216                 return state <= 1 ? state : 2-state;
217         }
218 }
219  
220 // pre-made transition functions to use with the 'transition' option
221 Animator.tx = {
222         easeInOut: function(pos){
223                 return ((-Math.cos(pos*Math.PI)/2) + 0.5);
224         },
225         linear: function(x) {
226                 return x;
227         },
228         easeIn: Animator.makeEaseIn(1.5),
229         easeOut: Animator.makeEaseOut(1.5),
230         strongEaseIn: Animator.makeEaseIn(2.5),
231         strongEaseOut: Animator.makeEaseOut(2.5),
232         elastic: Animator.makeElastic(1),
233         veryElastic: Animator.makeElastic(3),
234         bouncy: Animator.makeBounce(1),
235         veryBouncy: Animator.makeBounce(3)
236 }
237
238 // animates a pixel-based style property between two integer values
239 function NumericalStyleSubject(els, property, from, to, units) {
240         this.els = Animator.makeArray(els);
241         if (property == 'opacity' && window.ActiveXObject) {
242                 this.property = 'filter';
243         } else {
244                 this.property = Animator.camelize(property);
245         }
246         this.from = parseFloat(from);
247         this.to = parseFloat(to);
248         this.units = units != null ? units : 'px';
249 }
250 NumericalStyleSubject.prototype = {
251         setState: function(state) {
252                 var style = this.getStyle(state);
253                 var visibility = (this.property == 'opacity' && state == 0) ? 'hidden' : '';
254                 var j=0;
255                 for (var i=0; i<this.els.length; i++) {
256                         try {
257                                 this.els[i].style[this.property] = style;
258                         } catch (e) {
259                                 // ignore fontWeight - intermediate numerical values cause exeptions in firefox
260                                 if (this.property != 'fontWeight') throw e;
261                         }
262                         if (j++ > 20) return;
263                 }
264         },
265         getStyle: function(state) {
266                 state = this.from + ((this.to - this.from) * state);
267                 if (this.property == 'filter') return "alpha(opacity=" + Math.round(state*100) + ")";
268                 if (this.property == 'opacity') return state;
269                 return Math.round(state) + this.units;
270         },
271         inspect: function() {
272                 return "\t" + this.property + "(" + this.from + this.units + " to " + this.to + this.units + ")\n";
273         }
274 }
275
276 // animates a colour based style property between two hex values
277 function ColorStyleSubject(els, property, from, to) {
278         this.els = Animator.makeArray(els);
279         this.property = Animator.camelize(property);
280         this.to = this.expandColor(to);
281         this.from = this.expandColor(from);
282         this.origFrom = from;
283         this.origTo = to;
284 }
285
286 ColorStyleSubject.prototype = {
287         // parse "#FFFF00" to [256, 256, 0]
288         expandColor: function(color) {
289                 var hexColor, red, green, blue;
290                 hexColor = ColorStyleSubject.parseColor(color);
291                 if (hexColor) {
292                         red = parseInt(hexColor.slice(1, 3), 16);
293                         green = parseInt(hexColor.slice(3, 5), 16);
294                         blue = parseInt(hexColor.slice(5, 7), 16);
295                         return [red,green,blue]
296                 }
297                 if (window.DEBUG) {
298                         alert("Invalid colour: '" + color + "'");
299                 }
300         },
301         getValueForState: function(color, state) {
302                 return Math.round(this.from[color] + ((this.to[color] - this.from[color]) * state));
303         },
304         setState: function(state) {
305                 var color = '#'
306                                 + ColorStyleSubject.toColorPart(this.getValueForState(0, state))
307                                 + ColorStyleSubject.toColorPart(this.getValueForState(1, state))
308                                 + ColorStyleSubject.toColorPart(this.getValueForState(2, state));
309                 for (var i=0; i<this.els.length; i++) {
310                         this.els[i].style[this.property] = color;
311                 }
312         },
313         inspect: function() {
314                 return "\t" + this.property + "(" + this.origFrom + " to " + this.origTo + ")\n";
315         }
316 }
317
318 // return a properly formatted 6-digit hex colour spec, or false
319 ColorStyleSubject.parseColor = function(string) {
320         var color = '#', match;
321         if(match = ColorStyleSubject.parseColor.rgbRe.exec(string)) {
322                 var part;
323                 for (var i=1; i<=3; i++) {
324                         part = Math.max(0, Math.min(255, parseInt(match[i])));
325                         color += ColorStyleSubject.toColorPart(part);
326                 }
327                 return color;
328         }
329         if (match = ColorStyleSubject.parseColor.hexRe.exec(string)) {
330                 if(match[1].length == 3) {
331                         for (var i=0; i<3; i++) {
332                                 color += match[1].charAt(i) + match[1].charAt(i);
333                         }
334                         return color;
335                 }
336                 return '#' + match[1];
337         }
338         return false;
339 }
340 // convert a number to a 2 digit hex string
341 ColorStyleSubject.toColorPart = function(number) {
342         if (number > 255) number = 255;
343         var digits = number.toString(16);
344         if (number < 16) return '0' + digits;
345         return digits;
346 }
347 ColorStyleSubject.parseColor.rgbRe = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i;
348 ColorStyleSubject.parseColor.hexRe = /^\#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
349
350 // Animates discrete styles, i.e. ones that do not scale but have discrete values
351 // that can't be interpolated
352 function DiscreteStyleSubject(els, property, from, to, threshold) {
353         this.els = Animator.makeArray(els);
354         this.property = Animator.camelize(property);
355         this.from = from;
356         this.to = to;
357         this.threshold = threshold || 0.5;
358 }
359
360 DiscreteStyleSubject.prototype = {
361         setState: function(state) {
362                 var j=0;
363                 for (var i=0; i<this.els.length; i++) {
364                         this.els[i].style[this.property] = state <= this.threshold ? this.from : this.to; 
365                 }
366         },
367         inspect: function() {
368                 return "\t" + this.property + "(" + this.from + " to " + this.to + " @ " + this.threshold + ")\n";
369         }
370 }
371
372 // animates between two styles defined using CSS.
373 // if style1 and style2 are present, animate between them, if only style1
374 // is present, animate between the element's current style and style1
375 function CSSStyleSubject(els, style1, style2) {
376         els = Animator.makeArray(els);
377         this.subjects = [];
378         if (els.length == 0) return;
379         var prop, toStyle, fromStyle;
380         if (style2) {
381                 fromStyle = this.parseStyle(style1, els[0]);
382                 toStyle = this.parseStyle(style2, els[0]);
383         } else {
384                 toStyle = this.parseStyle(style1, els[0]);
385                 fromStyle = {};
386                 for (prop in toStyle) {
387                         fromStyle[prop] = CSSStyleSubject.getStyle(els[0], prop);
388                 }
389         }
390         // remove unchanging properties
391         var prop;
392         for (prop in fromStyle) {
393                 if (fromStyle[prop] == toStyle[prop]) {
394                         delete fromStyle[prop];
395                         delete toStyle[prop];
396                 }
397         }
398         // discover the type (numerical or colour) of each style
399         var prop, units, match, type, from, to;
400         for (prop in fromStyle) {
401                 var fromProp = String(fromStyle[prop]);
402                 var toProp = String(toStyle[prop]);
403                 if (toStyle[prop] == null) {
404                         if (window.DEBUG) alert("No to style provided for '" + prop + '"');
405                         continue;
406                 }
407                 
408                 if (from = ColorStyleSubject.parseColor(fromProp)) {
409                         to = ColorStyleSubject.parseColor(toProp);
410                         type = ColorStyleSubject;
411                 } else if (fromProp.match(CSSStyleSubject.numericalRe)
412                                 && toProp.match(CSSStyleSubject.numericalRe)) {
413                         from = parseFloat(fromProp);
414                         to = parseFloat(toProp);
415                         type = NumericalStyleSubject;
416                         match = CSSStyleSubject.numericalRe.exec(fromProp);
417                         var reResult = CSSStyleSubject.numericalRe.exec(toProp);
418                         if (match[1] != null) {
419                                 units = match[1];
420                         } else if (reResult[1] != null) {
421                                 units = reResult[1];
422                         } else {
423                                 units = reResult;
424                         }
425                 } else if (fromProp.match(CSSStyleSubject.discreteRe)
426                                 && toProp.match(CSSStyleSubject.discreteRe)) {
427                         from = fromProp;
428                         to = toProp;
429                         type = DiscreteStyleSubject;
430                         units = 0;   // hack - how to get an animator option down to here
431                 } else {
432                         if (window.DEBUG) {
433                                 alert("Unrecognised format for value of "
434                                         + prop + ": '" + fromStyle[prop] + "'");
435                         }
436                         continue;
437                 }
438                 this.subjects[this.subjects.length] = new type(els, prop, from, to, units);
439         }
440 }
441
442 CSSStyleSubject.prototype = {
443         // parses "width: 400px; color: #FFBB2E" to {width: "400px", color: "#FFBB2E"}
444         parseStyle: function(style, el) {
445                 var rtn = {};
446                 // if style is a rule set
447                 if (style.indexOf(":") != -1) {
448                         var styles = style.split(";");
449                         for (var i=0; i<styles.length; i++) {
450                                 var parts = CSSStyleSubject.ruleRe.exec(styles[i]);
451                                 if (parts) {
452                                         rtn[parts[1]] = parts[2];
453                                 }
454                         }
455                 }
456                 // else assume style is a class name
457                 else {
458                         var prop, value, oldClass;
459                         oldClass = el.className;
460                         el.className = style;
461                         for (var i=0; i<CSSStyleSubject.cssProperties.length; i++) {
462                                 prop = CSSStyleSubject.cssProperties[i];
463                                 value = CSSStyleSubject.getStyle(el, prop);
464                                 if (value != null) {
465                                         rtn[prop] = value;
466                                 }
467                         }
468                         el.className = oldClass;
469                 }
470                 return rtn;
471                 
472         },
473         setState: function(state) {
474                 for (var i=0; i<this.subjects.length; i++) {
475                         this.subjects[i].setState(state);
476                 }
477         },
478         inspect: function() {
479                 var str = "";
480                 for (var i=0; i<this.subjects.length; i++) {
481                         str += this.subjects[i].inspect();
482                 }
483                 return str;
484         }
485 }
486 // get the current value of a css property, 
487 CSSStyleSubject.getStyle = function(el, property){
488         var style;
489         if(document.defaultView && document.defaultView.getComputedStyle){
490                 style = document.defaultView.getComputedStyle(el, "").getPropertyValue(property);
491                 if (style) {
492                         return style;
493                 }
494         }
495         property = Animator.camelize(property);
496         if(el.currentStyle){
497                 style = el.currentStyle[property];
498         }
499         return style || el.style[property]
500 }
501
502
503 CSSStyleSubject.ruleRe = /^\s*([a-zA-Z\-]+)\s*:\s*(\S(.+\S)?)\s*$/;
504 CSSStyleSubject.numericalRe = /^-?\d+(?:\.\d+)?(%|[a-zA-Z]{2})?$/;
505 CSSStyleSubject.discreteRe = /^\w+$/;
506
507 // required because the style object of elements isn't enumerable in Safari
508 /*
509 CSSStyleSubject.cssProperties = ['background-color','border','border-color','border-spacing',
510 'border-style','border-top','border-right','border-bottom','border-left','border-top-color',
511 'border-right-color','border-bottom-color','border-left-color','border-top-width','border-right-width',
512 'border-bottom-width','border-left-width','border-width','bottom','color','font-size','font-size-adjust',
513 'font-stretch','font-style','height','left','letter-spacing','line-height','margin','margin-top',
514 'margin-right','margin-bottom','margin-left','marker-offset','max-height','max-width','min-height',
515 'min-width','orphans','outline','outline-color','outline-style','outline-width','overflow','padding',
516 'padding-top','padding-right','padding-bottom','padding-left','quotes','right','size','text-indent',
517 'top','width','word-spacing','z-index','opacity','outline-offset'];*/
518
519
520 CSSStyleSubject.cssProperties = ['azimuth','background','background-attachment','background-color','background-image','background-position','background-repeat','border-collapse','border-color','border-spacing','border-style','border-top','border-top-color','border-right-color','border-bottom-color','border-left-color','border-top-style','border-right-style','border-bottom-style','border-left-style','border-top-width','border-right-width','border-bottom-width','border-left-width','border-width','bottom','clear','clip','color','content','cursor','direction','display','elevation','empty-cells','css-float','font','font-family','font-size','font-size-adjust','font-stretch','font-style','font-variant','font-weight','height','left','letter-spacing','line-height','list-style','list-style-image','list-style-position','list-style-type','margin','margin-top','margin-right','margin-bottom','margin-left','max-height','max-width','min-height','min-width','orphans','outline','outline-color','outline-style','outline-width','overflow','padding','padding-top','padding-right','padding-bottom','padding-left','pause','position','right','size','table-layout','text-align','text-decoration','text-indent','text-shadow','text-transform','top','vertical-align','visibility','white-space','width','word-spacing','z-index','opacity','outline-offset','overflow-x','overflow-y'];
521
522
523 // chains several Animator objects together
524 function AnimatorChain(animators, options) {
525         this.animators = animators;
526         this.setOptions(options);
527         for (var i=0; i<this.animators.length; i++) {
528                 this.listenTo(this.animators[i]);
529         }
530         this.forwards = false;
531         this.current = 0;
532 }
533
534 AnimatorChain.prototype = {
535         // apply defaults
536         setOptions: function(options) {
537                 this.options = Animator.applyDefaults({
538                         // by default, each call to AnimatorChain.play() calls jumpTo(0) of each animator
539                         // before playing, which can cause flickering if you have multiple animators all
540                         // targeting the same element. Set this to false to avoid this.
541                         resetOnPlay: true
542                 }, options);
543         },
544         // play each animator in turn
545         play: function() {
546                 this.forwards = true;
547                 this.current = -1;
548                 if (this.options.resetOnPlay) {
549                         for (var i=0; i<this.animators.length; i++) {
550                                 this.animators[i].jumpTo(0);
551                         }
552                 }
553                 this.advance();
554         },
555         // play all animators backwards
556         reverse: function() {
557                 this.forwards = false;
558                 this.current = this.animators.length;
559                 if (this.options.resetOnPlay) {
560                         for (var i=0; i<this.animators.length; i++) {
561                                 this.animators[i].jumpTo(1);
562                         }
563                 }
564                 this.advance();
565         },
566         // if we have just play()'d, then call reverse(), and vice versa
567         toggle: function() {
568                 if (this.forwards) {
569                         this.seekTo(0);
570                 } else {
571                         this.seekTo(1);
572                 }
573         },
574         // internal: install an event listener on an animator's onComplete option
575         // to trigger the next animator
576         listenTo: function(animator) {
577                 var oldOnComplete = animator.options.onComplete;
578                 var _this = this;
579                 animator.options.onComplete = function() {
580                         if (oldOnComplete) oldOnComplete.call(animator);
581                         _this.advance();
582                 }
583         },
584         // play the next animator
585         advance: function() {
586                 if (this.forwards) {
587                         if (this.animators[this.current + 1] == null) return;
588                         this.current++;
589                         this.animators[this.current].play();
590                 } else {
591                         if (this.animators[this.current - 1] == null) return;
592                         this.current--;
593                         this.animators[this.current].reverse();
594                 }
595         },
596         // this function is provided for drop-in compatibility with Animator objects,
597         // but only accepts 0 and 1 as target values
598         seekTo: function(target) {
599                 if (target <= 0) {
600                         this.forwards = false;
601                         this.animators[this.current].seekTo(0);
602                 } else {
603                         this.forwards = true;
604                         this.animators[this.current].seekTo(1);
605                 }
606         }
607 }
608
609 // an Accordion is a class that creates and controls a number of Animators. An array of elements is passed in,
610 // and for each element an Animator and a activator button is created. When an Animator's activator button is
611 // clicked, the Animator and all before it seek to 0, and all Animators after it seek to 1. This can be used to
612 // create the classic Accordion effect, hence the name.
613 // see setOptions for arguments
614 function Accordion(options) {
615         this.setOptions(options);
616         var selected = this.options.initialSection, current;
617         if (this.options.rememberance) {
618                 current = document.location.hash.substring(1);
619         }
620         this.rememberanceTexts = [];
621         this.ans = [];
622         var _this = this;
623         for (var i=0; i<this.options.sections.length; i++) {
624                 var el = this.options.sections[i];
625                 var an = new Animator(this.options.animatorOptions);
626                 var from = this.options.from + (this.options.shift * i);
627                 var to = this.options.to + (this.options.shift * i);
628                 an.addSubject(new NumericalStyleSubject(el, this.options.property, from, to, this.options.units));
629                 an.jumpTo(0);
630                 var activator = this.options.getActivator(el);
631                 activator.index = i;
632                 activator.onclick = function(){_this.show(this.index)};
633                 this.ans[this.ans.length] = an;
634                 this.rememberanceTexts[i] = activator.innerHTML.replace(/\s/g, "");
635                 if (this.rememberanceTexts[i] === current) {
636                         selected = i;
637                 }
638         }
639         this.show(selected);
640 }
641
642 Accordion.prototype = {
643         // apply defaults
644         setOptions: function(options) {
645                 this.options = Object.extend({
646                         // REQUIRED: an array of elements to use as the accordion sections
647                         sections: null,
648                         // a function that locates an activator button element given a section element.
649                         // by default it takes a button id from the section's "activator" attibute
650                         getActivator: function(el) {return document.getElementById(el.getAttribute("activator"))},
651                         // shifts each animator's range, for example with options {from:0,to:100,shift:20}
652                         // the animators' ranges will be 0-100, 20-120, 40-140 etc.
653                         shift: 0,
654                         // the first page to show
655                         initialSection: 0,
656                         // if set to true, document.location.hash will be used to preserve the open section across page reloads 
657                         rememberance: true,
658                         // constructor arguments to the Animator objects
659                         animatorOptions: {}
660                 }, options || {});
661         },
662         show: function(section) {
663                 for (var i=0; i<this.ans.length; i++) {
664                         this.ans[i].seekTo(i > section ? 1 : 0);
665                 }
666                 if (this.options.rememberance) {
667                         document.location.hash = this.rememberanceTexts[section];
668                 }
669         }
670 }