Arrays en transformaties

In dit hoofdstuk leer je werken met arrays (lijsten). Ook ga je leren hoe je transformaties kan gebruiken. Dat is een handige en elegante manier om te bepalen waar en hoe vormen worden getekend op het scherm.


Arrays

JavaScript heeft (net als alle programmeertalen) een bepaalde datastructuur om gegevens op te slaan in een lijst. In JavaScript heet dat een array.

Een array is niks anders dan een hele lijst van variabelen achter elkaar. Stel dat je een aantal standaardmaten voor je vormen wilt gebruiken, dan kan je wel een heleboel variabelen maken (size1, size2, size3, etc) maar dat is erg onhandig. Je kan al die waarden beter opslaan in een array. De gegevens in een array noemen we elementen.

Hoe werkt dat dan?

Zo ziet een lege array eruit:

let sizes = [];

Elementen toevoegen:

sizes.push(5);
sizes.push(15);
sizes.push(10);

Maar je kunt de waarden er ook meteen in stoppen:

let sizes = [5, 15, 10, 25, 20];

De array bestaat nu uit 5 elementen: 5, 15 ,10, 25 en 20, in die volgorde. De positie van het element in de array noemen we de index. En computers beginnen te tellen bij 0, dus 5 is het 0e element, 15 het 1e, 10 het 2e, etc. Dit heet zero-based. Gebaseerd op nul betekent dat het eerste element van een array een index van 0 heeft in JavaScript. Dus als je het eerste element uit het array sizes wilt selecteren, moet je sizes[0] schrijven. En om het laatste element uit een array van vijf elementen te selecteren moet je sizes[4] schrijven (en dus niet sizes[5]).

De reden voor deze rare gewoonte ligt in de historie van het programmeren Wikipedia heeft een goede uitleg over de geschiedenis van het nul-indexen.

Elementen selecteren in de array doe je dus mbv. de index:

sizes[0];  // 5
sizes[3];  // 25

De lengte van de array:

sizes.length();  // 5

Loopen door de array:

for (let i = 0; i < sizes.length(); i++) {
console.log(sizes[i]);
}

Elementen verwijderen uit een array kan ook, op verschillende manieren.

Laatste element verwijderen:

sizes.pop();  // 20
console.log(sizes);  // [5, 15, 10, 25]

Eerste element verwijderen:

sizes.shift();  // 5
console.log(sizes);  // [15, 10, 25]

Een element verwijderen met de index:

sizes.splice(2, 1);  // verwijdert 1 element op index 2
console.log(sizes);  // [15, 10]

Goed, dat is nogal een hoop informatie in één keer. Laten we eens kijken hoe we een array in p5.js kunnen gebruiken, dan wordt het allemaal wat duidelijker. We nemen weer even het vorige voorbeeld met cirkels. Nu gaan we de grootte van de cirkels instellen mbv. een array:

Maar we kunnen ook de random() functie gebruiken om willekeurig een element uit een array te selecteren. In plaats van een getal als parameter kunnen we eenvoudigweg de array als parameter invoeren:

Of willekeurig de breedte en hoogte van de ellipsen variëren:

Als je een kleurenpalet wilt gebruiken met een aantal door jou vastgestelde kleuren is een array in combinatie met random ook heel handig:

Translate

Om vormen op een bepaalde plek op het scherm te tekenen kunnen we een "offset" variabele gebruiken. Hier is bijvoorbeeld een sketch die een smiley tekent waarbij je de positie van het gezicht kan veranderen door de xoffset en yoffset variabelen aan te passen (probeer maar eens):

Dit is op zich prima, maar er staat een hoop herhaling in de code: de namen xoffset en yoffset worden een aantal keer opgeschreven.

Je zal nu wel doorhebben dat veel programmeurs een hekel hebben aan het telkens opnieuw hetzelfde typen. Goede programmeurs zijn lui, en daarom verzinnen zij altijd manieren om herhaling te omzeilen. De programmeurs van p5js hebben voor dit geval de translate() functie bedacht.

Voordat we precies gaan uitleggen hoe de translate() werkt, is het misschien behulpzaam om de functie al een keer in actie te zien. Hier is hetzelfde voorbeeld als hierboven, maar nu herschreven met de translate() functie:

Wat gebeurt er hier? In het kort: de translate() functie verandert het nulpunt van de coördinaten. Voor iedere functie die iets tekent na de translate() functie, is de positie van 0, 0 op de coördinaten die je als argumenten in de translate() functie hebt ingevoerd.

