4
This library is released under the BSD license:
6
Copyright (c) 2006, Bernard Sumption. All rights reserved.
8
Redistribution and use in source and binary forms, with or without
9
modification, are permitted provided that the following conditions are met:
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.
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
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);
39
this.timerDelegate = function(){_this.onTimerEvent()};
45
Animator.prototype = {
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(){},
53
transition: Animator.tx.easeInOut
56
// animate from the current state to provided value
57
seekTo: function(to) {
58
this.seekFromTo(this.state, to);
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);
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));
74
// seek to the opposite of the current target
76
this.seekTo(1 - this.target);
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;
84
// remove all subjects
85
clearSubjects: function() {
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);
95
this.subjects[i](value);
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;
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;
108
this.state += movement;
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);
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();
135
// merge the properties of two objects
136
Animator.applyDefaults = function(defaults, prefs) {
138
var prop, result = {};
139
for (prop in defaults) result[prop] = prefs[prop] !== undefined ? prefs[prop] : defaults[prop];
142
// make an array from any object
143
Animator.makeArray = function(o) {
144
if (o == null) return [];
145
if (!o.length) return [o];
147
for (var i=0; i<o.length; i++) result[i] = o[i];
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];
155
var camelizedString = string.indexOf('-') == 0
156
? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
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);
163
return camelizedString;
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]));
170
return new Animator(options).addSubject(new CSSStyleSubject(el, style));
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);
179
// as makeEaseIn but for deceleration
180
Animator.makeEaseOut = function(a) {
181
return function(state) {
182
return 1 - Math.pow(1 - state, a*2);
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;
193
// make an Attack Decay Sustain Release envelope that starts and finishes on the same level
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;
201
if (state < decayEnd) {
202
return 1 - ((state - attackEnd) / (decayEnd - attackEnd) * (1 - sustainLevel));
204
if (state < sustainEnd) {
207
return sustainLevel * (1 - ((state - sustainEnd) / (1 - sustainEnd)));
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) {
216
return state <= 1 ? state : 2-state;
220
// pre-made transition functions to use with the 'transition' option
222
easeInOut: function(pos){
223
return ((-Math.cos(pos*Math.PI)/2) + 0.5);
225
linear: function(x) {
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)
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';
244
this.property = Animator.camelize(property);
246
this.from = parseFloat(from);
247
this.to = parseFloat(to);
248
this.units = units != null ? units : 'px';
250
NumericalStyleSubject.prototype = {
251
setState: function(state) {
252
var style = this.getStyle(state);
253
var visibility = (this.property == 'opacity' && state == 0) ? 'hidden' : '';
255
for (var i=0; i<this.els.length; i++) {
257
this.els[i].style[this.property] = style;
259
// ignore fontWeight - intermediate numerical values cause exeptions in firefox
260
if (this.property != 'fontWeight') throw e;
262
if (j++ > 20) return;
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;
271
inspect: function() {
272
return "\t" + this.property + "(" + this.from + this.units + " to " + this.to + this.units + ")\n";
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;
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);
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]
298
alert("Invalid colour: '" + color + "'");
301
getValueForState: function(color, state) {
302
return Math.round(this.from[color] + ((this.to[color] - this.from[color]) * state));
304
setState: function(state) {
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;
313
inspect: function() {
314
return "\t" + this.property + "(" + this.origFrom + " to " + this.origTo + ")\n";
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)) {
323
for (var i=1; i<=3; i++) {
324
part = Math.max(0, Math.min(255, parseInt(match[i])));
325
color += ColorStyleSubject.toColorPart(part);
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);
336
return '#' + match[1];
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;
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})$/;
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);
357
this.threshold = threshold || 0.5;
360
DiscreteStyleSubject.prototype = {
361
setState: function(state) {
363
for (var i=0; i<this.els.length; i++) {
364
this.els[i].style[this.property] = state <= this.threshold ? this.from : this.to;
367
inspect: function() {
368
return "\t" + this.property + "(" + this.from + " to " + this.to + " @ " + this.threshold + ")\n";
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);
378
if (els.length == 0) return;
379
var prop, toStyle, fromStyle;
381
fromStyle = this.parseStyle(style1, els[0]);
382
toStyle = this.parseStyle(style2, els[0]);
384
toStyle = this.parseStyle(style1, els[0]);
386
for (prop in toStyle) {
387
fromStyle[prop] = CSSStyleSubject.getStyle(els[0], prop);
390
// remove unchanging properties
392
for (prop in fromStyle) {
393
if (fromStyle[prop] == toStyle[prop]) {
394
delete fromStyle[prop];
395
delete toStyle[prop];
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 + '"');
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) {
420
} else if (reResult[1] != null) {
425
} else if (fromProp.match(CSSStyleSubject.discreteRe)
426
&& toProp.match(CSSStyleSubject.discreteRe)) {
429
type = DiscreteStyleSubject;
430
units = 0; // hack - how to get an animator option down to here
433
alert("Unrecognised format for value of "
434
+ prop + ": '" + fromStyle[prop] + "'");
438
this.subjects[this.subjects.length] = new type(els, prop, from, to, units);
442
CSSStyleSubject.prototype = {
443
// parses "width: 400px; color: #FFBB2E" to {width: "400px", color: "#FFBB2E"}
444
parseStyle: function(style, el) {
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]);
452
rtn[parts[1]] = parts[2];
456
// else assume style is a class name
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);
468
el.className = oldClass;
473
setState: function(state) {
474
for (var i=0; i<this.subjects.length; i++) {
475
this.subjects[i].setState(state);
478
inspect: function() {
480
for (var i=0; i<this.subjects.length; i++) {
481
str += this.subjects[i].inspect();
486
// get the current value of a css property,
487
CSSStyleSubject.getStyle = function(el, property){
489
if(document.defaultView && document.defaultView.getComputedStyle){
490
style = document.defaultView.getComputedStyle(el, "").getPropertyValue(property);
495
property = Animator.camelize(property);
497
style = el.currentStyle[property];
499
return style || el.style[property]
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+$/;
507
// required because the style object of elements isn't enumerable in Safari
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'];*/
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'];
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]);
530
this.forwards = false;
534
AnimatorChain.prototype = {
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.
544
// play each animator in turn
546
this.forwards = true;
548
if (this.options.resetOnPlay) {
549
for (var i=0; i<this.animators.length; i++) {
550
this.animators[i].jumpTo(0);
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);
566
// if we have just play()'d, then call reverse(), and vice versa
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;
579
animator.options.onComplete = function() {
580
if (oldOnComplete) oldOnComplete.call(animator);
584
// play the next animator
585
advance: function() {
587
if (this.animators[this.current + 1] == null) return;
589
this.animators[this.current].play();
591
if (this.animators[this.current - 1] == null) return;
593
this.animators[this.current].reverse();
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) {
600
this.forwards = false;
601
this.animators[this.current].seekTo(0);
603
this.forwards = true;
604
this.animators[this.current].seekTo(1);
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);
620
this.rememberanceTexts = [];
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));
630
var activator = this.options.getActivator(el);
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) {
642
Accordion.prototype = {
644
setOptions: function(options) {
645
this.options = Object.extend({
646
// REQUIRED: an array of elements to use as the accordion sections
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.
654
// the first page to show
656
// if set to true, document.location.hash will be used to preserve the open section across page reloads
658
// constructor arguments to the Animator objects
662
show: function(section) {
663
for (var i=0; i<this.ans.length; i++) {
664
this.ans[i].seekTo(i > section ? 1 : 0);
666
if (this.options.rememberance) {
667
document.location.hash = this.rememberanceTexts[section];