Im Rahmen des Projekts „Naturkunde 365/24“ des Museums für Naturkunde Berlin entsteht die Broschüren-Reihe „Hacking the Museum“, in welcher Grundlagen unterschiedlicher Programmiersprachen vermittelt werden. Unter anderem wird gezeigt, wie Anfragen an die API des Rechercheportals „Mediasphere for Nature“ gestellt werden, um hauseigene Medien des Museums abzufragen.
Anhand eines Memory-Spiels wird ein Einblick in die Verwendung von JavaScript gegeben.
Unten sind ein paar Screenshots des fertigen Spiels zu sehen:
Grundlegende Kenntnisse von HTML und CSS sollten vorhanden sein, da diese ohne umfassende Beschreibung eingesetzt werden.
Kontrollstrukturen wie if-else und for sowie die Verwendung von Variablen und Funktionen sollten auch bekannt sein.
Begleitend zu dieser Broschüre gibt es ein Repository auf GitHub. Das Repository kann von https://github.com/MfN-Berlin/JavaScript-Memory
heruntergeladen werden. Wer sich mit Git auskennt kann auch einen fork erstellen.
Zu jedem Kapitel gibt es einen Ordner, begonnen wird mit step-one. Das Ergebnis aus dem ersten Kapitel ist dann im Ordner step-two, der als Anfang für das zweite Kapitel dient, zu sehen. Aber im besten Fall, wird der nicht benötigt.
Die meisten Browser bieten Entwicklertools an, bei Firefox und Chrome kann man diese mit der Taste F12 öffnen. Der Inspektor bzw. unter dem Tab Elements wird das HTML angezeigt. Klickt man auf eins dieser Elemente, werden auch die zugehörigen CSS-Regeln angezeigt. Diese können nun auch direkt modifiziert und die Änderungen begutachtet werden. Unter dem Tab Konsole ist die JavaScript Konsole zu finden. Hier werden Warnungen und Fehlermeldungen angezeigt. Auch die Ausgaben des Befehls console.log() sind hier zu sehen. Vor allem, können ganz unten Befehle direkt eingegeben und ausgeführt werden. Bei Firefox ist die Eingabeaufforderung durch zwei schließende spitze Klammern (>>) gekennzeichnet, bei Chrome durch nur eine (>). Die Konsole ist gut geeignet, kleine Codeschnipsel schnell mal auszuprobieren und bietet außerdem Code Completion. Auch über JavaScript lassen sich HTML-Elemente bearbeiten. Ruf die Startseite von ecosia.org, öffne die Konsole und gib folgenden Befehl ein:
document.getElementsByClassName('container')[0].style.backgroundColor = 'blue'
Nach Betätigung der Enter-Taste wird ein Teil des weißen Hintergrunds Blau angezeigt. Die Änderung ist nur Temporär in deinem Browser zu sehen.
Ein weiterer Tab enthält den Debugger, verhält sich der Code nicht wie erwartet, kann er Schritt für Schritt ausgeführt werden und die Werte der verwendeten Variablen lassen sich inspizieren. Um an den Code an einer bestimmten Stelle anzuhalten, schreibt man das Schlüsselwort debugger in das Script.
All dies sind sehr nützliche Werkzeuge die das Lernen und die Arbeit erleichtern und Spaß machen - es lohnt sich also, ein paar Minuten zu investieren und in die Dokumentation deines Browsers zu schauen.
- https://www.w3schools.com
- https://git-scm.com/book/de/v2
- https://developer.mozilla.org/en-US/docs/Tools
- https://developers.google.com/web/tools/chrome-devtools/
Ordner: step-one
Ziel:
Im ersten Schritt wird das HTML um einen Button erweitert.
Wird dieser angeklickt, werden die Karten ausgeteilt.
Dem HTML-Gerüst wird dafür zunächst ein button-Element hinzugefügt. Über das onClick-Event wird die Startfunktion startGame aufgerufen. Diese bekommt als Parameter die Anzahl an Kartenpaaren mitgegeben, um später auf einfache Art die Anzahl an Karten zu verändern.
<button class="buttonNrCards" id="buttonNrCards6" onClick= "startGame(6)"> 6 Kartenpaare</button>
Die Funktion wird in der Datei game.js implementiert, die im selben Verzeichnis wie die Index Datei erstellt wird. Durch den Script-Tag wird sie zwischen den head-Tags in das HTML eingebunden:
<script src="game.js"></script>
Am Ende des Kapitels soll der Fensterinhalt nach dem Klick auf den Button so aussehen:
Der Button soll weg und eine Anzeige für die Zeit und die Züge sowie die verdeckten Karten sind zu sehen.
In der aufgerufenen Funktion startGame werden die einzelnen Schritte mit Hilfe von weiteren Funktionen ausgeführt:
var numberCards = 0;
function startGame(nrCards){
numberCards = nrCards;
setupGameGrid(nrCards);
let gameCards = pickCards(nrCards);
dealCards(gameCards);
showHideElements();
}
- setupGameGrid bestimmt die Größe des Grids anhand der Anzahl der Karten
- pickCards wählt die Karten
- dealCards erstellt für jede Karte die passenden HTML-Elmente
- showHideElements setzt den Button auf unsichtbar und die Anzeige für Zeit und Züge wird Sichtbar
Bevor diese im Einzelnen beschrieben werden, bedarf es noch einiger Änderungen in der HTML Datei. Für die Zeitangabe, die Anzahl der Züge sowie für die Anordnung der Karten auf dem Spielfeld wird je ein div benötigt.
<div id="timerDisplay" class="p2" ></div>
<div id="movesDisplay" class="p2"></div>
<div>
<button class="buttonNrCards" id="buttonNrCards6" onClick= "startGame(6)"> 6 Kartenpaare</button>
</div>
<div id="game">
<div id="grid"></div>
</div>
Die Funktion setupGameGrid stellt die Größe des Spielfelds über die CSS-Klasse ein. Im späteren Verlauf soll der Spieler wählen können mit wie viel Kartenpaaren er spielen möchte. Da diese Funktionalität noch nicht unterstützt wird ist die Implementierung recht simpel:
function setupGameGrid(nrCards){
let grid = document.getElementById('grid');
if(nrCards == 6){
grid.setAttribute('class', 'grid6');
}
}
Nun sollen die Karten gewählt werden. Noch haben wir allerdings gar keine Karten. Zunächst werden die Bilder aus dem img Ordner genommen, um die Funktionalität zu testen. Im Anschluss wird gezeigt, wie die Bilder mittels der API des Museums für Naturkunde Berlin geladen werden können.
In einem Array werden Objekte gespeichert, die zu jeder Karte den Pfad und einen eindeutigen Namen bereithalten.
let images = [
{'name': 'anglerfisch', 'img': '../img/anglerfisch.jpg',},
{'name': 'baumriese', 'img': '../img/baumriese.jpg',},
{'name': 'crewmithund', 'img': '../img/crewmithund.jpg',},
{'name': 'drachenfisch', 'img': '../img/drachenfisch.jpg',},
...
];
Aus diesem Array wird eine zufällige Menge ausgewählt, die Anzahl entsprechicht der gewünschten Anzahl an Kartenpaaren.
Dazu muss das Array zunächst gemischt werden, um im Anschluss die ersten X Elemente für die Karten verwenden zu können.
Die Utility-Library Lodash stellt die Funktion _.sampleSize(collection, [n=1]) zur Verfügung, welche genau das in einem einzigen Funktionsaufruf erledigt. Als Parameter übergibt man eine Collection(das Array mit den Bildern) und die gewünschte Anzahl an Elementen (in unserem Fall die Anzahl der Kartenpaare). Zurückgegeben wird ein neues Array mit der gewünschten Länge mit zufälligen Elementen des vorigen Arrays, womit es sich wunderbar weiterarbeiten lässt.
Um diese Funktion nutzen zu können muss lodash über ein Script Tag im Header eingebunden werden. Auf der Internetseite von Lodash (https://lodash.com/) findet man unter dem stichwort cdn, einen Link. Dieser wird als Wert für das src-Attribut verwendet:
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
Für ein Memory-Spiel wird jede Karte zweimal benötigt. Daher wird das Array mit den gewählten Kartenbildern mit Array.concat(collection) verdoppelt und noch mal gemischt.
Das ganze sieht dann so aus:
function pickCards(nrCards){
let cardSelection=_.sampleSize(images, nrCards);
return _.shuffle(cardSelection.concat(cardSelection));
}
_.sampleSize und _.shuffle sind Methoden aus der lodash-Bibliothek. Man erkennt sie an dem vorgesetzten Unterstrich.
Das Austeilen der Karten geschieht in der Funktion dealCards. Mit forEach wird über das Array gameCards iteriert und für jedes Element wird ein div erstellt. Der CSS-Eigenschaft background-image wird der Pfad zum Bild übergeben. Mithilfe von dataset können selbstdefinierte Attribute an ein HTML-Element gehängt werden. Hier wird der Name verwendet, um später überprüfen zu können, ob zwei Karten das gleiche Bild darstellen.
Um es so aussehen zu lassen, als ob die Karten gedreht werden, werden die CSS Eigenschaften backface-visibility und transform verwendet. Dafür wird jeweils ein div für die Vorder- und eines für die Rückseite verwendet. In den zugehörigen CSS-Klassen front und back ist der Einsatz der beiden genannten CSS Eigenschaften dargestellt. Unter https://jsfiddle.net/melanieKrauth/vdt9jcpk/ gibt es ein JSFiddle das die Eigenschaft backface-visibility ebenfalls demonstriert.
function dealCards(gameCards){
let grid = document.getElementById('grid');
gameCards.forEach(function(item){
var name = item.name;
var img = item.img;
var card = document.createElement('div');
card.classList.add('card');
card.dataset.name = name;
let front = document.createElement('div');
front.classList.add('front');
let back = document.createElement('div');
back.classList.add('back');
back.style.backgroundImage = `url(${img})`;
grid.appendChild(card);
card.appendChild(front);
card.appendChild(back);
});
}
Achtung bei der Zeile
back.style.backgroundImage = `url(${img})`;
Hier handelt es sich um template litrerals. Der Wert der Variablen img wird über das $-Zeichen in den String integriert. Wichtig ist die Verwendung der Backticks (die Taste für die Accent grave Zeichen, links des Fragezeichens).
Zugegeben, es ist hier ein bisschen verwirrend, dass das div der Klasse back das Bild als Hintergrund gesetzt wird - das ist historisch gewachsen ;-)
Info: dataset Attribute
Durch card.dataset.name wird dem HTML-Element ein individuelles Attribut angehängt. Informationen die nicht dargestellt werden sollen, können so JavaScript und CSS zur Verfügung gestellt werden. In HTML müssen diese Attribute mit data- beginnen und dürfen nur aus Kleinbuchstaben bestehen. Die Anzahl der Attribute ist beliebig.
<div class="card" data-name=name></div>Beim Zugriff durch JavaScript wird das data- weggelassen
document.getElementById(id).dataset.nameWeitere Information unter
https://www.w3schools.com/tags/att_global_data.asp
Zuletzt wird die Sichtbarkeit der nicht benötigten Elemente und die Anzeige für Zeit und Züge folgendermaßen gesetzt:
function showHideElements() {
document.getElementById('movesDisplay').innerHTML= "Züge: 0";
document.getElementById('timerDisplay').innerHTML= "Benötigte Zeit: 0 Minute(n) und 0 Sekunden";
document.getElementById('buttonNrCards6').disabled = true;
document.getElementById('buttonNrCards6').style.display = 'none';
}
- https://lodash.com/
- https://lodash.com/docs/4.17.10#sampleSize
- https://lodash.com/docs/4.17.10#shuffle
- https://www.w3schools.com/CSSref/css3_pr_backface-visibility.asp
Ordner: step-two
Ziel:
Die Karten drehen sich um wenn sie angeklickt werden.
Es können maximal 2 Karten angeklickt werden und nach einer gewissen Zeit werden diese wieder automatisch umgedreht.
Sind die ausgewählten Karten gleich, bleiben sie aufgedeckt liegen.
Die benötigte Zeit und Anzahl an Zügen wird angezeigt.
Wird jetzt mit der Maus auf eine Karte geklickt, passiert - nichts!
Also fügen wir dem grid am Ende der dealCards Funktion einen EventListener hinzu. Wenn eine Karte mit der Maus angeklickt wird, soll zunächst etwas auf der Konsole ausgegeben werden:
grid.addEventListener('click', () => console.log('click'));
Die Funktion addEventListener erwartet zwei Parameter, einen String, der den Typ des Events angibt und eine Funktion, die aufgerufen wird, wenn dieses Event eintrifft. Dieser Funktion wird bei Aufruf ein Event-Objekt übergeben.
() => console.log('click') ist eine Pfeilfunktion und äquivalent zu function printClick() {console.log('click')}
Info: EventListener
Aus HTML ist eventuell das onClick Event Attribut bekannt. Mit diesem lässt sich festlegen, was passieren soll, wenn das Element angeklickt wird. Das Anklicken wird als Event bezeichnet, es gibt eine Reihe an Events unterschiedlicher Kategorien. Mit der addEventListener Funktion lassen sich mit JavaScript EventListener nachträglich an selektierte oder neu erstellte Elemente anfügen.Syntax:
addEventListener(type, listener);
- type ist ein String der die Art des Events angibt
- listener ist die Funktion die aufgerufen werden soll, wenn dieses Event eintrifft. Als Parameter wird beim Aufruf ein Event-Objekt übergeben.
Dadurch ermöglicht JavaScript auf bestimmte Aktionen des Nutzers reagieren zu können und das HTML dynamisch zu verändern.
Bubbling
Als Bubbling bezeichnet man die Tatsache, dass, wenn eine Aktion auf einem Element ausgeführt wird, zunächst der ihm zugeordnete Listener aufgerufen wird, anschließend - falls vorhanden - der des darüberliegenden Elements und so weiter bis hinauf zum body-Element. Das Element auf dem das Event tatsächlich ausgeführt wurde, kann über event.target abgefragt werden.
Unsere JavaScript Datei ist bereits gut gewachsen. Um das Projekt übersichtlich zu gestalten, wird eine neue Datei erstellt, handler.js. Diese neue Datei muss auch in der index.html referenziert werden. Der Code wird in der Reihenfolge ausgeführt, in der die Script-Elemente im HTML auftauchen. Bisher wird nach dem Laden kein Code direkt aufgerufen, sondern erst beim anklicken des Buttons. Es ist aber wichtig, dies im Hinterkopf zu behalten da es Reference Errors auslösen kann, die darauf Hinweisen, dass Variablen oder Funktionen verwendet werden sollen, die noch nicht deklariert, bzw. definiert sind.
In die neue Datei kommt zunächst folgende Funktion:
function handleClick(event){
event.target.parentNode.classList.add('selected');
}
Als Parameter wird das Event-Objekt erwartet, das Informationen über das DOM-Element enthält, das angeklickt wurde. Da die Animation dem übergeordneten div zugeordnet ist, wird mittles parentNode dessen Klassenliste um selected erweitert.
Anstelle der Pfeilfunktion wird nun die Funktion handleClick vom eventListener in der game.js aufgerufen.
Um sicherzustellen, dass nur zwei Karten angeklickt werden, deklarieren wir eine neue globale Variable selectedCards erstellt und mit 0 initialisiert. Die handleClick wird um eine if-Abfrage erweitert, sodass die CSS-Klasse nur gesetzt wird, wenn die Anzahl der ausgewählten Karten kleiner zwei ist:
function handleClick(event){
if(selectedCards < 2){
event.target.parentNode.classList.add('selected');
selectedCards++;
}
}
Hat man zwei Karten gewählt, sollen sich die Karten nach einer gewissen Zeit wieder umdrehen. Zeitliche Steuerungen werden mit Timeouts realisiert, die über die globale Funktion setTimeout gesetzt werden.
Die Signatur der Funktion sieht so aus:
setTimeout(function, milliseconds);
Nach der angegebenen Zeit in Millisekunden wird die übergebene Funktion aufgerufen.
Achtung: Als Parameter wird nur der Name der Funktion übergeben, die runden Klammern würden einen sofortigen Aufruf der Funktion bedeuten. Soll die Funktion mit einem bestimmten Parameter aufgerufen werden, muss der Aufruf in einer weiteren Funktion gekapselt werden:
Ausprobieren kann man das auch in der JavaScript Konsole des Browsers, in Chrome und Firefox wird sie mit F12 geöffnet und dann der Tab Konsole gewählt.
setTimeout(() => alert('Hallo Welt'), 1500);
Hier wird nach 1.5 Sekunden ein PopUp mit dem Inhalt Hallo Welt im Browser geöffnet. Die Funktion alert wird mit "Hallo Welt" als Parameter aufgerufen.
Dieses Beispiel lässt sich ganz leicht in der JavaScript Konsole eines Browsers testen.
Damit die Karten wieder umgedreht werden, muss das CSS nach einer gewissen Zeit wieder geändert werden. Zunächst werden alle Elemente mit der Klasse selected gewählt, um dann von jedem dieser Elemente genau diese Klasse zu entfernen:
function resetGuesses(){
selectedCards = 0;
var selected = document.querySelectorAll('.selected');
selected.forEach(card => card.classList.remove('selected'));
}
In der handleClick wird der Timeout ausgelöst, wenn zwei Karten selektiert sind.
function handleClick(event){
const selectedElement = event.target;
if(selectedCards < 2){
selectedElement.parentNode.classList.add('selected');
selectedCards++;
}
if(selectedCards ==2){
setTimeout(resetGuesses, 1500);
}
}
Wenn jetzt zwischen die Karten geklickt wird, dreht sich das ganze Gitter. Um das zu verhindern findet zu Beginn der Funktion eine Überprüfung statt, dazu wird folgende Abfrage am Anfang der Funktion hinzugefügt:
if(selectedElement.id === 'grid' ||
selectedElement.parentNode.classList.contains('selected')){
return;
}
Bei der Gelegenheit wird auch gleich überprüft, ob das Element über die CSS-Klasse selected verfügt, also schon ausgewählt ist. Lässt man diesen Test aus, kann die selbe Karte zweimal angeklickt werden und so den Timeout auslösen.
Um die beiden gewählten Karten vergleichen zu können, werden die Elemente in ein Array gespeichert. Dazu wandeln wir die Variable selectedCards in ein leeres Array um. Auch zu beginn der Funktion resetGuesses wird die Variable selectedCards nun auf ein leeres Array anstatt auf 0 gesetzt.
In der handleClick Funktion wird das Element diesem Array hinzugefügt. Somit können bei zwei Karten die Namen aus dem Dataset verglichen werden. Sind sie gleich, wird die Methode match() nach einer Sekunde aufgerufen:
var selectedCards = [];
function handleClick(event){
const selectedElement = event.target;
if(selectedElement.id === 'grid' ||
selectedElement.parentNode.classList.contains('selected')){
return;
}
if(selectedCards.length < 2){
selectedCards.push(event.target);
selectedElement.parentNode.classList.add('selected');
}
if(selectedCards.length ==2){
if(selectedCards[0].parentNode.dataset.name === selectedCards[1].parentNode.dataset.name){
setTimeout(match, 1000);
return;
}
setTimeout(resetGuesses, 1500);
}
}
match() macht nichts weiter als den beiden selektierten Karten neue CSS-Klassen zuzuordnen und das Array zurück zu setzen.
function match(){
selectedCards = [];
const selected = document.querySelectorAll('.selected');
selected.forEach(function(card){
card.classList.add('disabled');
card.classList.add('match');
card.classList.remove('selected');
});
};
Um die Züge zu zählen, wird eine globale Variable moves erstellt und eine Funktion implementiert, die diese hochzählt und ausgibt. Innerhalb der match und resetGuesses Funktion wird incrementCounter aufgerufen.
function incrementCounter(){
moves++;
document.getElementById('movesDisplay').innerHTML= "Züge: " + moves;
}
Die Zeit anzuzeigen ist ein wenig komplexer. Dazu wird die Funktion setInterval verwendet. Diese führt eine Funktion in bestimmten Abständen aus. Wie setTimeout übergibt man eine Funktion und die Intervalldauer in Millisekunden. Der Rückgabewert stellt eine ID dar, welche an clearInterval übergeben werden kann, um die Wiederholung so anzuhalten.
In unserem Fall soll die Funktion jede Sekunde ausgeführt werden und dann die Zeit seit dem ersten Klick auf das Spielfeld zählen. Zunächst deklarieren wir zwei weitere globale Variablen namens startTime und intervalId die je auf undefined gesetzt werden. An den Beginn der handleClick-Funktion kommt eine if-Abfrage. Ist startTime undefined, so soll sie initial auf die aktuelle Zeit gesetzt werden. Diese holt man über ein neues Date-Objekt und der Funktion getTime(), welche die Anzahl an Millisekunden seit dem 1.1.1970 zurückgibt.
Hier der Anfang der geänderten handleClick:
function handleClick(event){
if(startTime === undefined){
startTime = new Date().getTime();
intervalId = setInterval(calculateTime , 1000);
}
var selectedElement = event.target;
if(selectedElement.id === 'grid' ||
selectedElement.parentNode.classList.contains('selected')){
...
Für calculateTime wird eine globale Variable elapsed benötigt. Innerhalb der Funktion wird durch ein weiteres Date-Objekt die aktuelle Zeit geholt und die Differenz zur Startzeit bestimmt. Um auch Minuten anzuzeigen, wird die vergangene Zeit durch 60 dividiert und mit Math.floor() abgerundet. Die Sekunden werden mithilfe des Modulo-Operators bestimmt und beides anschließend in das entsprechende HTML-Element geschrieben.
function calculateTime(){
var time = new Date().getTime() - startTime;
elapsed = Math.floor((time / 100) / 10 );
var min = 0, sec = 0;
if(elapsed > 59){
min = Math.floor(elapsed/60);
sec = elapsed % 60;
}else{
sec = elapsed;
}
document.getElementById('timerDisplay').innerHTML= "Benötigte Zeit: "+ min +" Minute(n) und " + sec + " Sekunden";
}
Sind alle Paare gefunden, soll calculateTime nicht mehr aufgerufen werden. Eine weitere globale Variable pairs speichert die Anzahl an gefundenen Paaren. Eine neue Funktion incrementPairs überprüft, ob sie gleich der Anzahl an Paaren ist und ruft dann clearInterval mit der ID auf. Die neu implementierte Funktion wird innerhalb der if-Abfrage aufgerufen, in der geprüft wird, ob die beiden selektierten Karten die gleichen sind.
function incrementPairs(){
pairs++;
if(pairs === numberCards){
clearInterval(intervalId)
}
}
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
- https://developer.mozilla.org/de/docs/Web/Events
- https://developer.mozilla.org/de/docs/Web/API/Event
- https://developer.mozilla.org/de/docs/Web/API/EventTarget/addEventListener
- https://www.w3schools.com/Jsref/met_win_settimeout.asp
- https://www.w3schools.com/jsref/met_win_setinterval.asp
- https://www.w3schools.com/js/js_dates.asp
- https://www.w3schools.com/js/js_date_methods.asp
- https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Math/floor
Ordner: step-three
Ziel:
Gefundene Paare verschwinden vom Spielfeld und werden an der rechten Seite angezeigt.
Sind alle Paare gefunden wird eine Zusammenfassung angezeigt und ein Ranking je nach benötigter Zeit und Anzahl von Zügen vergeben.
Wurde ein Paar gefunden, soll es jetzt nicht mehr auf dem Spielfeld liegen bleiben, sondern am Rand gelistet werden. Es kommt ein weiteres div mit der Id side in die index.html. In der showHideElements-Funktion wird die visibility auf visible gesetzt.
<div id="game">
<div id="grid"></div>
<div id="side" >
<p class="p2">Paare</p>
</div>
</div>
function showHideElements() {
...
document.getElementById('side').style.visibility = 'visible';
}
Die Bilder der gefundenen Paare sollen in dem neu erstellten div angezeigt und wie im folgendem Bild zu sehen übereinander gestapelt werden.
Beim ersten gefundenen Paar, soll das div um ein weiteres div (weiterführend in diesem Code-Abschnitt Container genannt) erweitert werden. An diesen Container werden die Bilder der nächsten drei gefundenen Paare angehängt. Das wird in der Funktion handlePairs implementiert. Zunächt wird das div mit der Id side selektiert. Je nachdem wieviele Paare bereits gefunden wurden, wird ein neuer Container erstellt, oder der letzte, bereits bestehende selektiert. Dann wird die Funktion createFoundPairElement(name) aufgerufen und das zurückgegebene Element dem letzten bestehenden Bild angehängt. Dazu wird eine weitere Funktion getLastChild(container) aufgerufen.
Diese neuen Funktionen werden zusammen in einer neuen Datei mit dem Namen side.js implementiert.
function handlePairs(clicked){
let container = undefined;
if(pairs === 1 || pairs % 3 == 1){
container = document.createElement('div');
container.className = 'container';
document.getElementById("side").appendChild(container);
}else{
let list = document.getElementsByClassName('container');
container = list[list.length-1];
}
let card = createFoundPairElement(clicked.parentNode.dataset.name);
let parent = getLastChild(container);
parent.appendChild(card);
}
createFoundPairElement(name) erstellt - ähnlich wie dealCards - ein neues div mit dem Bild des gefundenen Paar.
function createFoundPairElement(name){
let imgPath = '../img/'+name+'.jpg';
let card = document.createElement('div');
card.className = 'sideCard';
const cardImage = document.createElement('div');
cardImage.classList.add('sideCardImage');
cardImage.style.backgroundImage = `url(${imgPath})`;
card.appendChild(cardImage);
return card;
}
getLastChild(container) steigt durch die Kindknoten zum letzten hinab und gibt dieses zurück.
function getLastChild(container){
while(container.hasChildNodes()){
container = container.children[0]
}
return container;
}
Aufgerufen wird handlePairs nach der match Funktion in handleClick:
...
if(selectedCards.length === 2){
if(selectedCards[0].parentNode.dataset.name === selectedCards[1].parentNode.dataset.name){
setTimeout(match, 1000);
handlePairs(selectedElement);
incrementPairs();
...
Wenn alle paare gefunden sind, soll eine neue Anzeige mit der Zeit und einem Ranking erscheinen. Hier die zugehörigen weiteren HTML-Elemente:
<div id="popup1" class="overlay">
<div class="popup">
<h1 class="p3">Gewonnen! </h1>
<div class="result">
<p class="p2">Du hast <span id=finalMove> </span> Züge und
<span id=finalTime> </span> Sekunden gebraucht.
</p>
<p class="p3">
<span id=starRating></span>
</p>
</div>
</div>
<div>
<button class="restartButton" value="restart" onClick= "window.location.reload()">Neu starten</button>
</div>
</div>
Eine Funktion endGame macht die Elemente sichtbar und fügt die Zeit und Züge ein. Abhängig von der Anzahl der Karten und der Züge gibt es bis zu drei Sterne.
function endGame(){
let modal = document.getElementById("popup1")
modal.classList.add("show");
document.getElementById("finalMove").innerHTML = moves;
document.getElementById("finalTime").innerHTML = elapsed;
if(moves >= 6 && moves <10){
document.getElementById('starRating').innerHTML= "Rating: ★ ★ ★";
}else if(moves >= 10 && moves <14){
document.getElementById('starRating').innerHTML= "Rating: ★ ★ ☆";
}else if(moves => 14){
document.getElementById('starRating').innerHTML= "Rating: ★ ☆ ☆";
}
}
Diese Funktion wird in incrementPairs aufgerufen, wenn alle Paare gefunden wurden.
Ordner: step-four
Ziel:
Die Bilder werden übers Netzwerk geholt.
Um mehr Variation ins Spiel zu bringen werden die Bilder nicht mehr vom Ordner geladen, sondern über eine Schnittstelle vom Museum für Naturkunde Berlin geholt. Diese API bietet unterschiedliche Bildersets an. Um Inhalte von entfernten Quellen zu holen, bietet die fetch-API Möglichkeiten HTTP-Nachrichten zu verschicken. Zur Verfügungn steht die Funktion fetch() und die Objekte Headers, Request und Response.
Die Funktion hat einen vorgeschriebenen Parameter, einen String der den Pfad zum gewünschten Inhalt enthält oder ein Request-Objekt. Optional kann ein init-Objekt mit weiteren Angaben für den Request übergeben werden. Der Rückgabewert ist ein Promise-Objekt, dazu gleich mehr.
Folgendes einfaches Beispiel von fetch() lässt sich in der JavaScript Konsole des Browsers oder in einem JSFiddle testen:
fetch("https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js")
.then(function (response) {
if(!response.ok){
throw Error(response.statusText);
}
return response.text();})
.then(function (responseText) {
console.log(responseText);})
.catch(function(error){console.log('ERROR: ', error)});
Hier wird die JavaScript Library jQuery mittels fetch geholt und in der Konsole angezeigt. Dies läuft asynchron, wenn eine Antwort empfangen wurde, werden die beiden aneinander geketteten then-Funktionen aufgerufen. Die zweite erhält das Ergebnis der text-Funktion die von der ersten zurückgegeben wird, als Parameter. Wenn der Status der Antwort nicht ok ist wird eine Exception geworfen, sollten bei der Übertragung Netzwerkfehler auftreten, wird die catch-Funktion aufgerufen. Die Abfrage des Status muss manuell über das ok-Flag erfolgen.
Info: Promises
Promises werden für asynchrone Berechnung/Operation eingesetzt. Sie können sich in einem der folgenden Zustände befinden:
- pending: Initialer Status, Berechnung/Operation ist noch nicht fertig gestellt.
- fulfilled: Berechnung/Operation erfolgreich abgeschlossen
- rejected: Berechnung/Operation gescheitert
Promises haben eine then-Funktion. Dieser kann man als Parameter zwei Funktionen übergeben, eine die bei erfolgreiche Bereitstellung des Wertes ausgeführt wird, und die zweite für den Fehlerfall. Beide sind optional.
Da then auch wieder ein Promise zurückgibt, kann man mehrere hintereinander Ketten, siehe oben.
Das Museum für Naturkunde, Berlin arbeitet daran die digitalen Medien der Sammlung zur Verfügung zu stellen. Für die Bilder wird das IIIF Framework verwendet, welches von diversen Bibliotheken entwickelt wurde. Wikipedia beschreibt es folgendermaßen:
"Das International Interoperability Framework(IIIF) besteht aus zwei APIs.
- Die Image API definiert einen Web-Service zur Ausgabe von Bildern, zum Beispiel Format, Ausgabegröße und Zoomstufen, Ausschnitte, Farbtiefe und Rotation.
- Die Presentation API beschreibt die Ausgabe eines Objektes mit seinen bibliographischen und strukturellen Metadaten. Der Output erfolgt als JSON-LD-Objekte. Die Ausgabe der Images gemäß der Image API ist integriert."
Um ein einzelnes Bild über die Image API anzufordern muss folgende Syntax eingehalten werden:
{scheme}://{server}{/prefix}/{identifier}/{region}/{size}/{rotation}/{quality}.{format}
Praktisch kann das so aussehen:
http://www.example.org/image-service/abcd1234/full/full/0/default.jpg
Die Parameter region und size können neben full unter anderem auch als Pixel oder Prozent im Format x,y,w,h angegeben werden. Dabei definieren x und y die linke obere Ecke des Ausschnitts und w und h die Breite und Höhe darstellen. Die rotation erfolgt im Uhrzeigersinn in Grad. Wird ein Ausrufungszeichen davor gesetzt, wird das Bild zuerst gespiegelt und dann rotiert. Über den quality Parameter können die Bilder beispielsweise in Graustufen angefordert werden. Ausführliche Informationen und Beispiele gibt es in der Dokumentation.
Die Presentation API hält Informationen zu einem Objekt bereit, z.B einem Buch, in einer JSON Datei bereit, die Manifest genannt wird. Die einzelnen Ansichten des Objekts liegen in Sequenzen und die einzelnen Bilder wiederum in einem Canvas Container vor.
Unter https://jsfiddle.net/antkp8o0/61/ ist ein JSFiddle, in dem mittels fetch ein Manifest geholt wird und die einzelnen Bilder angezeigt werden. Dieses Beispiel zeigt die Verwendung eines Manifests. Ausserdem können die verschiedenen request Parameter ausprobiert werden.
Unter https://download.mediasphere.museumfuernaturkunde.berlin/data/ finden sich Links zu Unterschiedlichen Bildersets vom Museum für Naturkunde, Berlin.
Im Memory Code wird eine neue Funktion hinzugefügt. Im finalen Ordner ist diese Funktion in einer Extra Datei, getImagesFromAPI definiert.
function getImages(manifestUrl) {
fetch(manifestUrl)
.then(function (data) {
return data.json(); })
.then(function (json) {
images = [];
json.sequences[0].canvases.forEach((c, index) =>
images.push({
'name': index.toString(),
'img': c.images[0].resource.service['@id'] + '/full/227,153/0/default.jpg'
})
);
});
}
Wie im JSFiddle wird über die einzelnen Canvases des Manifests iteriert. Diesmal allerdings, wird das Array mit einem Objekt, bestehend aus der URL und dem Index gefüllt. Wird jetzt diese Funktion am Anfang der game.js aufgerufen, werden anstelle der Bilder aus dem img-Ordner, Bilder von Museumsobjekten angezeigt. Natürlich muss eine URL als Parameter übergeben werden, z.B.
getImages('https://download.mediasphere.museumfuernaturkunde.berlin/data/insekten_manifest.json');
Wenn jetzt eine runde gespielt wird, fällt auf, dass an der Seite kein Bild des gefundenen Paars mehr gezwigt wird. Das liegt daran, dass der imgPath in der Funktion createFoundPairElement jetzt nicht mehr stimmt. Zuvor wurde der Name der Bilddatei aus dem Dataset geholt. Schaut man im Browser in den Inspektor, sieht man, dass im data-name-Attribut jetzt der Index gespeichert wird. Die URL zum Bild muss aus dem style genauer gesagt dem backgroundImage des HTML-Elements geholt werden, dazu werden die folgenden - in Fett dargestellten - Zeilen in handleClick hinzugefügt und handlePairs mit dem entsprechenden Argument aufgerufen:
...
incrementPairs();
**let imgDiv = selectedElement.nextElementSibling**
**let style = (imgDiv.currentStyle || window.getComputedStyle(imgDiv, false));**
**let imgPath = style.backgroundImage.slice(4, -1).replace(/"/g, "");**
**handlePairs(imgPath);**
return;
...
In createFoundPairElement kann der Parameter dann direkt dem cardImage.style.backgroundImage mit `url(${name})` übergeben werden. Die Zeile let imgPath = '../img/'+name+'.jpg';
kann gelöscht werden, da der gesamte Pfad bereits in der Variable name enthalten ist.
Um zwischen den Bildersets wählen zu können, wird dem HTML ein form Element mit vier inputs vom Typ radio hinzugefügt:
<form id=select_image_set>
Mit welchen Bildern willst du spielen?<br>
<input type="radio" name="image_set" id="insects" onclick = "setManifest('insekten')" checked>Insekten
<input type="radio" name="image_set" id="molluscs" onclick = "setManifest('mollusken')">Mollusken
<input type="radio" name="image_set" id="minerals" onclick = "setManifest('mineralien')">Mineralien
<input type="radio" name="image_set" id="ehrenberg" onclick = "setManifest('ehrenberg')">Ehrenberg zeichnungen
</form>
Wird einer dieser Radio Buttons angeklickt, wird die Funktion setManifest aufgerufen, welche wiederum getImages mit der gewählten URL aufruft.
function setManifest(manifest){
getImages(MANIFESTE[manifest]);
}
Die URLs zu den Manifesten werden in einem konstanten Objekt zu beginn der game.js definiert:
const MANIFESTE = {
ehrenberg: "https://download.mediasphere.museumfuernaturkunde.berlin/data/ehrenberg_manifest.json",
insekten: "https://download.mediasphere.museumfuernaturkunde.berlin/data/insekten_manifest.json",
mollusken: "https://download.mediasphere.museumfuernaturkunde.berlin/data/mollusken_manifest.json",
mineralien: "https://download.mediasphere.museumfuernaturkunde.berlin/data/mineralien_manifest.json"
}
Das Schlüsselwort const besagt, dass der Wert dieser Variablen nicht verändert werden kann. Die einzelnen Eigenschaften eines konstanten Objekts allerdings, können verändert werden.
Die getImages Funktion kann nun initial mit einem Wert aus dem MANIFESTE-Objekt aufgerufen werden.
getImages(MANIFESTE['insekten']);
Auf Eigenschaften eines Objekts kann auf zwei Arten zugegriffen werden:
objektName.eigenschaftsName
Oder:
objektName["eigenschaftsName"]
Beachtet die Anführungszeichen bei der zweiten Methode.
Um all die Features der IIIF Image API zu nutzen schreiben wir eine Klasse, um auf einfache und sichere Art die Größe oder gewünschte Region zu setzen.
Die Klasse soll zwei Methoden haben, eine für die URL des default Bildes, mit Region und Größe auf full, Rotation 0. Eine weitere Methode, image() gibt die URL mit den manuell geänderten Parametern zurück. Für die definierten Parameter region, size, rotation und quality gibt es Attribute mit je einer set Methode.
Fangen wir einfach an:
class IIIFImage{
constructor( imageId){
this._imageId = imageId;
this._size = 'full';
this._region = 'full';
this._rotation = '0';
this._quality = 'default.jpg';
}
}
Klassen werden mit dem Schlüsselwort class deklariert. Die Funktion constructor wird beim erstellen einer Instanz mit dem new Operator automatisch aufgerufen und ist dafür verantwortlich, den benötigten Speicher zur Verfügung zu stellen. Die Felder _imageId, _size etc werden ebenfalls im Konstruktor definiert. Sie sind in allen der Klasse zugehörigen Funktionen sichtbar.
Alternativ können Klassen als sogenannte Expressions definiert werden:
var IIIFImage = class{
constructor( imageId){
this._imageId = imageId;
this._size = 'full';
this._region = 'full';
this._rotation = '0';
this._quality = 'default.jpg';
}
}
Eine Instanz dieser Klasse wird in beiden Fällen wie folgt erstellt:
let iiif = new IIIFImage('ganz/lange/url/zur/ID/des/Bildes');
Das hoisting funktioniert nicht für Klassen, soll heißen, sie muss vor Instantiierung deklariert sein.
Die IIIFImage-Klasse soll wie erwähnt zwei Funktionen haben um eine URL für das angegebene Bild zu generieren:
defaultImage(){
return this._imageId+'/full/full/0/default.jpg';
}
image(){
return this._imageId+'/'+this._region+'/'+this._size+'/'+this._rotation+'/'+this._quality;
}
Über den Punkt Operator können diese Funktionen nun aufgerufen werden:
let iiif = new IIIFImage('ganz/lange/url/zur/ID/des/Bildes');
iiif.defaultImage();
Die Parameter können ebenfalls über den Punkt Operator aufgerufen und geändert werden. Da die IIIF ImageAPI aber Parameter in einem bestimmten Format erwartet, ist das in diesem Falle nicht erwünscht. Umgehen lässt sich das mit dem Schlüsselwort set, die eine Funktion an eine Objekteigenschaft bindet. Diese Funktion wird aufgerufen, wenn die Eigenschaft geändert wird.
class IIIFImage{
...
set imageId(id){
if(typeof id === "string")
this._imageId = id;
}
}
let iiif = new IIIFImage('ganz/lange/url/zur/ID/des/Bildes');
iiif._imageId = 'neue/URL/zu/einem/anderen/Bild';
Die _imageId wird jetzt nur neu gesetzt wenn sie vom Typ "string" ist. Die ganze Klasse mit regulären Ausdrücken zur Überprüfung ist in einem JSFiddle unter https://jsfiddle.net/melanieKrauth/hbwm2yqn/ implementiert.
Ihr könnt nun euer Script um diese Klasse erweitern, dazu kommt sie am besten wieder in eine extra Datei, das ist zum einen übersichtlich und außerdem kann sie so leicht wiederverwendet werden.