Veranderingen in tijd

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 zal je leren hoe je een simpele p5.js sketch moet maken die animeren. Dus waar beweging in tijd in zit. Voor we daar aan beginnen zal ik eerst even uitleggen hoe tijd werkt in de programmeertaal Processing en p5.js. Want dit is anders dan in veel andere programmeertalen.

Setup en draw

Alle sketches die we tot nu toe hebben gemaakt bestonden uit twee delen, zoals ze misschien al is opgevallen: er ging code in de function setup() { ... } en er ging code in de function draw() { ... }. Ik heb nog niet precies uitgelegd wat deze twee delen precies doen, omdat het misschien een beetje veel was om het allemaal vanaf het begin allemaal in een keer te begrijpen. Ik hoop dat je er nu wel aan toe bent.

Maar om een lang verhaal kort te maken, wat er gebeurt is: alle code die je in setup() staat wordt maar een keer uitgevoerd, aan het begin van het programma. Alle code die je in draw() stopt wordt steeds opnieuw uitgevoerd, normaal gesproken heel veel keer per seconde.

“Maar wacht?” zal je denken. “Wat bedoel je? Het ziet er helemaal niet uit dat mijn sketch steeds opnieuw wordt getekend. Het lijkt gewoon een stilstaande tekening met cirkels of rechthoeken." En dat klopt! Alle sketches die je tot nu toe hebt gemaakt en die ik tot nu toe heb laten zien tekenen achter elkaar precies hetzelfde op dezelfde plek. Dus ze lijken helemaal niet opnieuw getekend te worden.

Maar neem het maar van mij aan, de code in draw() wordt echt steeds opnieuw uitgevoerd. Je kan dit testen door de volgende code in de webeditor te schrijven:

function setup() {
    createCanvas(400, 400);
}

function draw() {
    console.log("markeer"); 
}

Let op het console (of debug) venster (onder waar je de code typt). En je ziet het woord "markeer" vele keren en heel snel verschijnen.

Wat gebeurt er nu eigenlijk? Even als herinnering, de console.log() functie is een functie die er alleen maar is om een bepaalde waarde in het console te printen. Alles in de draw() wordt heel veel keer per seconde uitgevoerd, zoals ik hierboven al zei. Dus het resultaat van de code hierboven is een eindeloze herhaling van precies dezelfde console.log() functie aanroep, steeds maar opnieuw.

Frames

In computeranimatie heet het een keer draaien (en dus iets op het scherm tekenen) van de draw() functie, een "frame" (net als bij het maken van films). Processing en p5.js komen met twee handige functies om te werken met frames: de functie frameRate() en de variabele frameCount.

Met de frameRate() functie kan je vast stellen hoe vaak de code in de draw() per seconde uitgevoerd zal worden. Voer deze sketch maar eens uit:

function setup() {
    createCanvas(400, 400);
    frameRate(1);
}

function draw() {
    console.log("markeer"); 
}

De aanroep voor frameRate() moet in het setup() gedeelte van je code, niet in de draw(), omdat je de frame rate maar een keer hoeft vast te stellen. Het getal dat je tussen de haakjes zet bepaald hoe vaak per seconde de code in draw() uitgevoerd wordt. Je zal zien dat de code hierboven nog steeds het woord markeer print in het consolevenster, maar nu veel langzamer.

DeframeCount variabele is ingebouwd in p5.js. Je hoeft hem niet zelf te definieren, dat doet p5.js voordat de schets begint. De frameCount variabele is bijzonder omdat deze variabele iedere frame verandert: als de draw() voor het eerst wordt uitgevoerd is de frameCount gelijk aan nul; de tweede keer is dat de code in draw() wordt uitgevoerd is de frameCount gelijk aan één, enzovoort. De onderstaande schets is precies als die hierboven, maar nu print het niet het woord "markeer", maar print het de huidige waarde van de frameCount variabele:

function setup() {
    createCanvas(400, 400);
    frameRate(1);
}

function draw() {
    console.log(frameCount); 
}

Als je deze sketch in de webeditor laat uitvoeren zie je dat de sketch langzaam optelt, een getal per seconde, en de getallen in het consolevenster print.

(Natuurlijk kan je het getal in de frameRate() aanpassen om het getal sneller te laten optellen.)

