8. Grafica computazionale

Calcolare le coordinate di punti e vertici

Schema base (calcolo e uso delle coordinate)


	for (var n=0; n<totPunti; n++)
	{
		x = espressione;
		y = espressione;

		point( x, y );
	}
	

calcolo diretto di un valore numerico, non letto da altre immagini o dati esterni.

Le coordinate x e y possono definire anche i vertici di un poligono.

Configurazioni sinusoidali


	function setup() {
		createCanvas( 600, 200 );
		background( 200 );

		var ampiezza = 90;
		var passoX = 2;
		var passoRadianti = PI/20;

		for (var n=0; n<300; n++)
		{
			var radianti = n*passoRadianti;
			var x = n * passoX;
			var y = 100 + sin(radianti)*ampiezza;

			ellipse( x,y, 10,10 );
		}
	}
	

calcola le posizioni sull'asse orizzontale distanziate di passoX pixel.

calcola l'oscillazione sull'asse verticale, partendo da 100 e con variazioni da -ampiezza a +ampiezza.

Provare a intervenire sul valore iniziale di passoRadianti.

Configurazioni circolari


	function setup() {
		createCanvas( 400, 400 );
		background( 200 );

		var raggio = 180;
		var passoRadianti = TWO_PI/60;

		for (var n=0; n<59; n++)
		{
			var radianti = n*passoRadianti;
			var x = 200 + cos(radianti)*raggio;
			var y = 200 + sin(radianti)*raggio;

			ellipse( x,y, 10,10 );
		}
	}
	

calcola l'oscillazione sull'asse orizzontale, partendo da 200 e con variazioni da -raggio a +raggio.

calcola l'oscillazione sull'asse verticale, partendo da 200 e con variazioni da -raggio a +raggio.

Provare a far chiudere il cerchio intervenendo sul for.

Configurazioni spiraliformi


	function setup() {
		createCanvas( 400, 400 );
		background( 200 );

		var raggio = 180;
		var passoRadianti = TWO_PI/60;
		var passoRaggio = 0.55;

		for (var n=0; n<300; n++)
		{
			var radianti = n*passoRadianti;
			var x = 200 + cos(radianti)*raggio;
			var y = 200 + sin(radianti)*raggio;

			ellipse( x,y, 10,10 );

			raggio -= passoRaggio;
		}
	}
	

valore di riduzione del raggio a ogni ciclo del for.

numero di punti equivalente a 5 giri completi del valore di radianti.

riduzione progressiva del valore di raggio.

Provare a modificare i valori di passoRaggio.

Configurazioni con segmenti rotanti


	function setup() {
		createCanvas( 500, 500 );
		stroke( 0, 50 );

		var raggio1 = 120;
		var raggio2 = 130;
		var passoRad1 = PI/99;
		var passoRad2 = PI/37;

		var c0x = 250;
		var c0y = 250;

		for (var n=0; n<1188; n++)
		{
			var rad1 = n*passoRad1;
			var c1x = c0x + cos(rad1)*raggio1;
			var c1y = c0y + sin(rad1)*raggio1;

			var rad2 = n*passoRad2;
			var c2x = c1x + cos(rad2)*raggio2;
			var c2y = c1y + sin(rad2)*raggio2;

			line( c1x,c1y, c2x,c2y);
		}
	}
	

centro di rotazione principale.

2° centro di rotazione (e inizio segmento da disegnare).

3° centro di rotazione (e fine segmento da disegnare).

Configurazioni con 4 segmenti rotanti

Esempio animato e con il cambio casuale dei parametri al clic del mouse: openprocessing.org/sketch/146046.

Generazione vertici con librerie JavaScript: ad es. Toxiclibs.js

Una libreria complessa ma anche più completa per gli aspetti principali del computational design è Toxiclibs.js.

L'esempio interattivo si può vedere in: haptic-data.com/toxiclibsjs/examples/super-ellipsoid.

Il codice è integrato in uno script basato su processing.js ma la libreria funziona anche con p5.js [vedi: poly-smooth-p5]

Calcolare le coordinate con funzioni iterative

