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];