Als je benieuwd bent naar het aantal frames per seconde dat de computer standaard uitvoert (en wat je browser haalt) kan je deze code eens in de webeditor zetten en eens laten uitvoeren:
function setup() {
    createCanvas(400, 400);

}

function draw() {
    console.log(frameRate()); 
}

Animeren met frameCount

De frameCount variabele is, net als iedere andere variabele, iets wat je kan gebruiken in expressies en functies. En omdat frameCount iedere frame wijzigt kan je deze variabele gebruiken om een simpele animatie te maken. Hier is een voorbeeld:

Je zou een kleine ellips moeten zien die links van het scherm begint en dan langzaam naar rechts beweegt en langzaam groter wordt.

OEFENING: Gebruik de frameCount variabele eens in een andere functie, bijvoorbeeld fill() en/of gebruik de frameCount variabele als onderdeel van een expressie. Kijk eens wat er gebeurt!

De inhoud behouden van de vorige frame

Hier is nog een versie van de bovenstaande sketch met een klein, maar belangrijk verschil. Kan je zien wat er gebeurt en waarom?

Dat ziet er bijzonder uit! Het lijkt alsof iedere frame over de vorige frames wordt getekend. Hoe komt dat?

Het is belangrijk om te weten dat p5.js niet automatisch het scherm na iedere frame weer leeg maakt. Normaal blijft wat er na het vorige frame op het canvas staat gewoon staan in het volgende frame.

Dus het verschil tussen de twee sketches zit in het moment waarop we de background() functie aanroepen. In het voorbeeld hierboven is dat ergens in de setup() code, wat betekent dat de achtergrond maar eenmaal wordt getekend. In de draw() code, wordt de background() code niet meer gebruikt, dus krijgt de achtergrond niet iedere frame een "reset" tot een egale kleur

Als je dus de achtergrond niet ieder frame opnieuw laat tekenen is het effect dat het lijkt alsof de sketch iedere keer over zichzelf wordt getekend, steeds opnieuw. En als je de background() in de draw() zet wordt deze ieder frame opnieuw getekend en geeft dit een veel beter animatie effect. Je kan met beide situaties hele mooie effecten bereiken!

Herhalende bewegingen

Het is echt wel cool om zo snel animaties te kunnen maken, maar het probleem met frameCount is dat het lineair toeneemt, van laag naar hoog, en dat is geen herhalende beweging, je kan alleen iets maken dat steeds groter wordt en/of in een richting beweegt. Nu zullen we gebruik gaan maken van een wiskundige berekening die in de wiskunde niet veel wordt gebruikt. Deze kan voor een herhalende beweging zorgen, een oscillatie, zodat is heen en weer gaat tussen twee momenten, met alleen het gebruik maken van de lineaire frameCount waarde als basis van de verandering. Hoe zit dat?

Modulo

In de wiskunde is er een operator die “modulo” heet. Deze operator geeft je het restgetal van het resultaat van het delen van twee hele getallen. Het antwoord op 11 modulo 5 is bijvoorbeeld het restgetal van het delen van 11 door 5, dus dat is 1 (twee keer vijf is tien en dan hou je 1 over om tot elf te komen).

In Javascript is het teken, de operator, voor modulo: %. Deze kan je net zo gebruiken als alle andere operatoren. Type dit voorbeeld maar eens in de p5.js webeditor en kijk wat er gebeurt:

console.log(11 % 5)

Je zou nu 1 in het consolevenster moeten zien. Probeer maar eens verschillende getallen en zie wat er gebeurt!

Wat kunnen we nu met de modulo? We kunnen hiermee een hele makkelijke herhalende teller maken. Omdat de modulo zorgt voor het volgende patroon in uitkomsten:

0 % 5 => 0 (nul gedeeld door vijf is nul, met een restgetal van nul)
1 % 5 => 1 (een gedeeld door vijf is nul, met een restgetal van een) 
2 % 5 => 2
3 % 5 => 3
4 % 5 => 4
5 % 5 => 0 (vijf gedeeld door vijf is een, met een restgetal van nul) 
6 % 5 => 1 (zes gedeeld door vijf is een, met een restgetal van een) 
7 % 5 => 2 (enzovoort)
...

In andere woorden, je hebt een constant toenement getal aan de linkerkant van de modulo operator (de frameCount()) en een vast getal aan de rechterkant en het evalueren van de modulo expressie zal ons een repeterende teller geven van nul tot het getal dat we aan de rechterkant hebben geplaatst.