Het grote voordeel van het translate() commando is dat je eerst een aantal vormen kan tekenen zonder je druk te maken over de uiteindelijke positie die ze in moeten nemen. Je kan dat stukje code heel simpel kopiëren naar een andere sketch zolang je de translate() gebruikt om de positie te veranderen hoef je verder niks aan de code te veranderen.

Hier een versie van de bovenstaande sketch waarbij de smiley de muiscursor volgt. Om dit te laten werken is het niet nodig om in iedere vorm (hoofd, twee ogen en een mond) daar de mouseX en mouseY functie toe te voegen in ieder vorm; je hoeft alleen de translate() functie te veranderen.

Translate telt op!

Stel nu dat je twee smileys wilt tekenen. Eentje die de muis volgt en eentje die stil blijft staan in de linkerbovenhoek. Je eerste poging zal er misschien zo uitzien:

Hm dat gaat niet goed. Ze volgen allebei de muis. Hoezo?

Dit is het probleem: transformaties tellen op. De eerste aanroep voor translate()in bovenstaand voorbeeld verplaatst het nulpunt naar de positie van de muis. De tweede aanroep herstelt het nulpunt niet naar 50, 50, maar verplaatst het nulpunt naar 50, 50 relatief van het bestaande nulpunt, dat is hier dus de muispositie!

Het Pushen en poppen van de matrix

Ondanks dat ik het bovenstaande voorbeeld expres heb laten mislukken is dit gedrag eigenlijk heel fijn. (Om te zien waarom probeer eens te bedenken hoe je het gedrag van de bovenstaande sketch zou moeten uitvoeren als je dat zou willen. Met andere woorden probeer te bedenken hoe je anders twee gezichten zou willen tekenen die de muis volgen waarbij eentje net naast de andere staat als de translate() functie het nulpunt elke keer helemaal opnieuw zou instellen.)

Maar het zou wel handig zijn om transformaties te kunnen isoleren, zodat een deel van de code zijn eigen nulpunt heeft en een ander deel van de code een ander nulpunt ergens anders. Je doet dit door gebruik te maken van twee verschillende functies: de push() en pop(). Let wel, dit zijn andere push() en pop() functies dan die voor een array. Bij een array gebruiken we ze om elementen toe te voegen en te verwijderen uit een lijst. Maar nu gebruiken we push() om de huidige staat van alle transformaties in ons programma tijdelijk te bewaren, en pop() om weer terug te keren naar die bewaarde staat.

De push() functie zegt eigenlijk, “He, p5.js, alle translate() functies tot hier moet je even onthouden, zodat ik straks vanaf dat punt weer verder kan." De pop() functie zegt, “He, p5.js. Weet je nog dat ik je vroeg om die translates te onthouden? Ga daar maar weer naar terug."

Om dit te demonstreren is hier een voorbeeld van een stukje hierboven, maar nu herschreven zodat het doet wat we eigenlijk wilden: de ene smiley volgt de muis en de andere blijft in de linkerbovenhoek stil staan:

Schalen en roteren

De functies scale() en rotate() zijn net als translate() transformaties, en die tellen dus ook op. Bovendien wordt altijd geschaald en geroteerd om het nulpunt, dus scale() en rotate() gebruik je altijd in combinatie met translate(). En dus ook samen met push() en pop(). Om dat te demonstreren gaan we proberen een aantal rechthoeken onder elkaar te tekenen met dezelfde draaiïng. Als we dat zonder translate proberen:

Voor het gemak heb ik de rectMode op CENTER ingesteld en de angleMode op DEGREES (deze staat standaard ingesteld op RADIANS, maar de meeste mensen denken in graden). Maar wat gebeurt hier nu? Als eerste zien we dat alle rechthoeken geroteerd worden rond het nulpunt van de sketch ipv. rond hun eigen middelpunt. En ten tweede roteren ze telkens 5 graden meer omdat de rotate() functie optelt. Laten we eerst eens proberen om translate toe te passen, zodat ze rond hun eigen middelpunt roteren:

Mja, de rechthoeken roteren nu wel rond hun eigen middelpunt, maar translate() en rotate() tellen nog steeds op waardoor ze uit beeld verdwijnen. Dat gaan we oplossen met push() en pop():

Op deze manier kunnen we nu ook een raster van vormen tekenen met elk hun eigen rotatie: