/adei/trunk

To get this branch, use:
bzr branch http://darksoft.org/webbzr/adei/trunk
200 by Suren A. Chilingaryan
Initial commit of gestures support for iDevices by Toni Pirhonen, Fix IE support broken by revision r196
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
}