Schema base (calcolo e uso delle coordinate)


	var xPrec = 0;
	var yPrec = 0;

	for (var n=0; n<totPunti; n++)
	{
		x = espressione che usa xPrec e/o yPrec;
		y = espressione che usa xPrec e/o yPrec;

		point( x, y );

		xPrec = x;
		yPrec = y;
	}
	

coordinate punto precedente.

Attrattore strano di Henon


	var xPrec = 0;
	var yPrec = 0;

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

	function draw() {

		var x = yPrec + 1-1.4*(xPrec*xPrec);
		var y = 0.3*xPrec;

		point( 300+x*220, 200+y*220 );

		xPrec = x;
		yPrec = y;
	}
	

i calcoli tengono conto delle coordinate del punto precedente.

le coordinate vengono mappate in modo da essere centrate sulla coordinata 300, 200 del canvas e avere una variazione massima da -220 a +220 pixel.

Fonte dell'immagine (e approfondimenti matematici): Strange Attractors

Per accelerare la visualizzazione dell'insieme di punti, è possibile farne calcolare 100 a fotogramma inserendo il codice del draw() all'interno di un for:

function draw() {
   for (var i=0; i<100; ++i) {
      ...
   }
}

Attrattore strano di Peter de Jong


	var xPrec = 0;
	var yPrec = 0;

	var a = -2;
	var b = -2;
	var c = -1.2;
	var d = 2;

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

	function draw() {

		var x = sin( a*yPrec ) - cos( b*xPrec );
		var y = sin( c*xPrec ) - cos( d*yPrec );

		point( 300+x*140, 300+y*140 );

		xPrec = x;
		yPrec = y;
	}
	

parametri della formula di De Jong, reimpostabili per ottenere configurazioni diverse.

i calcoli tengono conto delle coordinate del punto precedente, oltre che di una serie di parametri costanti.

le coordinate vengono mappate in modo da essere centrate sulla coordinata 300, 300 del canvas e avere una variazione massima da -140 a +140 pixel.

Altre configurazioni (con relativi parametri): paulbourke.net/fractals/peterdejong/

Per accelerare la visualizzazione dell'insieme di punti, è possibile farne calcolare 100 a fotogramma inserendo il codice suggerito nell'esempio precedente.

Calcolare i valori RGB dei pixel

Schema base (calcolo e uso delle coordinate)


	for (var ix=0; ix<width; ix++)
	{
		for (var iy=0; iy<height; iy++)
		{
			rosso = espressione;
			verde = espressione;
			blu =   espressione;

			stroke( rosso, verde, blu );
			set( ix, iy );
		}
	}
	updatePixels();
	

calcolo diretto di un valore numerico, non letto da altre immagini o dati esterni.

l'uso dell'istruzione set() permette di colorare direttamente solo il pixel.

l'istruzione updatePixels() aggiorna i pixel del canvas con i colori impostati da set().

Per accelerare le operazioni sarebbe meglio usare l'array pixels ma è stata scelta l'istruzione set() per mantenere un po' più comprensibile il codice.

Onde sinusoidali


	function setup() {

		for (var ix=0; ix<width; ++ix) {
			var x = ix / 2;

			for (var iy=0; iy<height; ++iy) {

				var s = sin( x );

				set( ix, iy, color(s*128+128) );
			}
		}
		updatePixels();
	}
	

valore di x pari alla metà della coordinata x del pixel (ix).

valore di s (da -1 a +1) rimappato sull'intervallo di valori dei grigi (da 0 a 255).

Onde sinusoidali inclinate


	function setup() {

		for (var ix=0; ix<width; ++ix) {
			var x = ix / 2;

			for (var iy=0; iy<height; ++iy) {
				var y = iy / 2;

				var s = sin( x+y );

				set( ix, iy, color(s*128+128) );
			}
		}
		updatePixels();
	}
	

valore di y pari alla metà della coordinata y del pixel (iy).

Provare a far muovere le onde inserendo il codice nel draw() e modificando:
  var x = ix/2;
in:
  var x = ix/2 + frameCount/10;

Gradazioni sinusoidali sommate fra loro (Quasicrystals)

Facendo la media di più onde sinusoidali con inclinazioni diverse è possibile ottenere pattern che ricordano quelli dei quasicristalli:

Basato su un'idea di Keegan McAllister: Quasicrystals as sums of waves in the plane.

Versione WebGL animata e interattiva: Animating Quasicrystals.

Versione processing.js animata: openprocessing.org/sketch/146053.

Effetto plasma


	function setup() {
		createCanvas( 200, 200 );
		colorMode( HSB );
	}

	function draw() {

		for (var ix=0; ix<width; ++ix) {
			var x = ix*0.04;
			var s1 = sin(x);

			for (var iy=0; iy<height; ++iy) {
				var y = iy*0.04;
				var s2 = sin(y);
				var s3 = sin((x+y + frameCount*0.1) / 2);

				var s = ( s1 + s2 + s3 ) / 3;  // media livelli

				var c = s * 128 + 128;
				set( ix, iy, color(c,255-c,255) );
			}
		}
		updatePixels();
	}
	

Potendo modificare tono, saturazione e brillantezza [vedi colorMode(HSB)] è possibile ottenere più colori a partire da un solo valore [vedi color(c,255-c,255)]:

Calcoli iterativi: l'insieme di Mandelbrot

L'immagine è ottenuta partendo da un numero complesso (C) con i valori immaginari [Im(C)] mappati sull'asse delle y e i valori reali [Re(C)] mappati sull'asse delle x.

Per ogni punto viene eseguito ripetutamente il calcolo: Z = Z² + C con Z che parte da 0,0 e non arriva mai a una distanza superiore a 2 se appartiene all'insieme di Mandelbrot. In tutti gli altri casi si usa il numero di iterazioni prima del superamento per determinare il colore del pixel mappato sui valori di C analizzati.

Modificando i limiti dei valori immaginari e reali di C, si riescono a ottenere dettagli interessanti soprattutto sul confine con i punti appartenenti all'insieme di Mandelbrot:

Una versione in processing.js con commenti in italiano si può trovare in: openprocessing.org/sketch/146383.

Una versione "navigabile" si può provare sul sito FractalLab dove è possibile esplorare anche i frattali 3D.

Generare e animare con funzioni pseudorandomiche

Valori casuali "coerenti" (Perlin noise)

Per ottenere una successione di valori non completamente slegati fra loro (come succede con random()) è possibile utilizzare una funzione che restituisce successioni coerenti di valori su una, due o tre dimensioni. Nella versione a due dimensioni il procedimento è il seguente:

Leggendo i valori dell'ultimo insieme, progressivamente e in entrambe le dimensioni, si possono ottenere valori casuali ma non discontinui come quelli del primo.

Approfondimenti: Generating Perlin Noise

La funzione noise()


	noise( x );

	noise( x, y );

	noise( x, y, z );
	

Tutte le varianti restituiscono valori che vanno da 0 a 1.

In p5.js si possono ottenere valori Perlin noise anche a 3 dimensioni.

Immagine di Anthony Mattox non più reperibile online.

Uso della funzione noise()


	function setup() {

		for (var x=0;  x<width;  x++) {
			for (var y=0;  y<height;  y++) {

				var grigio = noise( x/30, y/30 );

				set( x, y, color(grigio*255) );
			}
		}
		updatePixels();
	}
	

le coordinate possono avere anche valori decimali.

L'insieme dei valori cambia a ogni esecuzione dello sketch.
Altri parametri generali possono essere modificati attraverso l'istruzione noiseDetail()

Uso cinetico della funzione noise()


	function draw() {

		for (var x=0;  x<width;  x++) {
			for (var y=0;  y<height;  y++) {

				var grigio = noise( x/30.0, y/30.0, frameCount/150 );

				set( x, y, color(grigio*255) );
			}
		}
		updatePixels();
	}
	

per ottenere un'animazione.

variazione progressiva della terza coordinata per generare fotogrammi diversi ma ancora in continuità.

Uso di noise() per ricavare coordinate x e y

I valori della funzione noise() possono naturalmente essere utilizzati per definire qualsiasi parametri comprese le coordinate x e y:

Esempio in processing.js: openprocessing.org/sketch/199450.

Applicazioni "realistiche"

Nella grafica 3D sono stati utilizzati per simulare elementi naturali come materiali, fiamme, paesaggi, ecc.:

Slide dell'autore dell'algoritmo: noisemachine.com/talk1
Esempio di generazione di terreni: manatee-combine.com/?p=34.

Definire regole per simulare le leggi della fisica

Schema implementazione regole

Per ogni oggetto è necessario definire:

le proprietà parametri cromatici e geometrici che possono variare
(compresi i valori iniziali)

la visualizzazione istruzioni che visualizzano l'oggetto tenendo conto delle sue proprietà

gli aggiornamenti istruzioni che modificano le proprietà tenendo conto di fattori esterni
(tempo, input utente, altri oggetti, ecc.)

Esempio di implementazione


	var pos;
	var vel;

	function setup() {
		pos = createVector();
		vel = createVector( 1.7, 1.9 );
	}

	function draw() {
		background(204);

		ellipse( pos.x,pos.y, 6,6 );

		pos.add( vel );
		if (pos.x < 0 || pos.x > width) {
			vel.x = -vel.x;
		}
		if (pos.y < 0 || pos.y > height) {
			vel.y = -vel.y;
		}
	}
	

proprietà variabili del cerchio e inizializzazioni.

visualizzazione del cerchio.

aggiornamento della posizione e della direzione di spostamento (vel.x = -vel.x e vel.y = -vel.y).

Implementazione con una classe

Se sono necessarie più istanze dello stesso oggetto è utile definire e usare una classe:


	function Cerchio() {
		this.pos = createVector();
		this.vel = createVector( 1.7, 1.9 );
		this.display = function() {
			ellipse( this.pos.x, this.pos.y, 6, 6 );
		};
		this.update = function() {
			this.pos.add( this.vel );
			if (this.pos.x < 0 || this.pos.x > width) {
				this.vel.x = -this.vel.x;
			}
			if (this.pos.y < 0 || this.pos.y > height) {
				this.vel.y = -this.vel.y;
			}
		};
	}

	var cerchio;

	function setup() {
		cerchio = new Cerchio();
	}

	function draw() {
		background(204);
		cerchio.display();
		cerchio.update();
	}
	

Quando i codici non sono particolarmente complessi, le operazioni di aggiornamento vengono spesso inserite nel metodo display().

Uso di una libreria: ad es. Matter.js

Per utilizzare le leggi della fisica, senza conoscerne le formule, è possibile ricorrere a una libreria che gestisce automaticamente le proprietà di ogni oggetto.

Per p5.js non ne esistono ancora ma si può utilizzare una libreria JavaScript come Matter.js.

Videotutorial di Daniel Shiffman sull'uso di Matter.js con p5.js:
Physics Libraries - The Nature of Code [vedi a partire da 5.17]

Esempio d'uso minimo di Matter.js


	var box;     // oggetto che cade
	var circle;  // oggetto che rimbalza
	var ground;  // piano statico

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

		var engine = Matter.Engine.create();

		box = Matter.Bodies.rectangle( 150, 50, 40, 40 );
		circle = Matter.Bodies.circle( 250, 0, 20, { restitution: 1.0 } );
		ground = Matter.Bodies.rectangle( 200, 340, 300, 30, { isStatic: true } );

		Matter.World.add( engine.world, [circle, box, ground] );

		Matter.Engine.run(engine);
	}

	function draw() {
		background(200);

		rectMode( CENTER );
		rect( box.position.x, box.position.y, 40, 40 );
		ellipse( circle.position.x, circle.position.y, 40, 40 );
		rect( ground.position.x, ground.position.y, 300, 30 );
	}
	

inizializzazione della libreria.

creazione degli oggetti con preimpostazione dei parametri per far rimbalzare il cerchio (restitution: 1.0) e rendere immobile il piano (isStatic: true).

aggiunta degli oggetti al "mondo" (engine.world) che gestirà le interazioni fra loro e con lo "ambiente".

avvio degli aggiornamenti automatici delle proprietà di tutti gli ogggetti del "mondo".

Per far funzionare lo sketch è necessario linkare la libreria scaricabile da: github.com/liabru/matter-js/releases

Per poter interagire con gli oggetti attraverso il mouse è necessario sostituire le prime due righe del setup() con:

var canvas = createCanvas( 400,400 );
var engine = Matter.Engine.create();

var canvasM = Matter.Mouse.create( canvas.elt );
var mouseC = Matter.MouseConstraint.create(engine,{mouse:canvasM});
Matter.World.add( engine.world, mouseC );

Librerie alternative: ancora Toxiclibs.js

Codice dell'esempio (in Processing.js): Attraction2D.

Guida per Processing ma adattabile a p5.js: Verlet Physics with toxiclibs.

Esempio d'uso grafico delle leggi della fisica

Altri risultati: Mechanical Attractors.

Sistemi particellari

Fonte dello schema (e approfondimenti): Building an Advanced Particle System.
Immagini tratte da: vimeo.com/12052703.

Esempio d'uso grafico dei sistemi particellari

Video: vimeo.com/6284199.

Simulazione dei comportamenti

Agenti autonomi

Fonte immagini (e approfondimenti): Autonomous Agents.

Agente (quasi) autonomo: esempio minimo


	var pos;

	function setup() {
		pos = createVector();
	}

	function draw() {
		background(204);

		ellipse( pos.x,pos.y, 6,6 );

		var mouse = createVector(mouseX,mouseY);
		pos.lerp( mouse, 0.05 );
	}
	

interpolazione della posizione di pos con quella del mouse (mouse) e spostamento al 5% della distanza.

Provare a modificare il valore di interpolazione (0.05) da 0 a 1.

Implementazione di un agente autonomo con una classe

Classe di un agente che si sposta casualmente a partire da un punto (partenza) e invertendo la direzione quando supera i 340 pixel di distanza dal centro del canvas.


	function Agente( partenza ) {
		this.pos = partenza.copy();
		this.vel = p5.Vector.random2D();

		this.display = function() {
			ellipse( this.pos.x,this.pos.y, 9,9 );
			if (this.pos.dist(centro) > 340) {
				this.vel.mult( -1 );
			}
			if (random(100) < 33) {
				this.vel.rotate( random(-0.4,0.4) );
			}
			this.pos.add( this.vel );
		};
	}
	

inverte i valori x e y di spostamento (e quindi la direzione) se la posizione dell'agente è oltre i 340 pixel dal centro del canvas.

effettua una lieve rotazione casuale del vettore di spostamento con una probabilità del 33%.

applica lo spostamento.

Codice che crea a caso gli agenti facendoli partire dal centro ed eliminando i primi quando diventano troppi.


	var agenti = [];    // array degli agenti
	var agentiMax = 500;
	var centro;

	function setup() {
		createCanvas(720, 720);
		centro = createVector(width/2, height/2);
	}

	function draw() {
		if (random(100) < 20) {
			agenti.push( new Agente(centro) );
			if (agenti.length > agentiMax) {
				agenti.shift();
			}
		}
		for (var i=0; i<agenti.length; ++i) {
			agenti[i].display();
		}
	}
	

aggiunge un nuovo agente con una probabilità del 20% a ogni fotogramma.

elimina ogni volta il primo agente se il loro numero supera quello massimo impostato (agentiMax).

visualizza in successione ogni singolo agente "sopravvissuto".

Entrambi i codici possono essere messi all'interno dello stesso file (solitamente sketck.js) ma sarebbe meglio usare un file distinto per ogni classe prevedendo i link nel file HTML principale, ad esempio:
<script src="agente.js" type="text/javascript"></script>
<script src="sketch.js" type="text/javascript"></script>

Esempio di comportamenti "intelligenti"

Schema (e approfondimenti): red3d.com/cwr/boids.
Video: youtu.be/NYBes8ki3lo.
Esempio p5.js interattivo: p5.js: Flocking

Uso grafico del flocking

Immagini a destra (con codici in Processing): Follow Me 2.
Immagine a sinistra: Schwarm.