Transformaties en functies

Deze site is grotendeels een vertaling van een tutorial van Allison Parrish en is vertaald en bewerkt door Marijn van der Meer voor het gebruik bij Informatica lessen op het IJburg College

In dit hoofdstuk ga je leren hoe je transformaties kan gebruiken. Dit is een handige en elegante manier om te bepalen waar en hoe vormen worden getekend op het scherm. Ook zal je de basis gaan leren van het maken van functies. Deze gebruik je om je code leesbaarder, vaak korter te maken en om de code mooi in te delen.

Translate

In een eerder voorbeeld maakten we een "offset" variabele om de plek waar een vorm werd getekend op het scherm te veranderen. Hier is bijvoorbeeld een sketch dat een soort van 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 lijkt een hoop herhaling in de code te staan: de namen xoffset en yoffset worden een aantal keer opgeschreven.

Je zal nu wel doorhebben dat veel programmeurs een hekel hebben aan het continue opnieuw hetzelfde te typen. In dit geval, en ik veel andere gevallen, hebben programmeurs een manier bedacht om deze herhaling te "omzeilen" door de translate() functie te bedenken.

Voordat ik precies ga 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 (twee ogen en een mond) om daar de mouseX en mouseY functie toe te voegen in ieder vorm; nee, het volstaat om de translate() functie te veranderen.

Translate telt op!

Met dit in gedachten denk je misschien: "OK ik wil twee smileys! Een die de muis volgt en een die stil blijft staan in de linkerbovenhoek. Dat ga ik doen!" Je eerste poging zal er misschien zo uitzien:

Ho wacht? Dit is bizar. Ze volgen allebei de muis. Hoezo?

Dit is het probleem: transformaties tellen op. De eerste aanroep voor translate() in het bovenstaande 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 eens 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 continue helemaal opnieuw zou zetten.)

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().

De push() functie zegt, “He, p5.js, alle translate() functies vanaf hier moet je onthouden! Waarom, omdat ik ze later weer ongedaan wil maken." De pop() functie zegt, “He, p5.js. Herinner je nog dat ik je vroeg dat je al die translates moest onthouden? Vergeet ze maar, dank en doei."

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

Een eigen functie

De meest luie leerlingen zullen opgemerkt hebben dat in de bovenstaande code weer een nutteloze herhaling staat: de code om het gezicht te tekenen staat er TWEE keer in! Zou het niet fijn zijn om deze herhaling er ook nog eens uit te halen? Bijvoorbeeld door de code die de smiley tekent ergens onder een naam op te slaan en dan Javascript te vragen om die code uit te voeren als we ergens weer die naam opschrijven in plaats van de code te kopieren? Eigenlijk net als met variabelen. Toch?

Yes! Javascript heeft uiteraard een manier om dit te doen: je kan een functie definieren. Een functie is niets minder dan een verzameling van opdrachten die je een naam geeft, zodat je deze opdrachten niet iedere keer opnieuw hoeft te typen.

We hebben tot nu toe continue met functies gewerkt, maar tot nu toe alleen met functies die de makers van p5.js (of Javascript) voor ons gemaakt hebben. Maar het is heel makkelijk om ook zelf functies te maken. De syntax ziet er zo uit:

function naam_van_functie() {
  statements
}

… waar je naam_van_functie moet veranderen met de naam die je aan de functie wilt geven, en statements met de code die je in de functie wilt plaatsen: dit kunnen ook weer aanroepen naar (andere) functies zijn, for loops, if statements, wat je maar wilt.

LET OP: Namen van functies moeten dezelfde regels volgen als de variabele namen. (Eigenlijk zijn functies ook een vorm van een variabele.)

Wanneer je deze code in je sketch stopt heet dat een functie definitie. Je kan functie overal in je broncode definieren. Maar leer wel een stijl aan met een logische volgorde. Zodat je code niet van de hak op de tak springt. Dat is heel lastig lezen voor iemand anders. Ik zou functies in p5.js zo veel mogelijk onderaan je code zetten, na setup() en draw(). Om vervolgens een functie te gebruiken moet je de functie "aanroepen" ("run" of "call" in het Engels), dus Javascript vertellen dat het de code in de functie moet gaan uitvoeren. Dat doe je door de naam van de functie te schrijven meteen gevolgt door haakjes (())

Hier is een versie van een functie van het bovenstaande voorbeeld waar nu de functie maakSmiley() wordt gedefinieerd met de code om een smiley te tekenen en daarboven in de draw() wordt de functie aangeroepen:

Maar dat is nog niet alles! Nu we onze smiley-teken-functie hebben, kunnen we deze functie gebruiken waar en wanneer we maar willen. Hier zijn twintig smileys over elkaar, gewoon omdat dat kan:

Het gebruik van functies zorgt er niet alleen voor dat er veel minder herhaling in je broncode voor komt, maar ze geven ook een mogelijkheid om je code in logische stukken in te delen. Het idee is dat het makkelijker is om een for loop te begrijpen waar een functie maakSmiley() in staat, zoals het voorbeeld hierboven: je ziet in een oog opslag wat de code in loop doet. En het is ook nog makkelijker om code van de ene sketch te kopieren naar de andere sketch: dus recyclen. Je hoeft nu een functie alleen te copy-pasten (in plaats van het zoeken naar de relevante delen in het draw() blok.

Schalen

De translate() functie is niet de enige transformatie die je kan gebruiken bij je tekencommando's. Een andere transformatie is scale(), deze functie schaalt hetgeen je tekent, dus verandert de grootte. Waar translate() bepaalt waar het nulpunt van de coördinaten zich bevinden, zo bepaalt scale() de afstand tot het nulpunt. Dus het schaalt vanaf het nulpunt. Misschien makkelijker om even te zien wat er gebeurt.

We gaan verder met het vorige voorbeeld, we roepen weer de maakSmiley() functie op en nu wel 3 keer: een keer op een halve schaal, een keer op normale schaal en een keer op dubbele schaal.

En nog een voorbeeld waar twee smileys worden getekend die de muis volgen en geschaald worden met behulp van de sinus en het framenummer in de scale() functie, wat zie je nu gebeuren:

Nu is het ook heel makkelijk om wel 50 smileys te tekenen, met allemaal verschillende grootte en kleuren:

De scale() functie kan ook in de X- en Y-dimensie apart schalen; om dit te bereiken moet je een tweede parameter toevoegen in de functie. De eerste parameter bepaalt de X-schaal en de tweede de Y-schaal. Nu lijkt de tekening 'uitgetrokken' te worden in een bepaalde richting. Hier wederom het smiley voorbeeld, maar nu random geschaald in beide richtingen:

Rotation

De laatste transformatie die we behandelen is rotatie. De rotate() functie verandert de orientatie van het coördinatensysteem. Dit is ook het makkelijkst te begrijpen met een voorbeeld:

Zoals je ziet zijn in iedere herhaling van de for loop, de parameters van de line() precies hetzelfde. Het enige verschil is de rotatie. MetThe only difference is the rotation. Bij een rotatie van nul wordt een lijn getekend van het nulpunt (0, 0) naar 350 pixels links. Door het coördinatensysteem te roteren blijft het nulpunt gelijk, maar draait de locatie van (350, 0) met de klok mee.

Het rotate() commando leest de waarde van de parameter in radialen en niet in graden. Radialen werken met PI. De waarde van PI (~3.14) is een halve cirkel (180 graden), en de waarde van twee keer PI (~6.28) is een heel rondje (360 graden).

Hier een voorbeeld dat de maakSmiley() functie aanroept en vervolgens het resultaat laat ronddraaien met behulp van de muispositie:

Rotatie en translate

Het is een beetje een anticlimax om de smiley alleen helemaal in de bovenhoek van het scherm te tekenen. Laten we de smiley tekenen in het midden van het scherm te tekenen met behulp van de translate() functie. Hier is de eerste poging:

… op zich prima, maar niet helemaal wat we bedoelde. Wat er hier gebeurt is dat we eerst de rotate() en dan de translate() aanroepen, wat betekent dat eerst het coördinatensysteem gedraaid wordt en dan vindt de translatie plaats binnen de gedraaide coördinaten. Dus de aanroep voor de translate() gaat niet 150 pixels naar rechts en dan 150 pixels naar beneden... Nee; het beweegt 150 pixels langs de orientatie van het coördinatensysteem, en dan 150 pixels haaks van dat punt.

Als we eerst de translatie toepassen (dus dat gebeurt in de standaard orientatie) en pas daarna de rotatie ziet het resultaat er iets meer uit als wat we hadden verwacht:

Deze twee voorbeelden laten zien dat, omdat transformaties bij elkaar optellen, is de volgorde van de transformaties van belang. Iedere transformatie heeft op een of andere manier effect op het coördinatensysteem en de daarop volgende transformaties veranderen de al gewijzigde coördinaten nog een keer.

Al deze veranderingen zijn moeilijk te laten zien en vervolgens uit te leggen. Je moet het vooral uitproberen om ze te leren kennen en te leren te begrijpen. Een vuistregel die meestal werkt voor vrij simpele gevallen: translate eerst (dus verplaats het nulpunt naar de gewenste plek), roteer vervolgens (om de orientatie van de vorm te veranderen en pas als laatste pas de schaal toe (om de vorm te vergroten of te verkleinen).

Ontwerpen rond het nulpunt

Een probleem dat veel beginnende programmeurs tegenkomen is dat ze al hun code hebben geschreven ten opzichte van het het nulpunt (0, 0), en alle vormen zijn dus getekend met het nulpunt in de linkerbovenhoek. Deze programmeurs zijn vervolgens verbaasd als sketches zoals hierboven zich ineens raar gaan gedragen als ze de vormen willen transformeren: intuitief denken we "roteer deze vorm" en dan gaan we ervan uit dat de vorm rond zijn middelpunt draait. Maar de vorm draait dus rond het nulpunt (0,0) en deze ligt normaal gesproken in de linkerbovenhoek, en zal na een rotate() de vorm rond de linkerbovenhoek gaan draaien.

In het geval van de bovenstaande sketch is het het makkelijkst om het probleem op te lossen door simpelweg de functie te herschrijven zodat het middelpunt van de smiley ook echt op de coördinaat (0, 0) zit. Om dit voor elkaar te krijgen moeten we even iets raars doen, we moeten negatieve getallen gebruiken voor een aantal coördinaten van de vormen in de sketch. Hier is een nieuwe versie van de sketch waar deze verandering is doorgevoerd:

Het middelpunt van een rechthoek

Je zal waarschijnlijk al gemerkt hebben dat een heel aantal p5.js teken-functies geörienteerd zijn rond de linkerbovenhoek van de vorm. De rect() functie bijvoorbeeld, gebruikt de eerste twee parameters als de X- en Y-positie van de linkerbovenhoek. Dit maakt het draaien van een rechthoek rond zijn middelpunt nogal lastig. Gelukkig is er nog een functie beschikbaar, de rectMode(), waarin je kan bepalen of je de eerste twee parameters van de rect() wil gebruiken voor het middelpunt van de rechthoek in plaats van de linkerbovenhoek. Hier een voorbeeld dat het verschil laat zien:

Zoals je kan zien zorgt het aanroepen van rectMode(CENTER) er voor dat p5.js de rechthoek tekent om het middelpunt van de rechthoek, en wordt de rechthoek met rectMode(CORNER) (het standaard gedrag) door p5.js met de gegeven coördinaten in de linkerbovenhoek getekend.

Een griezelige ontknoping

In dit voorbeeld tekenen we heel veel smileys met willekeurige posities, groottes en rotaties.