Malleus' Javascript Tutorials:
Version: 0.12 (25.04.2005) |
Das Ursprungs-Apfelmännchen | Vorschaubilder | Zoomfaktor: 8 |
![]() |
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
![]() |
Was ist aber nun das Besondere an einer Javascript-Implementierung?
Javascript kennt standardmäßig keine Funktion "setPixel(x,y,c)". Diese Funktion wird aber benötigt, um an der Stelle (x,y) des Koordinatensystems einen Punkt der Farbe "c" zu setzen.
Interessiert Dich das Thema? Dann kann's ja losgehen!
Im Laufe des Tutorials wirst Du verschiedene Arten "Scripting-Code" sehen. Diese werden der Übersicht halber verschiedenfarbig angezeigt:
Des Weiteren möchte ich noch darauf hinweisen, daß meine Scripte nach Fertigstellung stark komprimiert werden. Solltest Du also auf eine Funktion treffen, die Du nicht kennst, besteht die große Wahrscheinlichkeit, daß diese in der Scriptdatei "standard.js" definiert wird. Nähere Infos zur Javascript Komprimierung findest Du
hier.
<body onload="initPage()">
function initPage() { initCrossBrowser(); getElements( elementIds );
Dann aktiviere ich eine Browserweiche , die mir verschiedene "Flags" wie z.B. "isMSIE", "isMozilla" und "isO7" bereitstellt.
Nun erzeuge ich zu jedem HTML-Element, auf das ich später zugreifen werde, eine Referenz-Variable mit Namen "oElementId", d.h. "oSelectMode" würde auf das HTML-Element mit der Id "SelectMode" zeigen.
Zu diesem Zweck definiere ich mir am Anfang der Scriptdatei eine Stringvariable, die alle erforderlichen Ids enthält.
var elementIds = "MyBody,Box,MouseOverBox,ZoomBox,DebugBox,Col3,PBoxRow,BoxRow,"+ "StatusBox,HeaderBox,Pixel,Line," + "ButtonStart,ButtonReset,ButtonGoBackZoom," + "SelectZoomFactor,SelectFavorites," + "InputX1,InputX2,InputY1,InputY2," + "SelectMaxDepth,SelectColors,SelectSize,CheckBoxAnimate,"+ "OperaBug";
Aus dieser Liste erzeuge ich mir jetzt mit Hilfe der Funktion "getElements( elementIds )" entsprechende "globale" Variablen:
function getElements( o ) { o = o.split( "," ); for (var i = 0 ; i < o.length ; i++ ) { eval( "o" + o[ i ] + "=document.getElementById( '" + o[ i ] + "' ) " ); } }
Zuerst wird der als Parameter "o" übergebene "elementIds"-String in ein Array "transformiert". Danach wird für jedes Array-Element "o[i]" das dazugehörige HTML-Element referenziert und "abgespeichert".
Das HTML-Element mit der Id "ButtonStart" hat den Array-Index i = 13. Daraus wird nun der folgender String gebildet und als Scriptbefehl ausgewertet,
eval("oButtonStart=document.getElementById('ButtonStart')");
d.h. wenn ich später auf das Input-Element mit der Id="ButtonStart" zugreifen will, benutze ich einfach die Variable "oButtonStart".
Da es immer wieder Probleme mit dem Gültigkeitsbereich von Variablen gibt, möchte ich hier noch "ganz kurz" darauf eingehen:
var a = 1, b = 2, c = 3; function doSomething(a,e) { var f = b; c = a; a = 6; g = a + b; b = 7; }; function showValues() { alert( "a="+a+" b="+b+" c="+c+" g="+g ); } doSomething( 4 , 5 ); showValues();
Schau' Dir 'mal das Beispiel an. Was denkst Du, welche Werte wird die Funktion "showValues()" für "a","b","c" und "g" ausgeben?
Wir beginnen mit der Definition von drei globalen Variablen "a", "b" und "c". Dann rufen wir die Funktion "doSomething(4,5)" auf.
Der erste Parameter der Funktion "doSomething" heißt "a", d.h. innerhalb der Funktion wird die "globale" Variable "a" "versteckt".
Der zweite Parameter heißt "e" und ist wie die nachfolgende lokal definierte Variable "f" nur innerhalb der Funktion "doSomething" sichtbar.
Daraus ergibt sich folgendes:
Da die Variable "b" weder als Parameter noch als Variable innerhalb der Funktion "doSomething" definiert wurde, sucht der Scriptinterpreter nach der Variable "b" "eins höher", d.h. in dem Bereich, der die Funktion "doSomething" umschließt. Hier findet er die "globale" Variable "b". Diese hat den Wert "2", somit wird "f" auf "2" gesetzt.
Das gleiche gilt für "c": "c" ist kein Parameter und auch nicht lokal definiert. Somit ist die globale Variable "c" gemeint. Diese hatte ursprünglich den Wert "3". Dieser wird jetzt mit dem Wert des Parameters "a" überschrieben, d.h. "c" ist "4".
Nun wird die Parameter-Variable "a" auf den Wert "6" gesetzt.
Dann wird die Summe aus den Werten "a" und "b" ( 6 + 2 = 8 ) der Variablen "g" zugewiesen. Da die Variable "g" weder lokal noch global definiert wurde, geht der Scriptinterpreter hin und erzeugt eine neue "globale" Variable "g".
Zum Schluß der Funktion wird noch der Wert der globalen Variablen "b" auf "7" geändert. Das war's!
Somit gibt "showValues" folgende Werte aus: "a= 1 b=7 c=4 g=8".
Hättest Du es gewußt?
Nach diesem kleinen Abstecher geht's auch schon weiter: Eine Selectbox mit "Bild-Vorschlägen" wird erzeugt: Zu diesem Zweck wird für jeden Eintrag im "FAVORITES"-Array eine entsprechende Option der SelectBox mit der Id "SelectFavorites" erzeugt:
var FAVORITES=new Array( "-0.30625000000000035|0.06874999999999964|-0.6375|-0.9375|0|3|8" , "-1.553125|-1.178125|0.15937500000000004|-0.14062499999999994|0|3|8" // Die restlichen sechs Array-Elemente werden der Übersicht wegen entfernt. );
var options = new Array("0: - "); for ( var i = 0 ; i < FAVORITES.length ; i++) option.push(( i + 1 ) + ":" + i ); createToggleOptions( oSelectFavorites , options );
Um die Selectbox mit Hilfe der Funktion "createToggleOptions" "befüllen" zu können, erzeuge ich mir vorher ein kleines Array, das die Texte und Values der Selectbox definiert. Text und Value des dazugehörenden "OPTION"-Tags sind hierbei durch ein ":" getrennt.
Da es öfter vorkommt, daß man eine Selectbox zur Laufzeit befüllen möchte, werde ich dies anhand des nun folgenden Beispiels näher erklären:
var myOptionCodeArray = "0:A,1:B,2:C".split( "," ); createToggleOptions ( document.getElementById( "MySelect" ), myOptionCodeArray , "1" )
function createToggleOptions( obj , codes , searchItem ) { var startIndex = obj.options.length, selectedItem = -1; if ( startIndex == 1 && obj.options[0].text =="" ) { obj.options.length = 0; startIndex = 0; }; for (var i = 0 ; i < codes.length ; i++ ) { var v = codes[ i ].split( ":" ); obj.options[ startIndex + i ] = new Option( v[ 0 ], v[ 1 ] ); if ( searchItem == v[ 0 ] ) selectedItem = i; }; if ( selectedItem != -1 ) setSelectedIndex( obj , selectedItem ); };
<select id="test"> <option></option> </select>
Ist nur eine "OPTION" mit leerem Text vorhanden, gehe ich davon aus, daß es sich um ein "Dummy" handelt, d.h. das "OPTION"-Tag wurde "nur" aus "Validierungsgründen" in die HTML-Seite integriert. (siehe oben). Ich lösche daher das "OPTION"-Array der Selectbox und setze "startIndex" auf 0.
Nun gehe ich alle Array-Elemente der Reihe nach durch.
Die einzelnen Optionen werden durch einen String der Form "Value:Text" spezifiziert, d.h. ich muß nun die einzelnen Elemente trennen. Dies tue ich mit Hilfe der "split"-Methode, die einen String anhand eines Trenners, hier z.B. ":", in Teilstrings aufsplittet. Aus diesen n Teilstrings wird wiederum ein neues Array mit n Elementen erzeugt. In unserem Fall wäre das das Array "v", d.h. v[0] entspricht dem Value und v[1] dem Text.
Nun wird's interessant: Ich erzeuge mit "new Option()" ein neues "OPTION"-Element, welches ich der Selectbox hinzufüge und damit "sichtbar" schalte. Hier kommt der "startIndex" ins Spiel. Wenn keine "OPTION"-Elemente existieren, ist "startIndex" 0, d.h. das "i"-te neue Element ist auch das "i"-te "OPTION"-Tag der Selectbox. Bestanden beim Aufruf schon "OPTION"-Elemente, wird der "i"-te Neueintrag der "( startIndex + i )" - te Eintrag.<select id="yourSelect"> <option value="1"</>A<option> <option value="2"</>B<option> </select>
Der dritte Parameter "searchItem" gibt an, welcher Togglewert standarmäßig markiert werden soll. Ich überprüfe daher, ob der "Value" des gerade erzeugten "OPTION"-Tags diesem "searchItem" entspricht und merke mir dann dessen Index in "selectedItem".
Wurden alle Toggleitems erzeugt und "selectedItem" gesetzt, wird das dynamisch erzeugte "SELECT"-Element noch entsprechend eingestellt.
Nach dem doch etwas länger gewordenen Einschub geht's weiter mit der Initialisierung:
Um "sprachunabhängig" zu sein, habe ich alle Texte bzw. Meldungen in die HTML-Seite "mandel.html" ausgelagert. Sollte ich irgendwann einmal eine anderssprachige Version online stellen wollen, müßte ich nur die "HTML"-Seite anzupassen. Der Javascriptcode bliebe unberührt.
<script language="JavaScript" type="text/javascript"> var MSG=new Array( "Bild berechnen" , "Abbrechen", "Zoom", "Zeit" ); </script>
setValue( oButtonStart , MSG[ 0 ] ); oldSize = getSelectedIndex( oSelectSize ); setHeight( oO7Bug , getOffsetHeight( oBoxRow ) ); };
Gleich ist es geschafft:
Der Button zur Programmberechnung bekommt sein Label "Bild berechnen". Diverse "Layout-Bookmarks", auf die später noch eingehen werde, werden aktiviert.
Das war die Initalisierung!
Die Funktion "startCalculation" wird durch Drücken des Buttons "Bild berechnen" aufgerufen.
function startCalculation( reset ) { if ( isRunning ) { isRunning = false; setValue( oButtonStart,MSG [ 0 ] ); enable( oButtonReset ); return true; }; if ( reset ) { setValue( oInputX1 , "-2.2" ); setValue( oInputX2 , "0.8" ); setValue( oInputY1 , "1.2" ); setValue( oInputY2 , "-1.2" ); setSelectedIndex( oSelectMaxDepth , 0 ); totalZoomFactor = 1; zoomData = newArray(); disable( oButtonGoBackZoom ); };
"Läuft" gerade eine Bildberechung, d.h. "isRunning" ist "true", wird diese abgebrochen. Das Label des Buttons "ButtonStart" zeigt zu diesem Zeitpunkt den Text "MSG[ 1 ]" an, d.h. "Abbrechen". Dieser Text wird nun wieder auf seinen Ursprungswert "Bild berechnen" zurückgesetzt. Der beim Berechnungsstart deaktivierte Button zum Resetten der Berechnungsparameter wird wieder aktiviert. Die Funktion wird beendet.
Wurde die Funktion mit dem Parameter "true" aufgerufen, werden die Initialwerte des "großen Apfelmännchens" reaktiviert.
oInputX1, oInputX2, oInputY1 und oInputY spezifieren hierbei den zu berechneten Ausschnitt des Koordinatensystems.
oSelectMaxDepth, welches den Detailschärfe des Bildes beeinflußt, wird auf die niedrigste Stufe eingestellt.
Alle Zoom-spezifischen Daten werden gelöscht.
Mit dem MSIE besteht die Möglichkeit, das Apfelmännchen zu "animieren". Wurde die dazugehörende Checkbox, die bei anderen Browsern ausgeblendet wird, markiert, wird die "Farben"-Selectbox auf die "einzig" mögliche Farbpalette "COLORS[1]" eingestellt.
if ( isAnimating = isChecked( oCheckBoxAnimate ) ) setSelectedIndex( oSelectColors , 1 ); activeColorTable = COLORS[ getSelectedIndex( oSelectColors ) ]; maxColors = activeColorTable.length - 1;
Nährere Infos bezüglich der Animation findest Du weiter unten.
Farbpaletten werden in der globalen Variablen "COLORS" wie folgt spezifiziert:
var COLORS= new Array(); COLORS[ 0 ] = new Array( "" , "#FF00FF" , "#8000FF" , "#0000FF", "#0080FF" , "#00FFFF", "#00FF80" , "#00FF00" , "#80FF00" , "#FFFF00", "#FF8000" , "#FF0000" , "#FF0080" ); COLORS[ 1 ] = new Array( "", "#FF00FF", "#AA00FF" , "#5500FF" , "#0000FF" , "#0055FF" , "#00AAFF" , "#00FFFF" , "#00FFAA" , "#00FF55" , "#00FF00" , "#55FF00" , "#AAFF00" , "#FFFF00" , "#FFAA00" , "#FF5500" , "#FF0000" , "#FF0055" , "#FF00AA" ); COLORS[ 2 ] = new Array( "" , "#FF00FF" , "#CC00FF" , "#9900FF" , "#6600FF" , "#3300FF" , "#0000FF" , "#0033FF" , "#0066FF" , "#0099FF" , "#00CCFF" , "#00FFFF" , "#00FFCC" , "#00FF99" , "#00FF66" , "#00FF33" , "#00FF00" , "#33FF00" , "#66FF00" , "#99FF00" , "#CCFF00" , "#FFFF00" , "#FFCC00" , "#FF9900" , "#FF6600" , "#FF3300" , "#FF0000" , "#FF0033" , "#FF0066" , "#FF0099" , "#FF00CC" ); COLORS[ 3 ] = new Array( "" , "#FF00FF" , "#EE00FF" , "#DD00FF" , "#CC00FF" , "#BB00FF" , "#AA00FF" , "#9900FF" , "#8800FF" , "#7700FF" , "#6600FF" , "#5500FF" , "#4400FF" , "#3300FF" , "#2200FF" , "#1100FF", "#0000FF" , "#0011FF" , "#0022FF" , /* ... und so weiter ... */ "#00FFFF" , "#00FFEE" , // .... "#00FF00" , "#11FF00" , // ... "#FFFF00" , "#FFEE00" , // ... "#FF0000" , "#FF0011" , /* .... */ "#FF00CC" , "#FF00DD" , "#FF00EE" );
Mit diesem Daten versuche ich eine möglich schöne "Regenbogen"-Palette zu "bauen". Je geringer der Abstand zwischen zwei Farbwerten desto "fließender" wird der Übergang von einem Farbton zum anderen.
for (var i = 4 ; i < 9 ; i++ ) { COLORS[ i ] = COLORS[ 3 ].slice(); COLORS[ i ].rotate( ( i - 3 ) * 15 ); COLORS[ i ].deleteIndex( 91 - ( i - 3 ) * 15 ); };
Die Farbpaletten "4" bis "8" werden auf Basis der "3" berechnet. Da der Übergang von einem Farbton zum nächsten in 15 Schritte vollzogen wird, werden diese um den Faktor 15 "rotiert", d.h. COLORS[4][1] entspricht dem Wert "0000FF" bzw. COLORS[5][1] "00FFFF".
Array.prototype.rotate = function( n ) { for (var i = 0 ; i < n ; i++ ) this.push( this.shift() ); }
"Wie wird ein Array rotiert", wirst Du Dich jetzt vielleicht fragen? Ganz einfach:
Die "Prototype"-Methode "rotate" entfernt die "n" ersten Elemente des Arrays und fügt diese dann anschießend am Ende des Arrays wieder ein.
[Anmerkung: Solltest Du mit dem "Prototyping" nicht so vertraut sein, findest Du in meinem Wörterlabyrinth-Tutorial eine "kleine" Erklärung dazu.]
Der Leereintrag, der initial in "COLOR[3][0]" steht, wird nach der Rotation aus der neu generierten Palette entfernt.
var size = getToggleText( oSelectSize ).split( " x " ); maxX = 1 * size[ 0 ]; maxY = 1 * size[ 1 ]; maxDepth = 1*oSelectMaxDepth.value; xStart = getNumericValue( oInputX1 , -2.2 , 0.8 , -2.2 ); xStop = getNumericValue( oInputX2 ,-2.2 , 0.8 , 0.8 ); yStart = getNumericValue( oInputY1 , -1.2 , 1.2 , 1.2 ); yStop = getNumericValue( oInputY2 , -1.2 , 1.2 , -1.2 ); if ( xStop < xStart ) { setValue( oInputX2 , xStart ); setValue( oInputX1 , xStop ); }; if ( yStop > yStart ) { setValue( oInputY2 , yStart ); setValue( oInputY1 , yStop ); };
Nun bestimme ich die Breite und Höhe des zu berechneten Bildes. Dazu lese ich den Text des ausgewählten "OPTION"-Tags aus. Den linken bzw. rechten Teil dieses Wertes wandele ich dann in einen numerischen Wert um, d.h. aus "320 x 256" wird "maxX=320" und "maxY=256".
Danach bestimme ich die Berechnungstiefe. Dieser Wert gibt die Anzahl durchzuführender Berechnungsschritte pro Bildpunkt an. Hierbei gilt: Je tiefer man in das Apfelmännchen "hineinzoomt" desto mehr "Berechnungsschritte" pro Bildpunkt sollten durchgeführt werden.
Jetzt fehlen noch die Parameter des ausgewählten Ausschnittes:
function getNumericValue( o , a , b , d ) { var v = 1 * o.value; if ( isNaN( v )) { v = d; setValue( o , v ); } else if ( !( v >= a && v <= b) ) { v = d; setValue( o , v ); }; return v; }
Zu diesem Zweck überprüfe ich für jeden Koordinatenpunkt dessen Gültigkeit, d.h.
Die Variable "xStart" wird durch das "INPUT"-Element mit der ID "InputX1" spezifiziert. Dieser Wert muß größer als "-2.2" und kleiner als "0.8" sein. Ist dies nicht der Fall, wird der Wert auf "-2.2" gesetzt.
Für den Fall, das "xStart > xStop" bzw. "yStart > yStop" ist, vertausche ich die beiden Werte. Somit ist gewährleistet, daß z.B. eine Schleife von "xStart" bis "xStop" korrekt durchlaufen werden kann.
var newZoom = getValue( oInputX1 ) + "|" + getValue( oInputX2 ) + "|" + getValue( oInputY1 ) + "|" + getValue( oInputY2 ) + "|" + getValue( oSelectZoomFactor ); if ( zoomData.getLastChild() != newZoom ) zoomData.push( newZoom ); if ( zoomData.length > 1 ) enable( oButtonGoBackZoom ); else disable( oButtonGoBackZoom ); };
Nun merke ich mir noch die aktuellen Berechnungs-Parameter und aktiviere bzw. deaktiviere den "GoBack"-Button.
var newSize = getSelectedIndex( oSelectSize ); if (oldSize != newSize ) { if ( isO7 ) setDisplayNone(oMyBody); setHeightWidth( oBox , maxY , maxX - 1 ); setHeightWidth( oMouseOverBox , 2 * maxY , 2 * maxX ); setHeight(oPBoxRow , maxY ); setWidths( maxX , oStatusBox , oHeaderBox , oLine ); setWidth( oCol3 , maxX + 13 ); setHeight( oOperaBug , maxY + 6 * 20 ); if ( isO7 ) setDisplayBlock( oMyBody ); oldSize = newSize; };
Wurde die Größe des Bildes verändert, müssen einige HTML-Elemente wie z.B. die Status- und Kopfzeile angepaßt werden.
Zu diesem Zweck muß ich leider" bei Opera die HTML-Seite "vorher" unsichtbar schalten. Würde ich dies nicht tun, würde Opera die Tabelle nicht mehr "richtig" zentrieren. Ein kleines Übel, das hoffentlich bald behoben wird, aber okay: Was tue ich?
Im Wesentlichen verändere ich nur die Breite und Höhe des "Containers", der später das zu berechnete Bild enthalten soll.
Bist Du noch da? Dann halte durch, gleich wird's interessant.
changeZoomBox(); setHTML( oHeaderBox , MSG[ 2 ] + ": " + totalZoomFactor + "x" ); // MSG [ 2 ] = "Zoom" setHTML( oStatusBox ); // oStatusBox.innerHTML="&s;nbsp;" setValue( oButtonStart , MSG[ 1 ] ); // MSG[ 1 ] = "Abbruch" disable( oButtonReset ); enable( oSelectZoomFactor ); removeChilds( oBox ); startTime = newDate(); firstRun = isRunning = true; mandelY = yStart; dx = ( xStop - xStart ) / maxX; dy = ( yStop - yStart ) / maxY; call( "createMandelbrotMenge( 0 ) " ); // window.setTimeout( "createMandelbrotMenge( 0 )" , 1); };
Bevor die Berechnung des Bildes beginnt, wird noch
Du wirst Dich jetzt vielleicht fragen, warum ich die Funktion "createMandelbrotMenge" über einen "Timeout" starte. Die Erklärung dafür ist das Rendering-Verhalten der Browser. Der MSIE z.B. aktualisiert erst dann den Bildschirm, wenn die aufgerufende Funktion "komplett" abgeschlossen wurde. <script language="JavaScript" type="text/javascript"> function swapText() { var sum=0; document.getElementById( "testDiv" ).innerHTML = "Ich rechne..."; for (var i = 0 ; i < 1000000000 ; i++ ) for (var j = 0 ; j < 1000000000 ; j++ ) sum++; document.getElementById( "testDiv" ).innerHTML = "Fertig"; } </script> <DIV id="testDiv"></DIV> Bei dieser Funktion nimmt man doch normalerweise an, daß zuerst eine Ausgabe "Ich rechne..." erfolgt. Dann, nachdem die beiden "for"-Schleifen abgearbeitet worden sind, wird der Text "fertig" ausgegeben. Dem ist aber nicht so. Der Text "Ich rechne..." wird nicht erscheinen! Wie wir das ganze in unserem Fall einsetzen werden, wirst Du später erfahren. Also etwas Geduld ;-)
3. Weiterführende externe LinksCSS 4 You - Meiner Meinung nach "die" deutsche CSS-Referenz ! CSS/EDGE - Verblüffende CSS-Demos des bekannten Buchauthors "Eric A. Meyer" (en) Kristof Lipfert Browsererkennung durch JavaScript - Eigenschaften-Browserweiche eBook - Probekapitel:Speed Up Your Site: Web Site Optimization - Optimizing JavaScript for Execution Speed (en) Danny Goodman: Dynamic HTML: The Definitive Reference - Crossbrowser Event-Handling (en) 4. Disclaimer:Alle Scripte und Dokumente sind urheberrechtlich geschützt und dürfen nicht ohne meine Zustimmung kopiert, vervielfältigt oder übernommen werden. Alle Informationen auf meiner WebSite wurden von mir selbst mit bestem Wissen und Gewissen zusammengestellt. Einen Anspruch auf Fehlerfreiheit bzw. Vollständigkeit kann ich aber leider nicht erheben. Auch übernehme ich keinerlei Haftung für Fehler, die auf die Verwendung der Informationen zurückzuführen sind. Tschau! Frank Hammerschmidt ( aka Malleus ) P.S. Über einen Eintrag in mein Gästebuch bzw. einen Link auf meine Seite würde ich mich sehr freuen. Danke ! |