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.)
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 ofi
andj
. 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.