Eventi: mousePressed() e keyPressed()

Un po’ alla volta stiamo rendendo i nostri sketch sempre più interattivi grazie ai movimenti del mouse. Per proseguire il percorso della scorsa lezione e prima di imparare a creare e utilizzare a nostro piacere delle variabili, argomento che verrà trattato la prossima settimana, oggi parliamo di eventi.

Per fare un breve recap: abbiamo imparato come utilizzare i blocchi di codice setup() e draw() per suddividere i nostri sketch in due parti e far sì che parte del nostro codice sia eseguito solo all’avvio del programma e una parte, invece, venga processata in un loop costante. Grazie a questo, abbiamo anche visto che Processing è in grado di restituirci in ogni momento la posizione X e Y del mouse e di come possiamo usare questi dati per arricchire i nostri sketch.

Oltre al semplice movimento del mouse possiamo fare in modo che i nostri sketch riconoscano anche altri input disponibili comunemente su un computer: la pressione e il rilascio di un tasto del mouse o della tastiera.

Eventi

Comportandosi in modo simile a setup() e draw(), gli eventi sono descritti, anch’essi, come blocchi di codice. La loro particolarità è che il codice scritto al loro interno non viene eseguito automaticamente all’avvio del programma ma  al verificarsi di una condizione specifica come, per l’appunto, il click del mouse. A differenza della funzione draw(), le porzioni di codice all’interno degli eventi non vengono eseguiti in loop ma una volta soltanto.

Eventi del mouse: mousePressed() e mouseReleased()

La sintassi è simile a quella che abbiamo già visto; per il momento ignoriamo il significato di void e facciamo attenzione all’utilizzo delle maiuscole.

/*
 * Eventi mousePressed() e mouseReleased()
 * by Federico Pepe
 * http://blog.federicopepe.com
 */

void setup() {
  size(500, 500);
  background(255);
}

void draw() {
}

void mousePressed() {
  background(255, 0, 0);
}

void mouseReleased() {
  background(255);
}

Analizziamo insieme questo semplice programma:

  • dentro setup() impostiamo la grandezza della finestra 500x500px e il colore di sfondo bianco.
  • la funzione draw() per il momento non contiene alcuna riga di codice
  • il codice contenuto all’interno di mousePressed() verrà eseguito al click del mouse e imposterà rosso come colore di sfondo.
  • nel momento in cui verrà rilasciato il tasto del mouse, verrà eseguita la funzione mouseReleased() che ripristinerà il bianco come background.

Nel Reference di Processing trovate anche gli altri eventi legati al mouse:

È inutile che mi soffermi ora sul fare un esempio per ciascuno di essi: una volta capito il funzionamento è abbastanza intuitivo capire come usare le altre. Vi invito ugualmente a sperimentarle tutte ed, eventualmente, lasciare un commento qualora riscontraste dei problemi.

Eventi della tastiera: keyPressed() e keyReleased()

Allo stesso modo e con funzioni simili è possibile determinare se un tasto della tastiera è stato premuto oppure no:

/*
 * Eventi keyPressed() e keyReleased()
 * by Federico Pepe
 * http://blog.federicopepe.com
 */
 
void setup() {
  size(500, 500);
  background(255);
}

void draw() {
}

void keyPressed() {
  background(255, 0, 0);
}

void keyReleased() {
  background(255);
}

Se vi state chiedendo se è possibile determinare quale tasto è stato premuto la risposta è ovviamente sì. Per realizzare uno sketch del genere sarebbe, però, necessario introdurre alcuni argomenti che tratteremo in futuro: variabili booleane e cicli if… else if… else… per cui, per oggi, non andrei oltre.

Variabili built-in (mouseX, mouseY…)

Nell’ultimo post, abbiamo creato il nostro primo programma interattivo sfruttando le potenzialità della funzione draw() che, vi ricordo, viene eseguita in un loop costante a 60fps (di default) dal momento in cui avviamo il nostro programma fino a quando non lo fermiamo.

Negli ultimi due esempi di codice che ho postato ho aggiunto, senza dare troppe spiegazioni, due parole mouseX mouseY che possiamo definire come variabili built-in.

È arrivato il momento di capire cosa intentiamo per variabili, come funzionano e se ce ne sono altre che possiamo utilizzare.

Hard coding vs variabili

In tutti i programmi che abbiamo realizzato finora, abbiamo sempre scritto i parametri che volevamo utilizzare direttamente nelle funzioni. Per fare un esempio: per disegnare un cerchio con un diametro di 100 pixel al centro di una finestra di dimensione 500×500 pixel, la linea di codice che abbiamo usato è:

ellipse(250, 250, 100, 100);

Questa pratica viene definita hard coding ed è fortemente sconsigliata perché rende i nostri programmi lunghi da modificare e inutilizzabili o imprecisi al variare di alcune condizioni (se, riprendendo l’esempio di cui sopra, modifico la dimensione della finestra portandola a 700×700 pixel, il cerchio non sarà più centrato come prima).

Più avanti, in questo post, riprenderò l’esempio qui descritto con codice e screenshot.

Per ovviare a questo problema si utilizzano delle variabili ovvero dei parametri che possono assumere un valore che può essere cambiato durante l’esecuzione del programma attraverso, ad esempio, semplici funzioni matematiche.

Variabili built-in

È possibile creare un numero di variabili a nostro piacimento e con tipologie di dati differenti e, in un altro post, vi spiegherò come fare. In questo articolo voglio concentrarmi su un insieme particolare di variabili: quelle cosiddette built-in ovvero già presenti “all’interno” di Processing.

Una cosa molto importante quando si utilizzano variabili è prestare attenzione alla sintassi corretta: Processing ci viene in aiuto colorandole di rosa.

Built-in variables

mouseX, mouseY

Abbiamo già usato mouseX e mouseY in uno sketch e abbiamo capito che, se inseriamo queste variabili, all’interno del blocco di codice draw() ci restituiscono in tempo reale i valori X e Y del mouse quando ci muoviamo all’interno della finestra.

width, height

width e height significano letteralmente larghezza e altezza e, com’è facile intuire, restituiscono le dimensioni della finestra dopo che queste sono state impostate nel blocco di codice setup().

Analizziamo insieme questo codice e il risultato finale:

/*
 * Hard coding vs built-in variables
 * by Federico Pepe
 * http://blog.federicopepe.com
 */
void setup() {
  // Imposto la grandezza della finestra a 500x500px
  size(500, 500);
  // Imposto colore di riempimento ed elimino il bordo
  noStroke();
  fill(255, 0, 0, 100);
  // Disegno un cerchio di diametro 100px al centro
  // della finestra utilizzando l'hard-coding
  ellipse(250, 250, 100, 100);
  // Disegno un cerchio di diametro 150px al centro
  // della finestra utilizzando due variabili built-in
  ellipse(width/2, height/2, 150, 150);
}

Processing hard coding vs built-in variables

Abbiamo disegnato due cerchi di diametro differente in una finestra di 500x500px. Nel primo caso i parametri x e y sono stati inseriti direttamente (hard-coding) quindi:

ellipse(250, 250, 100, 100);

Nel secondo cerchio, invece, abbiamo utilizzato le variabili width e height e, per centrare il cerchio, le abbiamo divise per 2.

ellipse(width/2, height/2, 150, 150);

Cosa accade se modifico la dimensione della finestra?

/*
 * Hard coding vs built-in variables
 * by Federico Pepe
 * http://blog.federicopepe.com
 */
void setup() {
  // Imposto la grandezza della finestra a 700x700px
  size(700, 700);
  // Imposto colore di riempimento ed elimino il bordo
  noStroke();
  fill(255, 0, 0, 100);
  // Disegno un cerchio di diametro 100px al centro
  // della finestra utilizzando l'hard-coding
  ellipse(250, 250, 100, 100);
  // Disegno un cerchio di diametro 150px al centro
  // della finestra utilizzando due variabili built-in
  ellipse(width/2, height/2, 150, 150);
}

Hard coding vs Variabili 2

Come previsto, il primo cerchio si trova spostato in alto a sinistra rispetto al centro della finestra mentre, il secondo, è rimasto perfettamente centrato. Per ottenere a livello visivo lo stesso effetto di prima nel caso del primo cerchio dovrei andare a modificare direttamente i parametri (oppure sostituirli con delle variabili).

pmouseX, pmouseY

Le ultime due variabili built-in di cui voglio parlarvi sono pmouseX e pmouseY che, a differenza di mouseX e mouseY restituiscono i valori x e y del mouse del frame precedente a quello corrente.

Come possiamo utilizzarle in modo creativo? Con poche righe di codice possiamo, ad esempio, creare un programma per disegnare:

/*
 * Drawing Tool
 * by Federico Pepe
 * http://blog.federicopepe.com
 */
void setup() {
  size(700, 700);
  background(0);
}

void draw() {
  stroke(255);
  line(pmouseX, pmouseY, mouseX, mouseY);
}
Processing example
Quattro linee di codice sono sufficienti per creare, in Processing, un programma per disegnare.

Blocchi di codice e flusso: setup() e draw()

Oggi introduciamo due concetti di base molto importanti che ci porteranno a realizzare dei programmi interattivi con Processing.

Fino ad oggi, ci siamo limitati a scrivere una linea di codice dopo l’altra e abbiamo imparato che quando clicchiamo sul pulsante Run, se non ci sono errori, ciascuna di esse viene processata in ordine dalla prima all’ultima.

Gli sketch che abbiamo realizzato nelle scorse lezioni sono belli ma poco interessanti perché siamo abituati a utilizzare programmi che, una volta lanciati, cambiano aspetto in base al procedere del tempo e a come interagiamo con essi attraverso mouse, tastiera, ecc…

Pensate, ad esempio, allo screensaver del vostro computer oppure a un qualsiasi gioco per pc: tutte queste applicazioni partono da uno stato iniziale in cui vengono settate alcune impostazioni e, mentre il programma funziona, il computer controlla costantemente la posizione del mouse oppure se abbiamo premuto un determinato tasto. Idealmente questo controllo dovrebbe avvenire 30 volte al secondo.

setup() e draw()

In Processing esistono due funzioni molto importanti: setup() draw(). Prima di procedere con la spiegazione del loro funzionamento, dobbiamo capire come dividere in porzioni o, per meglio dire, blocchi il nostro codice in modo da suddividere in modo preciso le operazioni che saranno eseguite in fase iniziale di setup e quelle che, invece, saranno costantemente ripetute dal computer in draw.

Per delimitare l’inizio e la fine dei blocchi di codice si usano le parentesi graffe:

void setup() {
  // Blocco di codice di setup
}

void draw() {
  // Blocco di codice di draw
}

Vi chiedo di ignorare, per il momento, il significato di void e il fatto che, a differenza delle funzioni usate finora, non abbiamo inserito nessun parametro all’interno delle parentesi tonde; torneremo su questo argomento più avanti quando tratteremo in modo più approfondito le funzioni.

Ora possiamo riprendere alcuni esempi delle lezioni precedenti e ragionare su quali porzioni di codice siano da inserire in setup() e quali in draw(). Sottolineo ancora una volta che le linee inserite nel blocco di codice setup verranno eseguite una sola volta all’avvio del programma mentre, quelle nel blocco draw, saranno processate continuamente dal nostro computer a una velocità, di default in Processing, di 60 frame per secondo, d’ora in poi abbreviato in fps.

Impostare la dimensione della finestra è un’operazione che può e deve essere eseguita una volta sola; disegnare un cerchio in una determinata posizione e impostare il suo colore è un’operazione che, invece, può essere ripetuta.

Ecco dunque che questo codice, preso dalla scorsa lezione:

size(500, 500);
fill(255, 0, 0);
ellipse(250, 250, 150, 150);

diventa:

void setup() {
  // Questo codice verrà eseguito una sola volta
  size(500, 500);
}

void draw() {
  // Questo codice verrà ripetuto a una frequenza di 60fps
  fill(255, 0, 0);
  ellipse(250, 250, 150, 150);
}

Se copiate e incollate questi due esempi in Processing e cliccate su Run non noterete alcuna differenza a livello visivo ma il computer sta processando questi programmi in modo diverso:

Nel primo caso:

- Imposta la dimensione della finestra a 500x500px
- Imposta il colore di riempimento: RGB(255, 0, 0)
- Disegna un ellisse con centro x = 250, y = 250, largh. = 150 e alt. = 150

Nel secondo caso:

- Imposta la dimensione della finestra a 500x500px
- Imposta il colore di riempimento: RGB(255, 0, 0)
- Disegna un ellisse con centro x = 250, y = 250, largh. = 150 e alt. = 150
- Imposta il colore di riempimento: RGB(255, 0, 0)
- Disegna un ellisse con centro x = 250, y = 250, largh. = 150 e alt. = 150
- Imposta il colore di riempimento: RGB(255, 0, 0)
- Disegna un ellisse con centro x = 250, y = 250, largh. = 150 e alt. = 150
- Imposta il colore di riempimento: RGB(255, 0, 0)
- Disegna un ellisse con centro x = 250, y = 250, largh. = 150 e alt. = 150
...

Le ultime righe di informazioni sono ripetute 60 volte al secondo all’infinito finché non interrompiamo il programma premendo il tasto Stop.

La nostra prima animazione

Dal momento che con questo esempio non riusciamo a vedere nulla di diverso sullo schermo, rendiamo le cose un po’ più interessanti introducendo un’altra novità di cui parleremo in modo approfondito nelle prossime lezioni: anziché impostare una posizione x e y predefinita, disegniamo il nostro cerchio rosso in base alla posizione del mouse (mouseX e mouseY):

void setup() {
  size(500, 500);
}

void draw() {
  background(0);
  fill(255, 0, 0);
  ellipse(mouseX, mouseY, 150, 150);
}

Ecco cosa accade:

Processing Animation

Anche se in questa immagine non si vede il cursore del mouse, il nostro occhio percepirà il movimento del cerchio come un’animazione: abbiamo creato il nostro primo sketch interattivo perché risponde a un input esterno e che cambia col passare del tempo.

A questo punto, per essere sicuri di aver compreso la differenza tra setup e draw possiamo domandarci: cosa accade se spostiamo background(0) dal blocco draw a quello di setup?

void setup() {
  size(500, 500);
  background(0);
}

void draw() {
  fill(255, 0, 0);
  ellipse(mouseX, mouseY, 150, 150);
}

Questa volta il colore di sfondo viene impostato solo una volta all’avvio del programma, questo significa che ogni volta che verrà mosso il mouse, un nuovo cerchio verrà disegnato sopra ai precedenti.

Processing setup and draw

 

Esercizio 1: Piet Mondrian

Ecco una delle possibili soluzioni del primo compito a casa: realizzare uno sketch ispirato alle composizioni geometriche di Piet Mondrian.

Come ho già avuto modo di scrivere:

Imparare a programmare non significa soltanto studiare, capire la sintassi e copiare-incollare del codice trovato su internet ma anche porsi dei problemi e provare a risolverli autonomamente.

Il mio consiglio è, ancora una volta, non fermarsi a guardare la soluzione da me proposta ma provare a realizzare una soluzione alternativa. Vi invito a condividere i vostri sketch nei commenti!

Piet Mondrian
Una delle possibili soluzioni: realizzare uno sketch ispirandosi a Piet Mondrian.

E il codice:

/*
 *  Esercizio 1: "Piet Mondrian"
 *  by Federico Pepe
 *  http://blog.federicopepe.com/processing
 */
background(0);
size(700, 500);
noStroke();
fill(255);
rect(10, 10, 100, 100);
rect(10, 125, 100, 250);
fill(0, 0, 255);
rect(10, 390, 100, 100);
fill(255, 0, 0);
rect(120, 10, 400, 365);
fill(255, 255, 0);
rect(120, 390, 400, 100);
fill(255);
rect(530, 10, 160, 250);
rect(530, 270, 160, 220);

Colori RGB

Nell’ultimo post abbiamo imparato cosa sono le primitive 2D e siamo riusciti a far disegnare al nostro computer alcune semplici forme geometriche sullo schermo. Nel realizzare questi semplici sketch forse qualcuno di voi avrà notato una particolarità: di default Processing colora di bianco l’interno della forma e di nero il bordo mentre lo sfondo dello sketch rimane grigio.

Processing's function ellipse()
Uno screenshot preso dagli esempi del post precedente.

RGB: Red, Green and Blue

Prima di cominciare a utilizzare i colori è importante capire come funzionano. Alle scuole elementari si impara che attraverso la miscelazione dei tre colori primari, rosso, giallo e blu, si possono ottenere tutti i colori nelle diverse sfumature. Anche gli schermi funzionano in modo simile ma i tre colori di base sono rosso, verdeblu in inglese RGB.

Colori RGB
Mescolanza additiva dei tre colori primari.

Grazie a questa immagine si può capire come ottenere i colori secondari:

  • rosso + verde = giallo
  • rosso + blu = viola
  • verde + blu = azzurro
  • rosso + verde + blu = bianco
  • nessun colore = nero

Le funzioni fill() e stroke()

In Processing esistono due funzioni per colorare le forme: fill() stroke(). Con la prima indichiamo il colore di riempimento della forma mentre, con la seconda, il colore del bordo.

Quando lavoriamo nella modalità RGB, che è quella di default, queste due funzioni possono accettare unodue, tre o quattro parametri.

Facciamo degli esempi pratici:

Utilizzo di tre parametri per assegnare un colore

size(500, 500);
fill(255, 0, 0);
ellipse(250, 250, 150, 150);
Cerchio Rosso
Con la funzione fill() abbiamo creato un cerchio rosso.

Se provo a tradurre in italiano quanto scritto nel codice, le istruzioni date al computer sarebbero:

- Disegna una finestra di 500 pixel di larghezza e 500 pixel di altezza.
- Colora la forma con la massima quantità di rosso (255), niente verde (0), e niente blu (0).
- Disegna un ellisse con centro nella posizione x = 250, y = 250, larghezza = 150 e altezza = 150.

Non avendo specificato un parametro per il colore del bordo, come accaduto in precedenza, Processing utilizzerà il parametro di default e colorerà il bordo di nero.

Ora facciamo una piccola modifica e aggiungiamo una riga al nostro codice:

size(500, 500);
fill(255, 0, 0);
stroke(0, 0, 255);
ellipse(250, 250, 150, 150);

Ora abbiamo aggiunto la funzione stroke() assegnando i parametri rosso = 0, verde = 0, blu = 255 e questo è il risultato:

Cerchio rosso con bordo blu
Il cerchio è ancora rosso ma ora c’è anche il bordo blu

Come avrete capito, se utilizzo tre parametri nelle funzioni fill() e stroke() sto indicando la quantità di rosso, verde e blu il cui valore deve essere compreso tra 0, il valore minimo, e 255, quello massimo.

Un parametro solo: la scala di grigi

Come scritto in precedenza, entrambe le funzioni possono accettare anche un solo parametro: in questo caso Processing considererà il valore inserito come valore assegnato a tutti e tre i parametri RGB.

Scrivere:

fill(127);

Equivale a:

fill(127, 127, 127);

Facendo un po’ di esperimenti con i colori, scoprirete che quando i parametri RGB sono equivalenti, sto lavorando in scala di grigi.

Per ottenere il bianco, infatti, mi basterà scrivere: fill(255) mentre, per il nero fill(0).

Due o quattro parametri: la trasparenza

Se utilizziamo due parametri con il primo indichiamo il colore nella scala di grigi che abbiamo scelto e con il secondo impostiamo un livello di trasparenza in una scala da 0 (completamente trasparente) a 255 (completamente opaco).

Quando, invece, utilizziamo quattro parametri stiamo aggiungendo la trasparenza a un colore specifico dettato dai primi tre parametri.

size(500, 500);
fill(255, 0, 0, 100);
stroke(0, 0, 255);
ellipse(250, 250, 150, 150);
RGB with Transparency
Il quarto parametro nella funzione fill() indica la trasparenza del colore.

background(), noFill() e noStroke()

Finora abbiamo sempre parlato di colorare forme. Ma se volessimo modificare lo sfondo della finestra dobbiamo ricorrere alla funzione background() che, per quanto concerne la sintassi e il numero di parametri accettati dalla funzione, ricalca fill() e stroke().

background(0);
size(500, 500);
fill(255, 0, 0, 100);
stroke(0, 0, 255);
ellipse(250, 250, 150, 150);
background(0)
Tramite la funzione background, abbiamo impostato uno sfondo nero.

Con le funzioni noFill() e noStroke() – attenzione alle maiuscole! – possiamo eliminare il colore del bordo o quello di riempimento. Com’è facile intuire, queste funzioni non richiedono alcun parametro.

Color selector

Giustamente vi starete chiedendo: come faccio a conoscere i valori RGB di un determinato colore? Non vi preoccupate, Processing mette a disposizione uno strumento molto utile chiamato Color Selector accessibile dal menu Tools > Color Selector la cui funzione è esattamente quella di aiutarvi nella selezione dei colori.

Color Selector
Color Selector ci aiuta a trovare i parametri RGB, HSB e Hex di ogni colore.

Oltre ai valori RGB, troviamo anche quelli HSB e Hexadecimal di cui parleremo prossimamente.

Mettiamo tutto insieme

Esempio 1:

/*
 *  Colori RGB
 *  Federico Pepe
 *  http://blog.federicopepe.com/processing
 */
 
// Impostiamo la dimensione della finestra a 500x500px
size(500, 500);
// Impostiamo lo sfondo di colore bianco
background(255);
// Impostiamo di non avere un bordo
noStroke();
// Impostiamo una tonalità di rosso come riempimento
fill(255, 157, 157);
// Disegnamo il primo cerchio
ellipse(100, 250, 100, 100);
// Impostiamo il bordo di colore nero
stroke(0);
// Disegniamo il secondo cerchio
ellipse(250, 250, 100, 100);
// Impostiamo una tonalità di verde come riempimento
fill(157, 255, 179);
// Disegniamo il terzo cerchio
ellipse(400, 250, 100, 100);

Sono tante linee di codice ma non spaventatevi! Prima di analizzare il risultato vorrei introdurre un’ultima novità: l’utilizzo dei commenti nel codice.

I programmatori spesso commentano il proprio codice per spiegare brevemente come funziona quella porzione di codice oppure per lasciare dei promemoria o delle indicazioni. I commenti sono utili sia se dobbiamo condividere il nostro codice con altre persone sia per ricordare a noi stessi come funzionava quel particolare programma.

Quando diventerete dei programmatori esperti, sarà interessare recuperare dall’hard disk i vostri primi programmi e confrontarli con gli ultimi. È per questo che io ho preso l’abitudine di inserire sempre, all’inizio di ogni programma, la data di creazione o di ultima modifica.

Se dovete scrivere un commento di molte righe di testo come, ad esempio, l’intestazione all’inizio del programma, dovete utilizzare /* per indicare l’inizio del commento e */ per indicarne la fine.

Se, invece, il commento è di una sola riga, è sufficiente scrivere // all’inizio della riga stessa.

Il codice qui sopra darà questo risultato:

Processing RGB Color Example 1

La cosa interessante da notare è che una volta specificato un colore con la funzione fill() o stroke(), rimarrà impostato anche per le forme successive finché non utilizziamo la funzione noFill() o noStroke() oppure finché non impostiamo nuovamente il colore.

Nello sketch di esempio, infatti, il riempimento di colore rosso viene settato solo una volta all’inizio eppure viene applicato ai primi due cerchi così come il bordo nero, inserito subito prima di disegnare la seconda forma, rimane anche per la terza.

Esempio 2

/*
 *  Colori RGB e trasparenza
 *  Federico Pepe
 *  http://blog.federicopepe.com/processing
 */
 
// Impostiamo la dimensione della finestra a 500x500px
size(500, 500);
// Impostiamo lo sfondo di colore nero
background(0);
// Impostiamo di non avere un bordo
noStroke();
// Impostiamo una tonalità di rosso come riempimento
fill(255, 0, 0, 127);
// Disegnamo il primo cerchio
ellipse(200, 250, 150, 150);
// Disegniamo il secondo cerchio
ellipse(250, 250, 150, 150);
// Impostiamo una tonalità di verde come riempimento
fill(127, 255, 127);
// Disegniamo il terzo cerchio
ellipse(300, 250, 150, 150);

Esempio 2

In questo secondo esempio ho utilizzato la trasparenza nei primi due cerchi rossi (notate cosa succede quando si sovrappongono). Il terzo cerchio, quello verde, non avendo un parametro per la trasparenza ed essendo l’ultimo inserito nello sketch viene disegnato “sopra” gli altri due.

Compiti per casa

Arrivati a questo punto, è il momento di mettersi in gioco. Imparare a programmare non significa soltanto studiare, capire la sintassi e copiare-incollare del codice trovato su internet ma anche porsi dei problemi e provare a risolverli autonomamente.

Ho pensato che per rendere questa serie di post più interessante fosse necessario introdurre dei compiti a casa per dare la possibilità, a voi che leggete, di sbattere un po’ la testa.

Il compito di questa settimana è realizzare uno sketch ispirato alle composizioni geometriche di Piet Mondrian. Potete inviare le vostre soluzioni nei commenti qui sotto oppure via twitter @fedpep.