We kunnen dit principe van de modulo gebruiken om "loops" te creeeren die zich iedere n frames zal herhalen, waar n een getal is dat we zelf kiezen. Hier is bijvoorbeeld een sketch dat een ellips tekent die gedurende dertig frames groeit en zich dan reset naar zijn orginele grootte:

En hier is een sketch die een cirkel tekent die beweegt van links naar rechts en zich dan "reset" naar zijn originele plek links. Ik heb er een tweede beweging ingestopt waarbij de lijndikte op en neer gaat:

OEFENING: Maak een sketch met meerdere bewegingen die verschillende modulo's gebruikt, dus door verschillende getallen aan de rechterzijde van het % teken te zetten.

Sinus

De modulo techniek is leuk (en de modulo kan je met programmeren vaker gebruiken), maar je kan er alleen makkelijk loops mee maken die lineair groeien en ineens weer opnieuw beginnen. Een andere makkelijke wiskundige tool die we kunnen gebruiken voor animaties die lijken te groeien en te krimpen is de sinus. Als we de sinus berekenen van de frameCount variabele.

Hier behoeven we niet in detail te gaan over wat de sinus precies is of hoe het werkt. Dat krijg je wel bij wiskunde. Het belangrijkste is dat we de sinus-functie kunnen gebruiken om mooie vloeiende regelmatige bewegingen te krijgen. In p5.js is de sinus-functie te gebruiken met de sin() functie. Anders dan de meeste functies die we tot nu toe hebben gebruikt, wordt de sin() niet gebruikt om iets op het scherm te krijgen. We gebruiken het om van een waarde (de frameCount) een andere waarde te krijgen na een wiskundige berekening, die we vervolgens kunnen gebruiken.

Desin() functie heeft maar een parameter en berekent deze tot een waarde tussen -1 en 1. De sin() functie evalueert (berekent) verschillende waarden zo:

sin(0) = 0
sin(0.39) = 0.38
sin(0.78) = 0.70
sin(1.17) = 0.92
sin(1.57) = 1
sin(1.96) = 0.92
sin(2.35) = 0.70
sin(2.74) = 0.38
sin(3.14) = 0
sin(3.53) = -0.38
sin(3.92) = -0.70
sin(4.31) = -0.92
sin(4.71) = -1
sin(5.10) = -0.92
sin(5.49) = -0.70
sin(5.89) = -0.38
sin(6.28) = 0

In andere woorden, de sinus van nul is nul, de sinus van pi/2 (~1.57) is een; de sinus van pi is weer nul; de sinus van 3/2pi (~4.71) is min een; en de sinus van 2*pi (~6.28) is opnieuw nul. Hogere waarden in de sin() functie zullen altijd tot waarden tussen -1 and 1 komen.

Dus wat gebeurt er als we de frameCount variabele in de sin() stoppen? Dan krijg je zoiets als dit:

… nou dat is ook niet indrukwekkend. De cirkel.... wiebelt alleen een beetje? Dat is omdat de sin() functie altijd een waarde retourneert tussen -1 to 1. Als we dit willen veranderen tot iets echt zichtbaars, moeten we het resultaat nog vermenigvuldigen zodat de waarde groter wordt dan 1 pixel beweging:

Ok, dat is beter! Maar het is een beetje, uhm, onrustig. Dat is omdat de sin() functie de hele cyclus van 1 naar -1 afgaat in zes tellen en frameCount gaat heel snel in stappen van zes (in de default frame rate wel 10 keer per seconde). Om de bewegingen langzamer te maken moeten we de frameCount delen door een andere waarde. Laten we het eens tien keer zo langzaam gaan door te delen door tien:

Dus meestal als je de sin() functie zal gebruiken, zal je dat in combinatie met twee andere waarden doen, een die de amplitude van de beweging bepaalt (dat wil zeggen, hoe groot de beweging wordt) en de frequentie van de beweging (dus hoe snel de beweging is). En het zal er zo uit zien:

basis + (sin(frameCount / freq) * ampl)

… waar basis, freq en ampl allemaal getallen zijn. Als je freq groter maakt wordt de beweging langzamer; als je ampl groter maakt wordt de beweging groter en de basis waarde is het centrumpunt van de beweging, waar gaat de beweging "omheen".

Herhaling en variatie

Hier is een sketch dat een aantal cirkels tekent die bewegen over het scherm en de sin() functie gebruiken voor het bepalen van de horizontale posities:

En hier is een wat bijzonderdere versie, die de loop variabele i gebruikt om subtiele variaties in iedere cirkel te krijgen:

Het ingewikkelde gedeelte van deze code zit in deze expressie:

200+(sin(frameCount/(i+10))*(i+20))

Om deze expressie te begrijpen, probeer hem eens in het Nederlands te vertalen en bereken dan zelf eens de waarde voor verschillende waarden van frameCount en i.

Ongeveer hetzelfde werkt de cosinus, de cos(). En vooral samen met de sinus kan je daar hele mooie bewegingen mee simuleren. Als je bij bijvoorbeeld een ellips, de x-positie door de sinus laat bepalen en de y-positie door de cosinus, krijg je een mooie circulaire beweging:

Variabelen laten veranderen in tijd

In vorige voorbeelden hadden de variabelen die we gebruikten maar een waarde: de waarde die we voor ze instelden (initialiseerde) aan het begin van de code, voor setup(). Maar de waarde van variabelen kan veranderen tijdens het programma: als we een variabele gemaakt hebben kunnen we Javascript de opdracht geven de huidige waarde van de variabelen te veranderen in iets anders (een andere waarde in het laatje stoppen). Je kan deze eigenschap van Javascript gebruiken om sketches te maken die in tijd veranderen, maar wel op een iets mooiere en zorgvuldigere manier dan met alleen de frameCount variabele.

Als je je goed kan herinneren, de syntax voor het vaststellen (declare) van een variabele zag er zo uit:

var x = expr;

… waarbij x de naam van de variabele is, en expr een expressie is waarvan de waarde gekoppeld moet worden aan x. Je kan de waarde van een variabele veranderen door de volgende syntax te gebruiken:

x = expr;

… waarbij, opnieuw, x de naam van de variabele is, en expr een expressie is die een waarde geeft aan x.

Hier een sketch die twee variabelen declareert (aanmaakt), foo en bar (gebruikelijke variabele-namen bij voorbeelden in de programmeerwereld), en de waarden van deze variabelen veranderen in de draw() code.

Zoals je kan zien wordt, bij ieder frame, 1 opgeteld bij foo en 1 afgetrokken bij bar.

De expressie van de variabele neemt de huidige waarde van de variabele en stopt dan de oude waarde plus of min 1 weer terug in de variabele. Dit gebeurt bij het programmeren zo vaak dat er een shortcut voor is. Deze expressie:

foo = foo + 1;

kan ook worden geschreven als:

foo += 1;

Je kan overigens ook andere waarden dan 1 in deze expressie gebruiken.

Hier is een voorbeeld hoe we een bovenstaande code met de sin() functie veel logischer en leesbaarder kunnen schrijven. Door de waarde van de variabele maar een klein beetje te laten toenemen bij iedere frame in plaats van de frameCount te moeten delen:

Laten we ook een y-positie toevoegen:

OEFENING: Wat zou er gebeuren als het getal waarmee je de xpos en ypos op iedere frame laat toenemen zelf gedurende de tijd laat toenemen, in plaats van een vaste waarde? Waar was deze ook alweer voor?

Willekeurige veranderingen

Ik vind dat kunst, gegenereerd door de computer, alleen leuk wordt als het lijkt dat het niet allemaal vastgesteld is wat er gaat gebeuren. Dus laten we wat onvoorspelbaarheid in onze sketches stoppen.

p5.js (en vele programmeertalen) heeft een functie met de naam random() dat een willekeurig (random) getal genereerd tussen nul en een. Bekijk de referentiepagina voor meer informatie en alternatieve manieren om de functie aan te roepen. Het getal dat door een random functie wordt gegenereerd zal iedere keer dat de sketch wordt gebruikt anders zijn. En ook als deze in de draw() staat zal het getal iedere frame anders zijn.

Je kan de random() functie gebruiken om vormen op willekeurige plekken in de sketch te laten tekenen:

Je kan de random() functie ook met twee parameters gebruiken. In dat geval zal de functie een willekeurig getal geven dat tussen de waarde van de eerste en tweede parameter zit. Om een vorm te maken die "ronddoolt" over het scherm: