Animatie

In dit hoofdstuk leer je de basisprincipes om je vormen te animeren.


Frames

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

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

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 bepaalt hoe vaak per seconde de code in draw() uitgevoerd wordt.

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 ieder frame verandert: als de draw() voor het eerst wordt uitgevoerd is de frameCount 0; de tweede keer dat de code in draw() wordt uitgevoerd is de frameCount 1, enzovoort. De onderstaande schets print de huidige waarde van de frameCount variabele:

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

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

Als je deze sketch in de webeditor laat uitvoeren zie je dat de sketch razendsnel optelt, ca. 30 per seconde, en de getallen in het consolevenster print. (Natuurlijk kun je het getal in de frameRate() aanpassen om het getal sneller of langzamer 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 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 éénmaal 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 ander animatie effect. Je kan met beide situaties hele mooie effecten bereiken!

Herhalende bewegingen

Op deze manier kun je vrij makkelijk animaties maken, maar het probleem met frameCount is dat deze lineair toeneemt, van laag naar hoog, en dat is geen herhalende beweging, je kunt alleen iets maken dat steeds groter wordt en/of in één bepaalde richting beweegt. Daarom gaan we nu gebruik maken van een wiskundige berekening die in de wiskunde niet veel wordt gebruikt, maar in de informatica wel. Deze kan voor een herhalende beweging zorgen, een oscillatie, iets dat heen en weer gaat tussen twee momenten, alleen gebruik makend 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. Typ 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 heel makkelijk een herhalend tellertje 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 toenemend 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 die 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. We hebben 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.

We hoeven niet in detail te bespreken 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 0 is 0, de sinus van pi/2 (~1.57) is 1; de sinus van pi is weer nul; de sinus van 3/2pi (~4.71) is -1; en de sinus van 2*pi (~6.28) is opnieuw 0. Hogere waarden in de sin() functie zullen altijd op waarden tussen -1 and 1 uitkomen.

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:

Animatie met variabelen

In vorige voorbeelden hadden de variabelen die we gebruikten maar één waarde: de waarde die we voor ze instelden (initialiseerden) 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 geanimeerde sketches te maken, maar wel op een iets mooiere en zorgvuldigere manier dan met alleen de frameCount variabele.

Hier een sketch die twee variabelen declareert (aanmaakt) en initialiseert (instelt), xpos en ypos. De waarden van deze variabelen veranderen in de draw() code.

Zoals je kan zien wordt, bij ieder frame, 1 opgeteld bij xpos en 1 afgetrokken bij ypos.

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:

xpos = xpos + 1;

kan ook worden geschreven als:

xpos += 1;

en het kan zelfs nog korter, zoals we hebben gezien in de for loop:

xpos++;

In de eerste twee expressies kun je overigens ook andere waarden dan 1 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:

Animatie met objecten

Als we meerdere vormen willen animeren met variatie is het handiger om te gaan werken met objecten, zoals we gezien hebben in het vorige hoofdstuk. Ieder object heeft dan zijn eigen variabelen die je kunt aanpassen, telkens op een iets andere manier om variatie aan te brengen. Eerst gaan we het vorige voorbeeld herschrijven naar objectgeoriënteerd. Dan ziet dezelfde code er zo uit:

Nu we een object MovingCircle hebben, wordt het veel makkelijker om een heleboel cirkels tegelijkertijd te animeren met wat variatie. In onderstaand voorbeeld wordt een grid van cirkels aangemaakt in de setup functie. Ieder object krijgt ook een eigen index, die weer gebruikt wordt om de positie van elke cirkel net iets anders te animeren.

Om de animaties wat onvoorspelbaarder te maken kunnen we de random() functie gebruiken.

In bovenstaand voorbeeld gebeurt nog iets anders interessants, er wordt een grid van vormen gemaakt met maar één loop. Eerder hadden we daar nog twee geneste loops voor nodig, hoe kan dat? Dat kan door gebruik te maken van de modulo operator. De x-positie wordt bepaald door 56 + (i % 10) * 32, dus na elke tien iteraties is i % 10 weer gelijk aan nul en begint de x-positie weer aan de linkerkant. En de y-positie wordt bepaald door 56 + floor(i / 10) * 32, elke tien iteraties is de uitkomst van floor(i / 10) één hoger waardoor de y-positie opgehoogd wordt. Dus op hetzelfde moment dat de x-positie weer aan de linkerkant uitkomt, wordt de y-positie opgehoogd zodat een nieuw rijtje van 10 vormen getekend wordt onder het vorige rijtje.

Omdat we nu met objecten werken kunnen we ook makkelijker meer eigenschappen van de vormen animeren. In het voorbeeld hieronder zijn naast de positie ook de kleur en de grootte van de cirkels geanimeerd. De variabelen van de objecten worden ieder frame opgehoogd of verlaagd en met behulp van sin() gemanipuleerd om een vloeiende animatie te creëren. Verder is een blendMode ingesteld om de kleuren van de cirkels te mengen.

Met een aantal kleine aanpassingen aan bovenstaand voorbeeld kunnen we een heel ander resultaat krijgen. Wat hebben we precies veranderd?

  • Er worden minder cirkels aangemaakt met meer variatie in diameter en ze worden allemaal op dezelfde positie geplaats ipv. in een grid.
  • Er wordt een andere blendMode ingesteld.
  • Er wordt fill() gebruikt ipv. stroke().
  • De kleuren wordt iets anders ingesteld en er is transparantie toegevoegd.
  • De snelheid van sommige animaties is iets aangepast.

En dan krijgen we zoiets: