Media

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 wil ik uitleggen hoe je externe media (plaatjes, geluid e.d.) kan inladen. Het enige probleem is dat de widget geen externe data wil inladen uit veiligheidsoverwegingen. Dus worden de voorbeelden in de bekende P5-webeditor gegeven.

Foto's

Het is makkelijk om een beeld in te laden en weer te geven in p5.js. Voor het volgende voorbeeld moet je een afbeelding vinden (of maken) in een PNG of JPG formaat. (Als je je werk uiteindelijk online wilt delen moet je de rechten van je afbeelding wel op orde hebben, mag je het wel gebruiken? Creative Commons Search is een goede plek om afbeeldingen te vinden die je mag gebruiken.)

Om je afbeelding te kunnen gebruiken in je p5.js sketch moet je het eerst uploaden naar de sketch folder. Het makkelijkst om dit te doen in de p5.js webeditor: kies in menu het View > Show Sketch Folder. Nu kan je Finder gebruiken om je beeld te zoeken en naar deze folder te kopieren.

Onthoudt de naam van de file: je hebt hem straks nodig om hem in je sketch te laden

In het volgende voorbeeld, gebruik ik een afbeelding met de naam kitty.jpg, die je hier kan downloaden. (Bron, gebruikt onder de CC BY 2.0 licentie.)

Werken met plaatjes 1

En nu hebben we een kat die de muis volgt!

Er zijn een paar nieuwe functies en concepten in dit voorbeeld waar we even op in moeten gaan. De eerste is de preload() functie. Dit is een functie die je, net zoals setup() and draw(), definieert in je sketch en die automatisch wordt aangeroepen door p5.js. De code in preload() wordt uitgevoerd vóór setup() en draw() aangeroepen worden, en meestal is het om plaatjes (of andere media) die je in je sketch gaat gebruiken alvast te laden, zodat je zeker weet dat deze bestanden beschikbaar zijn als de rest van de sketch uitgevoerd wordt.

De loadImage() functie. Deze functie haalt de data uit het bestand van het plaatje en maakt die beschikbaar voor gebruik binnen de sketch. De functie zelf tekent niets op het scherm: de functie returns de image data in een Javascript object. Deze code:

kitty = loadImage("kitty.jpg");

assigns (wijst toe) de waarde van het object aan de variable kitty. (Er is niets speciaals aan de variable naam kitty hier komt het overeen met de bestandsnaam, van het plaatje, maar je zou de variabele, als je dat handig vindt, ook een heel andere naam kunnen geven.)

Om onze code netjes en overzichtelijk te houden en om er zeker van te zijn dat de kitty variabele beschikbaar is voor de draw() functie, heb ik de kitty variabele aangemaakt (*declared*)voor preload() en setup(). Het aanmaken van een variabele is een beetje vreemd, omdat ik er bij het aanmaken geen waarde aan toeken (assign). Het komt erop neer dat we Javascript vertellen: “Hey, ik ga straks gebruik maken van een variabele kitty, het is maar dat je het weet.” (Hieronder meer uitleg.)

Tenslotte, om het plaatje op het scherm te tekenen, roep ik de image() functie aan binnen de draw() functie. De image() functie heeft *tenminste* drie parameters: de eerste is het image object dat je wilt tekenen, en de tweede en derde geven de X en Y coördinaten van waar het plaatje getekend moet worden.

De image() functie kan echter meer. Je kunt een vierde en vijfde parameter opgeven om gewenste breedte en hoogte (in pixels) te specificeren. Bovendien is er een imageMode() functie, vergelijkbaar met de rectMode() functie, die je laten kiezen of je de opgegeven X en Y coördinaten wilt gebruiken als *midden* van het plaatje of als *linker bovenhoek*. Hier is een sketch die beide varianten laat zien.

De mogelijkheden … zijn eindeloos.

Preload, setup en draw

Misschien vraag je je af: waarom beginnen we het programma niet gewoon met:

var kitty = loadImage("kitty.jpg"); // werkt niet!! waarom??

Oftewel: waarom niet meteen de loadImage() functie aanroepen bij het declareren (aanmaken) van de variabele? Helder en duidelijk, maar helaas werkt het niet omdat sommige functies (waaronder de meeste load...() functies die ik behandel in deze cursus) uitsluitend binnen de preload() functie kunnen worden aangeroepen. Dit komt door de manier waarop p5.js is gebouwd, niet iets om nu diep op in te gaan, maar wel goed om te weten.

Nou ja, je kunt loadImage() op andere plekken aanroepen, en regelmatig zie je dat mensen het in setup() aanroepen als een soort van afsnijden. Maar je zult zeker nooit loadImage() in draw() tegenkomen. Hier is een eenvoudige sketch die laat zien waarom:

Afhankelijk van je computer, kunnen er verschillende dingen gebeuren als je deze sketch uitvoert. Bij mij lijkt het alsof er niets gebeurt. Dat komt doordat p5.js de draw() functie tientallen keren per seconde uitvoert, waarmee één enkele run van draw() niet veel langer duurt dan een honderdste van een seconde. Een plaatje laden, zelfs een klen plaatje, duurt veel langer, bijvoorbeeld een halve seconde. Dus wat er in deze sketch gebeurt is dat we p5.js vragen een plaatje te laden maar, lang voordat dat gelukt is we vragen om het plaatje opnieuw te laden. Waardoor het plaatje nooit getoond kan worden.

Wat je moet onthouden bij het aanroepen van loadImage() op andere plekken dan binnen preload(), is dat p5.js je sketch zal uitvoerenvoordat het plaatje geladen is. Dit betekent dat bepaalde attributen van het plaatje (zoals de breedte, hoogte of pixelgegevens) mogelijk niet beschikbaar zijn tot de draw() functie een flink aantal keer uitgevoerd is. Daarom is het een goede gewoonte om gebruik te maken van preload().

Meerdere plaatjes

Je bent niet beperkt tot het werken met één beeld. Je kunt zoveel variabelen om beelden in op te slaan aanmaken als je wil. Je moet alleen niet vergeten ze te laden in de preload() (of setup()). Het volgende voorbeeld laat een cat in een hond veranderen als de muis wordt ingedrukt. (Ik gebruik dit plaatje, oorspronkelijk van hier, opnieuw met een CC BY 2.0 licentie.)

Alpha kanaal

De plaatjes die we tot nog toe gebruikten waren in JPEG formaat. Afbeeldingen in JPEG formaat are gecomprimeerd en hebben in het algemeen een goede kwaliteit bij kleine bestandsgrootte. Een beperking van JPEG is dat het geen doorzichtigheid (transparency) toestaat. Je kunt dit eenvoudig zichtbaar maken door een plaatje tweemaal te tekenen:

Je ziet dat wanneer een kat bovenop de andere wordt getekend de hele rechthoek van de afbeelding wordt getekend. Dat zou best handig kunnen zijn in sommige gevallen maar is hier niet wat je verwacht (of wil).

Om te begrijpen hoe je plaatjes met een transparante achtergrond tekent, moet ik het idee van alpha channel (alpha kanaal) uitleggen. Het alpha channel is een vierde stukje informatie dat is opgeslagen in een pixel, naast de RGB-waarden (rood, groen, blauw), dat bepaalt hoe doorzichtig een pixel er op het scherm moet uitzien. Een alpha waarde van 255 is volledig ondoorzichtig, en een alpha waarde van 0 is volledig doorzichtig. The functies die we tot nog toe gebruikt hebben om kleur in te stellen ondersteunen allemaal een vierde parameter om de doorzichtigheid in te stellen. Hier is een voorbeeld:

