Javascript ist Toll!

Javascript > Skripte

3. September 2010 - 13:27

Tweening – Effekte mit Javascript

Tweening und Transition sind zwei Begriffe aus der Welt der Computeranimation. Heute möchte ich eine Klasse zeigen, wie diese Techniken auch bei Javascript Animationen Verwendung finden.

Das Prinzip der Techniken ist relativ schnell erklärt.
Bei einer normalen Animation, wie sie z.b. auch mein aufgleitendes Popup benutzt, ist die Veränderung des Wertes (hier die Höhe und Breite) linear zur Zeit. In jedem Interval werden die Werte um den gleichen Wert verändert. Dadurch wirkt die Animation statisch und langweilig.

Ein weiterer großer Nachteil dieser Methode ist, dass wenn der Ablauf einmal haken sollte, z.b. weil die Berechnung länger dauert oder der Browser einen kurzen Moment mit etwas anderen beschäftigt ist, dann stockt die Animation kurz und wird danach einfach fortgesetzt, es findet also keine flüssige Ausführung statt.
Ausserdem läßt sich die Ausführungszeit nur verändern, in dem der Abstand der einzelnen Schritte in jedem Interval, vergrößert oder verkleinert wird.

Das folgende Diagramm, zeigt den Ablauf solch einer Art der Animation. Die horizontalen Achse ist die Zeit und vertikal, der Wert der Änderung (in dem Beispiel des aufgleitenden Popups, wär das die Breite oder die Höhe):

Diagramm: linearer Tween
Diagramm (1) - lineare Animation

Tweening

Die Animation läuft dagegen flüssiger, wenn der Wert der Veränderung, in Relation zu der abgelaufenen Zeit gesetzt wird.
Das heißt die Bewegung läuft im Verhältnis zur Zeit und wirkt dadurch geschmeidiger. Tweening wird es deshalb genannt, weil die Bilder (hier Werte) zwischen dem Zeitinterval berechnet werden, aber nicht jedes Einzelbild (Wert). Transition heißt Übergang. Also der Übergang von einem Zustand in den anderen.

Bildlich läßt sich Tweening in einem Diagramm sichtbar machen. Wenn im Formular des Beispiel unten die lineare Funktion ausgewählt wird und ein relativ kurzer Zeitinterval für den Ablauf der Animation eingetragen wird, z.b. 100ms, dann sieht ein Diagramm des Ablauf z.b. so aus:

Diagramm: linearer schneller Tween
Diagramm (2) - lienar Tweening in 100ms

Es läßt sich erkennen, dass zwischen den einzelnen Punkten ein großer Abstand ist. Aber alle Punkte liegen auf der selben Geraden, wie im Diagramm oben.

Der Abstand kommt daher, weil der Browser nur in bestimmten minimalen Intervallen die Funktion zum Berechnen des Wertes ausführen kann. Firefox braucht typischerweise ca. 10ms für ein Interval, egal wie klein die Funktion ist, die ausgeführt wird. Er kann also in einer Gesamtdauer von 100ms, nur ca. 10 Punkte berechnen. Trotzdem wirkt die Animation so, als ob das Viereck sich flüssig bewegt, da unser Auge aus den Sprüngen, die eigentlich statt finden, eine kontinuierliche Bewegung formen.

Ease Effekte

Ein weiterer Effekt, der in einem solchen Bewegungsablauf oft eingebaut wird, heißt im englischen Ease. Was soviel bedeutet wie bremsen. Es soll also ein Bremseffekt (oder Beschleunigungseffekt. Was aber im Prinzip das Gleiche ist.) erzeugt werden, der den Ablauf natürlicher erscheinen läßt. Je nachdem wann dieser Effekt wirkt, sprich man von easeIn oder easeOut. Also zu Beginn oder beim Ende der Animation.

Dieser Effekt kann dann noch durch Mathematische Funktionen verstärkt oder abgeschwächt werden, z.b. durch die Berechnung einer Potenz oder des Sinus.
Daneben gibt es besondere Effekte wie z.b. bounce, dadurch wird ein Springeffekt erzeugt wie wenn ein Gummiball auf den Boden fällt oder elastic, wie an einem Gummiband.
Daher die Bezeichnungen für die ease Efekte, wie z.b easeInQuad, easeInOutSine, bounceEaseOut usw.

Beispiel

.duration: ms - .ease:

Das folgende Diagramm zeigt nach Ablauf der Funktion an welche Punkte gesetzt wurden.

Die Funktionen, die in dem Beispiel Formular ausgewählt werden können, stammen zum großteil aus dem Kapitel von Robert Penners Buch und wurden von mir für meine Klasse modifiziert. Die Funktionen sahen im Prinzip alle so aus:

javascript
Ease.InQuad = function (t, b, c, d) {
return c*(t/=d)*t + b;
};

Der erste Parameter ist der Zeitinterval, der zweite der Offset, der dritte der maximale und der vierte die Differenz zwischen Offset und Maximal. Ich habe diese Funktionen modifiziert, um die Berechnungen etwas zu beschleunigen. Da alle Werte ausser t statisch sind und in meiner generischen Tween Klasse der Offset 0 ist (und damit die Differenz immer gleich dem maximal Wert), sieht die Funktion jetzt so aus:

javascript
Ease.InQuad: function (c) {
	return function(t) {
		return c * (t /= c) * t;
	};
}

Die Funktion gibt eine Funktion zurück, die nur noch den Zeit abhängigen Parameter erwartet. Das optimiert etwas die Berechnung (s. Quelltext) während einer Animation.

Weitere Informationen

Das meiste, was sich zu diesem Thema im Netz findet, handelt von ActionScript.
Dort sind diese Effekte Teile des Sprachumfangs und finden vielfältig Anwendung. Da die Sprache Javascript sehr ähnlich ist, lassen sich diese mit etwas Grundverständnis in Javascript, aber gut nachvollziehen.

Mir persönlich hat für das Verständnis der ganzen Sache sehr viel dieses Kapitel aus dem Buch Programming MAcromedia Flash MX< von Robert Penner geholfen. Auf seiner Seite finden sich noch ein paar Flash Beispiele, z.b. dieses Flashdemo.

Die Begriffe auf deutsch werden auf dieser Seite der FH Augsburg erklärt.

Und hier hat noch jemand anderes, vor ein paar Jahren, eine Umsetzung in Javascript geschrieben: Javascript motion tween. Das Skript ist mehr oder weniger Umsetzung der ActionScript API und auf der Seite sind einige Beispiele, die zeigen was damit alles möglich ist.

Nicht vergessen hinzuweisen, möchte ich an dieser Stelle auf Mootools. Dessen Effekte basieren ebenfalls auf dieser Technik. Die Klasse Fx.Tween ist im Rahmen des Frameworks ein ausgereiftes Hilfsmittel, um vielfältige JS Animationen zu produzieren. Wer jetzt nicht etwas selbstgeschriebenes basteln will, kann damit sicher gut Leben.

tween.js

tween.js

Die Klasse kann als Grundlage für eigene Animationsklassen benutzt werden und ich werde diese in Zukunft benutzen um zu zeigen, wie man einfache aber effektive Animation umsetzt. Abschliessend noch der komplette kommentierte Quelltext.

javascript: js/inc/tween.js
/*
 * =========================================================
 * Objekt: Tween
 * Version: 2.0
 * Datum: 12:09 22.06.2011
 * Autor: J. Strübig <http://javascript.jstruebig.de>
 * =========================================================
 *
 * Eine Klasse um einen Ablauf in einem Zeitfenster zu erzeugen.
 * Der Effekt heisst auf Englisch tweening (Kurzform für: in-betweening)
 * 
 * Es wird eine Zeitspanne festgelegt (in Millisekunden) innerhalb
 * der Effekt ausgeführt wird. 
 * 
 * 
 * Methoden
 * =========================================================
 * 
 * start(obj)   startet die Animation. obj sind die Parameter als Object
 * isBusy()     testet ob die Animation läuft
 * rewind()     läßt die Animation Rückwärts laufen
 * stop()       hält die Animation an
 * restart()    setzt die Animation fort
 *
 * Eigenschaften
 * =========================================================
 * 
 * .duration	Gesamtdauer des Effekts (default: 500ms)
 * .interval	Dauer eines Frames/Einzelschritt (default: 0ms)
 * .max			Maximalwert bei Ende des Effekts (default: 1)
 * .ease        Funktionsreferenz s. unten Ease Objekt
 *	
 *
 * Alle Attribute können beim Aufruf der Startfunktion,
 * als Objektattribut übergeben werden.
 *
 * var tween = new Tween();
 * tween.start({duration:800, max:100, ease: Ease.InOutQuad});
 * 
 * Eventattribute
 * =========================================================
 *
 * .onmove
 * =========================================================
 * 
 * Der Event wird so oft aufgerufen, wie möglich, dabei 3 Parameter übergeben
 * 1. aktueller Wert der Berechnung
 * 2. abgelaufene Zeit
 * 3. Dauer bis zum Ende
 *
 * Wenn diese Funktion false zurück gibt, wird der Effekt beendet.
 *
 * Zuweisung
 * obj.onmove = function(p, now, duration)  {}
 *
 * .onready
 * =========================================================
 *
 * wird am Ende, nach dem letzten Aufruf von onmove aufgerufen.
 * Parameter: Zeit des letzten Events
 *
*/
function Tween() {
    this.duration	= 500;
	this.interval 	= 0;
	this.max 		= 1;
    this.ease       = Ease.linear;
 
    var busy 		= false;    // Indikator
    var start_time;             // Sartzeit
    var step;                   // Wert pro Millisekunde
    var direction   = 1;
	var func;
    var _timer;
    var restart;
    var now;
    var _this 		= this;     // lokale Objektkopie
 
    // Lokale Funktionen
    var time 		= function() {return +new Date();};
    var move 		= function() {
        now     = time();
        var dif = Math.min(now - start_time, _this.duration);
        var val = direction < 0 ? _this.max - func(dif * step) : func(dif * step);
 
        if(
		_this.onmove(val, dif, _this.duration) === false 
		|| _this.duration === dif
		) {
			window.clearInterval(_timer);
			busy        = false;
            direction   = 1;
            restart     = 0;
			_this.onready(now - start_time);
        }
    };
    // Dummyevents    
    this.onmove = this.onready = function(){return true};
 
    this.start = function(p) {
        if(busy) return false;
        busy        = true;
 
		// Parameter setzen
		for(var a in p) this[a] = p[a];
		this.duration -= 0; // convert to number
		if(!this.duration || this.duration < 0) this.duration = 100;
        if(typeof this.ease != 'function') this.ease = Ease.linear;
 
		restart 	= 0;
		func 		= this.ease(this.max);
        step        = 1 / this.duration * this.max;
        start_time  = time();
 
		_timer = window.setInterval(move, this.interval);
        return true;
	};
    this.rewind = function() {
        direction = -direction;
        if(!busy) if(!this.restart()) this.start();
    };
    this.stop = function() { 
        if(!_timer) return false;
        window.clearInterval(_timer);
        restart = start_time - now;
        busy = false;
        return true;
    };
    this.restart = function() {
        if(!restart) return false;
        start_time  = time() + restart;
		_timer = window.setInterval(move, this.interval);
        return true;
    };
    this.isBusy = function() { return busy;};
}
 
/*
 * ease-object
 *
 * Every function return a function which accept one parameter
 *
 * To create this func we need one parameter:
 *
 * c = the range of the Transition
 *
*/
var Ease = {
linear: function (c) {
    return function(t){ return t; };
},
regularIn: function(c){
	return function(t) {
		return c * (t /= c) * t;
	};
},
regularOut : function(c){
	return function(t) {
		return -c * (t /= c) * (t - 2);
	};
},
regularInOut : function(c){
	var fix = c / 2;
	return function(t) {
		if ((t /= fix) < 1) return fix * t * t;
		return -fix * ((--t) * (t - 2) - 1);
	};
},
strongInOut : function(c){
    return function(t) {
        return c * ( t /= c) * t * t * t * t;
    };
},
strongIn : function(c){
	return function(t) {
        return c * (t /= c) * t * t * t * t;
    };
},
strongOut : function (c) {
	return function(t) {
        return c * ((t = t / c - 1) * t * t * t * t + 1);
	};
},
 
InOutCubic : function (c) {
	var fix = c / 2;
	return function(t) {
		return (t/=fix) < 1 
		? fix * Math.pow (t, 3)
		: fix * (Math.pow (t-2, 3) + 2)
		;
	};
},
InQuad: function (c) {
	return function(t) {
		return c * (t /= c) * t;
	};
},
OutQuad: function (c) {
	return function(t) {
		return -c * (t /= c) * (t - 2);
	};
},
 
InOutQuad : function (c) {
	var fix = c / 2;
	return function(t) {
		return (t /= fix) < 1 
		?  fix * t * t
		: -fix * ((--t)*(t - 2) - 1)
		;
	};
},
InOutQuart : function (c) {
	var fix = c / 2;
	return function(t) {
		if ( (t /= fix) < 1) return fix * Math.pow (t, 4);
		return -fix * (Math.pow (t - 2, 4) - 2);
	};
},
InOutQuint : function (c) {
	var fix = c / 2;
	return function(t) {
		return (t /= fix) < 1 
		? fix * Math.pow (t, 5)
		: fix * (Math.pow (t-2, 5) + 2)
		;
	};
},
InOutSine : function (c) {
	var fix = c / 2;
	return function(t) {
		return fix * (1 - Math.cos(Math.PI / c * t));
	};
 
},
OutExpo : function (c) {
	return function(t) {
		return c * (-Math.pow(2, -10 * t/c) + 1);
	};
},
InExpo : function (c) {
	return function(t) {
		return c * Math.pow(2, 10 * (t/c - 1));
	};
},
InOutExpo : function (c) {
	var fix = c / 2;
	return function(t) {
		if ((t /= fix) < 1) return fix * Math.pow(2, 10 * (t - 1));
		return fix * (-Math.pow(2, -10 * --t) + 2);
	};
},
InCirc : function (c) {
	return function(t) {
		return c * (1 - Math.sqrt(1 - (t /= c)*t));
	};
},
OutCirc : function (c) {
	return function(t) {
		return c * Math.sqrt(1 - (t = t / c-1) * t);
	};
},
InOutCirc : function (c) {
	var fix = c / 2;
	return function(t) {
		if ( (t /= fix) < 1) return fix * (1 - Math.sqrt(1 - t*t));
		return fix * (Math.sqrt(1 - (t -= 2)*t) + 1);
	};
},
bounceOut : function(c){
	return function(t) {
		if ((t/=c) < (1/2.75)) {
			return c*(7.5625*t*t);
		} else if (t < (2/2.75)) {
			return c*(7.5625*(t-=(1.5/2.75))*t + .75);
		} else if (t < (2.5/2.75)) {
			return c*(7.5625*(t-=(2.25/2.75))*t + .9375);
		}
		return c*(7.5625*(t-=(2.625/2.75))*t + .984375);
	};
},
bounceIn : function(c){
	var f = Ease.bounceOut(c);
	return function(t) {
		return c - f(c - t);
	};
},
bounceInOut : function(c){
	var f1 = Ease.bounceIn(c);
	var f2 = Ease.bounceOut(c);
	return function(t) {
		if (t < c/2) return  f1(t*2) * .5;
		else return  f2(t*2-c) * .5 + c *.5;
	};
},
elasticIn : function(c, a, p){
	if (!p) p= c * .3;
	if (!a || a < Math.abs(c)) {
		a = c; 
		var s = p / 4;
	} else {
		var s = p / (2 * Math.PI) * Math.asin (c / a);
	}
	return function(t) {
		if (t==0) return 0;  
		if ((t/=c)==1) return c;
		return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin( (t * c - s) * (2 * Math.PI) / p ));
	};
},
elasticOut : function (c, a, p){
	if (!p) p =c * .3;
	if (!a || a < Math.abs(c)) { 
		a = c; 
		var s = p / 4; 
	}else {
		var s = p / (2*Math.PI) * Math.asin (c/a);
	}
	return function(t) {
		if (t==0) return 0;
		if ((t/=c) == 1) return c;
		return a * Math.pow(2,-10 * t) * Math.sin( (t*c - s)*(2*Math.PI)/p ) + c;
	};
},
elasticInOut : function (c,a,p){
	if (!p) p = c * (.3 * 1.5);
	if (!a || a < Math.abs(c)) {
		a = c;
		var s = p / 4;
	} else {
		var s = p / (2 * Math.PI) * Math.asin (c / a);
	}
	return function(t) {
		if (t == 0) return 0;
		if ((t /= c/2) == 2) return c;
 
		if (t < 1) return -.5 * (a * Math.pow(2, 10 * (t-=1)) * Math.sin( (t * c - s) * (2 * Math.PI) / p));
		return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * c - s) * (2 * Math.PI) / p ) * .5 + c ;
	};
},
backIn: function(c){
    var s = 1.70158;
	return function(t) {
        return c * (t /= c) * t * ((s + 1) * t - s);
    };
},
backOut: function(c){
	var s = 1.70158;
    return function(t) {
        return c * ((t = t / c - 1) * t * ((s + 1) * t + s) + 1);
    };
},
backInOut : function(c){
 
    return function(t) {
        var s = 1.70158; 
        if ((t /= c / 2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s));
        return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2);
    };
}
 
}; // Ease
 

In eigener Sache

Vielleicht warten einige Leser auf die Fortsetzung meiner Reihe OOP mit Javascript. Ich bin während der Recherche zum 2. Teil auf einige Aspekte gestossen, die mein ursprüngliches Konzept völlig über den Haufen geworfen haben und ich daher die eigentlich vorgesehene Klasse neu geschrieben habe, aber dabei auch viele Ideen, wie in JS OOP am sinnvollsten umgesetzt wird. Hier gehen nämlich viele Ansätze zu weit und übersehen dabei, wie flexibel JS eigentlich ist.
In der Zwischenzeit kann ich diesen Artikel empfehlen, der eine der Problematiken bei meinem Ansatz in Worte faßt.
Aber auch interessant ist es, wie es die grossen Frameworks mit der Vererbung handhaben. Darüber hat Mathias einen aufschlussreichen Artikel geschrieben, der auch einige Funktionshintergründe anschaulich mit Grafiken illustriert.

Ähnliche Artikel

Comments (2)
3890 mal gelesen.

2 Comments

Einen Kommentar hinterlassen »

Kommentare

1. von DerGenaue
Freitag 8.Juli 2011: 14:21

Sehr interessanter Artikel, leide mit Fehler:
Z.37:
* var tween = new Tween();
* tween.start({duration:800, max:100, ease: Ease.InOutQuad});

Vielen Dank, derartiges kann ich gut gebrauchen!

2. von Struppi
Freitag 8.Juli 2011: 14:28

Danke!
Der Fehler ist korrigiert.

Einen Kommentar hinterlassen

Name (erforderlich)
Mail (wird nicht angezeigt) (erforderlich)
Website

Folgende HTML Tags sind erlaubt: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>



Powered by WordPress Stop Spam Harvesters, Join Project Honey Pot kostenloser Counter Browser-Statistiken
rats-wonderful
rats-wonderful
rats-wonderful
rats-wonderful