In dit voorbeeld, bepaalt de vierde parameter van fill() hoe doorzichtig de vulkleur is. (De uitdrukking met sin() gebruikt de frame count om deze waarde langzaam op en neer te laten gaan.

Transparante PNGs

There’s another image format, called PNG, which does store transparency information. In a PNG, every pixel in the data has a value for its red, blue, and green amounts, and then an extra value (the alpha channel) for its transparency. Many PNGs you’ll find on the Internet already have transparency information; for example, here’s a sketch using an image of a Filet-O-Fish that I found on Wikimedia Commons:

This is nice, but what about our kitties? Unfortunately, turning a JPEG with a sort-of-plain background into a PNG with a transparent background isn’t as simple as opening the JPEG in an image editing program and saving it as a PNG. You need to manually remove the background. Removing backgrounds from images is a fine art and there are many tutorials about the process on the web. I’ve taken the liberty of (poorly) removing the background from our kitty image so that this section of the tutorial has a proper denouement. Here is the resulting image.

Here’s the same sketch as above, but with the transparent kitty PNG (and a solid color background!)

Working with image data

You can do more with an image object than just display it to the screen. Image objects come with a few bits of associated data, and some associated functions, that allow you to read and manipulate the data inside the image in interesting ways.

You can get an image’s width and height by accessing its width and height attributes. An object’s attributes are special values belonging to that object that are accessed by putting a dot (.) after the object’s variable name, and the name of the attribute after the dot. Assuming you’d already used loadImage() to load an image object into a variable called img, you would access that image’s width and height like so:

img.width
img.height

In the following sketch, I’m drawing ten copies of of the same kitty image, increasing their size proportionally by writing expressions with the image’s width and height:

Every image object also supports a method called .get(), which takes two parameters, an X and a Y coordinate, and returns the color at that coordinate. Call this method like so:

img.get(x, y)

NOTE: A method is a special kind of function that is associated with a type of value. The image objects that loadImage() returns, for example, have their own “library” of code snippets, called “methods,” that go along with each individual object. These code snippets are just like functions, except they specifically reference the data contained inside of the objet they’re associated with, and have to be called using a special syntax. We’ll discuss methods and attributes more when we talk about object-oriented programming.

In order to use .get(), you must first call the image’s .loadPixels() method. The best place to do this is inside of .setup().

Here’s an example that uses .get() to make a canvas where you “draw” an underlying image with the rect() command by accessing the color of the pixel at the current mouse position. (The image I used is a stunningly beautiful photography of Pluto’s moon Charon taken by the New Horizons probe.)

Another classic Processing example is to use the pixel data from an image to create a mosaic. Here’s the same image of Charon drawn in chunks using a nested for loop:

NOTE: The arithmetic’s a little complicated with this one. If you’re confused about what’s going on, try working out what the expressions in the draw() function would evaluate to for a few different values of i and j. Draw it out by hand if you need to!

Sounds

It’s also easy to make your p5.js sketches play sounds! In this section, I’ll take you through the basics of how to make this happen.

The basic workflow of using a sound file in a p5.js sketch looks a lot like using an image: find the sound that you want to use, copy it to your sketch folder, and then load the sound file data inside the sketch.

Sound formats

For simplicity’s sake, all of the example audio files in this section will be in MP3 format. But you should be aware that not every web browser supports MP3. In particular, MP3 support in Firefox is operating-system dependent. This example sketch in the p5.js reference shows how you can provide your audio files in different formats for maximum cross-platform compatibility.

Loading and playing a sound

The basic workflow of using a sound file in a p5.js sketch looks a lot like using an image: find the sound that you want to use, copy it to your sketch folder, and then load the sound file data inside the sketch.

You need to create an empty variable to hold the sound object, and then use the loadSound() function in preload() to load the data and assign the variable to the object. Once you’ve done so, you can call the object’s .play() method inside of draw() in order to trigger the sound. Here’s an example, using this audio file (source).

Cats should not be robots

The example above called the .play() function when the frameCount variable reached 30, so that the audio snippet plays shortly after the sketch starts running. What if we wanted to trigger the file to play whenever the mouse is pressed? You might think you could do it like this:

But that does something weird. The kitty sounds robotic or something. What gives? I put the console.log() function in there in order to give you a clue: you can see that it prints multiple times whenever the mouse is pressed. That’s because draw() runs over and over again, many times a second, while a common mouse click lasts for (let’s say) a half second or so. That means that the audio file will be triggered with play() many times over the course of a single mouse press. (This is what gives it the metallic quality; the sample is being triggered over itself, making it sound like an echo in a very small metal can.)

Clearly, we need some way to trigger the sample when a mouse button is pressed, but only when the mouse is first pressed and not subsequently during the same press. One way to do this would be with the sound object’s .isPlaying() method, which returns true if the sound is playing, and false if not:

Hey! It works!

Event functions

Another approach would be to use a feature of p5.js that we haven’t yet discussed: event functions. These are functions that you can define in your program that p5.js will call when some external event happens. The p5.js reference has a full list, but the simplest to understand is mousePressed(). If you define a function called mousePressed() in your sketch, p5.js will call it whenever the person using your sketch presses the mouse button. The benefit of mousePressed() for our purposes is that it only gets called once per mouse press!

Here’s the example above, reworked to use mousePressed():

Another event function that may be useful is keyTyped(). This function gets called whenever a key is pressed. Here’s the same sketch as above, but the sound is triggered by a keypress instead of a mouse click. (You may need to click inside of the sketch to give it focus for your keypresses to have any effect.)

A simple drum kit

Inside of the keyTyped() function, a special variable named key contains the current key being pressed. To check to see if a particular key is being pressed, use the following expression:

key == "X"

… where X is the key you want to check for (make sure to keep the quotes!).

The following sketch exploits this functionality to create a simple keyboard-controlled drum kit. (“A” is the kick drum; “L” is a closed hihat; “S” is the snare drum. Any other key makes a meow.)

Uploading sketches that use media

When you’re uploading your sketches to the Internet, make sure to upload your media files as well! They need to be in the same folder as your index.html file. If something doesn’t work, check the Javascript Console in your browser (in Chrome, View > Developer > Javascript Console) for error messages.

Also, make sure that you upload the p5.sound.js file along with your sketch (in the libraries folder). This should happen by default if you’re using the files produced by the p5.js IDE, but if you’re not using the IDE, you may need to upload it manually.

Further reading

  • The p5.sound library documentation has a complete reference. In particular, the p5.SoundFile page shows you all of the methods you can use with the sound objects returned from loadSound().
  • See the “Sound” examples at the p5.js examples page for examples of how to do more sophisticated things with sound files, such as filtering and frequency analysis.