MokaByte Numero 07 - Aprile 1997

 
Grafica vettoriale con Java
qualche esperimento...
IV parte
 
di
Domenico De Riso
contiene applets

 

Con questo articolo si conclude la prima tappa di questa avventura.

E' stato mostrato come si può disegnare con Java e OOP generico in modo facile e senza quasi mai utilizzare formule e numeri.
Nonostante la libreria di "classi-entità geometriche" presentate sia non molto ricca (Punto, Linea e Circonferenza e qualcuna ausiliaria), ognuno di esse è sufficientemente potente grazie al gran numero di methods di sono dotate. Tali methods sono molto efficienti ed utili alla creazione facilitata di qualsiasi altra "entità" complessa.

In un qualsiasi corso tradizionale l'esposizione di questi articoli dovrebbe avvenire per gradi. Cioè trattare in modo completo tutte le "clssi-entità" e poi passare alle applicazioni pratiche. Ma questo non è un corso tradizionale! Piuttosto una serie di articoli che ha il compito primario di non annoiare (se non lo ha fato già!) chi legge. Perciò è meglio procedere in modo parallelo: su ogni prossimo numero, a partire da questo, prima tratteremo i nuovi argomenti e poi cercheremo di arricchire le classi già presentate con nuovi methods.


Il termine "entità" viene usato per indicare gli oggetti geometrici della grafica vettoriale perchè radicalmente diversi dagli oggetti disegnati usualmente su carta.
Una figura geometrica, ad esempio un linea, disegnata su carta non potrà mai fornire le sue caratteristiche geometriche in modo preciso perchè creata direttamente su supporto di visualizzazione (a meno che il disegno non venga completamente quotato). Un pò come quando si disegna con il Paint di Windows. Una linea disegnata è solo un insieme di pixel quasi allineati che non ci forniscono alcuna informazione. Su carta è ancora peggio. Se non fosse per una nostra elaborazione mentale, la linea verrebbe scambiata per una macchia. Infatti i programmi OCR o di vettorializzazione di disegni da supporto cartaceo hanno bisogno di un "coefficiente di rumore" che sta ad indicare fino a che punto il programma deve considerare un oggetto come buono e allora cercare di capire di quale carattere alfabetico o figura geometrica si tratta o se è semplicemente una macchia sul foglio.
Una linea disegnata con una matita è più vicina ad un rettangolo o una striscia che ad una linea... Invece, gli oggetti in un disegno vettoriale sono solo un'immagine delle loro reali informazioni tutt'altro che approssimative.
Le caratteristiche di ogni entità sono memorizzate in un database e consistono solo di quelle che sono necessarie e sufficienti a definire l'entità stessa. Ad esempio, quando viene disegnata una linea sullo schermo appaiono tutti i punti (possibili per lo schermo) per rappresentarla. In realtà' essi sono solo l'immagine approssimativa corrente che la linea avrebbe quando nel database le coordinate degli estremi sono del valore stabilito. Insomma nel database vanno ad essere memorizzati solo le coordinate degli estremi in modo da isolare la linea dal supporto di visualizzazione (schermo, plotter) che quanto più esso è di qualità superiore (e costa!) tanto più l'immagine della linea appare definita.
Cosi' noi in questi articoli stiamo trattando le figure geometriche: come "entità", e solo dopo ci preoccupiamo della loro rappresentazione.
Ma non dovete meravigliarvi. L'uomo lo fa da sempre.
In Geometria dire "sia C una circonferenza con centro in P e raggio r", non significa che debba essere obbligatoriamente disegnata. Essa deve essere solo immaginata, perchè nessun disegno potrebbe veramente rappresentare in modo preciso tale linea, così la grafica vettoriale tratta la linea "immaginandosela", cioè creandola nel database, o in memoria, e solo alla richiesta di visualizzazione (nel nostro caso i methods disegna() della varie classi geometriche) viene mostrata la sua forma in modo imperfetto sullo schermo.
Forse per questo è stata denominata "entità". Le entità sono qualcosa che non si possono vedere... nella loro completezza.
Ripassatevi un pò la filosofia di Platone e di Aristotele!

Ritorniamo alle nostre entità!

La grafica vettoriale si compone di quattro fasi fondamentali: creazione, gestione, modifica e visualizzazione.
Fino ad ora ci siamo occupati solo della fase di creazione tranne che per il method disegna() che appartiene appunto alla fase di visualizzazione.
Praticamente i methods e i costructors trattati fino ad ora hanno al massimo la funzione di creazione di una entità con caratteristiche specificate dal programmatore (come: sia L una linea tra i punti P1(10,20) e P2(30,40)) e/o dipendenti da eventi nello spazio geometrico causati da entità già esistenti (come: sia L una linea tra i punti d'intersezione delle circonferenze C1 e C2)
Ora tratteremo la fase di gestione e precisamente ci preoccuperemo di scrivere dei methods utili al cambio delle caratteristiche di base delle entità già esistenti nel disegno (ovvero nel database). Tali methods riguardano lo spostamento e la rotazione delle entità.

SPOSTAMENTO:
 

Per spostare un oggetto reale basta afferrarlo per un punto e trascinarlo fino al luogo di destinazione. Di solito un oggetto pesante è provvisto di una maniglia per rendere più agevole lo spostamento. Bene! Tale maniglia è di importanza fondamentale nella grafica vettoriale. Infatti esiste sempre e si chiama "punto origine dello spostamento" ed ha un vantaggio rispetto alla realtà: una valigetta ha la maniglia sempre legata fisicamente ad essa, mentre un oggetto della grafica vettoriale può avere la "maniglia" anche non legata all'oggetto stesso. Cioè il punto origine dello spostamento può essere rappresentato da qualsiasi punto. Quando poi si definisce il punto destinazione dello spostamento, l'entità viene traslata parallelamente lungo la linea immaginaria punto-origine punto-destinazione.
Se non è chiaro provate l'applet a destra cliccando un punto origine e uno destinazione e osservate lo spostamento della circonferenza rossa. Naturalmente lo spostamento sarà più comprensibile scegliendo un punto origine ("la maniglia") interno alla circonferenza rossa. La circonferenza nera mostra la posizione precedente di quella rossa. Se la circonfrenza esce dalla finestra di visualizzazione cliccate su RESET.
Il method sposta(...) relativo allo spostamento è definito in tutte e tre le classi di entità di base Punto, Linea e Circonferenza, ma quello che effettivamente causa lo spostamento è il method contenututo nella class Punto.

  void sposta(Punto Porig, Punto Pdest) {

    Linea lPoPd = new Linea(Porig, Pdest);

    x = x + lPoPd.deltaX();

    y = y + lPoPd.deltaY();

  }

Invece, i methods sposta() delle classi Linea e Circonferenza richiamano quello contenuto in Punto.

ROTAZIONE:
 

La rotazione implica sempre la specifica di un angolo. Un angolo ha sicuramente un vertice, ed un vertice è sicuramente un punto. Tale punto si chiama punto di rotazione. Perciò per effettuare una rotazione di un'entità bisogna specificare un punto di rotazione ed un angolo.
Il method successivo ruota(...) contenuto nella class Punto ruota il punto (this) intorno a quello specificato.
Tale method, come quello dello spostamento è definito in tutte e tre le classi di entità di base Punto, Linea e Circonferenza, ma chi effettivamente causa la rotazione è quello contenututo nella class Punto.

  void ruota(Punto Prot, double alfa) {

// precalcolo del seno e coseno

    double s=Math.sin(alfa), c=Math.cos(alfa);

// calcolo delle coordinate relative al punto di rotazione

    Linea Cp =new Linea(new Punto(0,0), Prot);

    double xo=x - Cp.deltaX();

    double yo=y - Cp.deltaY();

// rotazione

    double X = xo * c - yo * s;

    double Y = xo * s + yo * c;

// riassegnazione delle coordinate assolute al punto ruotato

    x=X+Cp.deltaX();

    y=Y+Cp.deltaY();

  }

Tutto il codice necessario alla sperimentazione è contenuto nei files newGraphics.java, che contiene anche il codice dell'articolo dei numeri precedenti, ma aggiornato, e vectors4.java contenente il codice necessario alle applet visibili in questa pagina.

I methods esaminati fino ad ora sono utilissimi per ricavare informazioni o a cambiare lo stato delle entità stesse, ma agiscono solo ed esclusivamente su una entità alla volta.
Nei casi più comuni, quando si deve rappresentare un grafico, soprattutto quando si tratta di animazioni, gli spostamenti e le rotazioni sono gli eventi più frequenti. Ma, il più delle volte, sono applicati a più oggetti contemporaneamente: ad oggetti raggruppati.
Ad esempio: se un astronomo deve rappresentare un sistema planetario ha bisogno di rotazioni applicate innanzitutto ai pianeti intorno alla stella (Sole) e poi applicate ai satelliti intorno ai pianeti e così via.
Passiamo allora all'introduzione di una nuova class utile sia al raggruppamento di entità semplici che a quella di entitè composte da gruppi stessi (sottogruppi).
La class si chiama gruppo e contiene gli stessi methods (disegna(), sposta(), ruota()) delle entità di base Punto, Linea e Circonferenza. Solo che vengono applicati a tutti gli elementi del gruppo. E se un elemento è a sua volta un sottogruppo allora vengono applicati ricorsivamente.
Le entità contenute in un gruppo vanno ad essere immesse in un Vector.
Inoltre se osservate vectors4.java vi acorgerete che tutte le classi di base sono diventate di tipo "extends entita" dove entità è appunto una abstract class utile al casting (entita) nei methods di gruppo disegna(), sposta(), ruota().

class gruppo extends entita {

  Vector v=new Vector();

  Color colore;
 
 

// definisce un gruppo di entita' con specifica di colore

  gruppo(Color colore) {

    this.colore=colore;

  }
 
 

// definisce un gruppo di entita' con colore standard Color.black

  gruppo() {

  }
 
 

// aggiunge un'entita' al gruppo

  void aggiungi(entita e) {

    v.addElement(e);

  }
 
 

// sposta il gruppo parallelamente allo spostamento Porig - Pdest

  void sposta(Punto Porig, Punto Pdest) {

    for (int i=0; i<v.size(); i++) {

       ((entita) v.elementAt(i)).sposta(Porig, Pdest);

    }

  }
 
 

// ruota di un angolo specificato il gruppo intorno ad un punto

  void ruota(Punto Prot, double alfa) {

    for (int i=0; i<v.size(); i++) {

       ((entita) v.elementAt(i)).ruota(Prot, alfa);

    }

  }
 
 

// disegna l'intero gruppo di entita'

  void disegna(Color colore, Graphics g) {

    for (int i=0; i<v.size(); i++) {

      ((entita) v.elementAt(i)).disegna(colore, g);

    }

    v.removeAllElements();

  }
 
 

  void disegna(Graphics g) {

    disegna(colore, g);

  }

}


APPLET DI ESEMPIO:

Visto che abbiamo accennato all'esempio dell'astronomo alle prese con un sistema planetario, perchè non realizzarlo? Useremo, però, delle semplici rotazioni non derivanti dalle formule di Keplero.
 

Allora sia S una stella intorno alla quale girano i pianeti C1 e C2. Intorno a C2 ruotano due satelliti C2a e C2b con versi opposti di rotazione.
Analizzando tale sistema si può ridurlo a due gruppi costituiti in questo modo:

  1. Il primo costituito da S e C1 e tutto il gruppo ruotante intorno ad S. (così S ruota anche su se stessa)
  2. Il secondo costituito da C2 e dai satelliti C2a e C2b ruotanti intorno a C2. Il tutto ruotante intorno ad S.
Come si scrive?
// Si definiscono le masse dei corpi celesti (solo le circonferenze)

  Circonferenza S =new Circonferenza(P1, 10);

  Circonferenza C1 =new Circonferenza(P1.relativo(20, 0), 5);

  Circonferenza C2 =new Circonferenza(P2, 10);

  Circonferenza C2a =new Circonferenza(P2.relativo(20, 0), 5);

  Circonferenza C2b =new Circonferenza(P2.relativo(20, 20), 3);
 
 

// si creano i gruppi assegnando già i colori

  gruppo gr1 =new gruppo(Color.red);

  gruppo gr2 =new gruppo(Color.blue);
 
 

  public void paint(Graphics g) {
 
 

// si aggiungono le entità ai gruppi

    gr1.aggiungi(S);

    gr1.aggiungi(C1);

    gr2.aggiungi(C2);

    gr2.aggiungi(C2a);

    gr2.aggiungi(C2b);
 
 

// si effettuano le rotazioni (se inverse basta il segno "-")
 
 

// si ruota il primo gruppo

    gr1.ruota(S.centro, -Math.PI/16);

// rotazioni interne al secondo gruppo

    C2a.ruota(C2.centro, Math.PI/6);

    C2b.ruota(C2.centro, -Math.PI/8);

// poi si ruota l'intero secondo gruppo intorno ad S appart. al primo gruppo

    gr2.ruota(S.centro, Math.PI/32);

// alla fine si disegna tutto

    gr1.disegna(g);

    gr2.disegna(g);

 }

}

Niente formule o numeri!

In seguito cercheremo di riprendere questo esempio ed applicare al posto del method ruota(), un method, magari, di nome orbita() che calcolerebbe una traiettoria secondo le leggi di Keplero e al posto della Circonferenza una classe Massa che potrà interagire con altre Massa secondo la Legge della Gravitazione Universale di Newton.


Nuovi METHODS:

I methods che seguono sono di arricchimento alla libreria di base che stiamo, di volta in volta, incrementando.

Per la class Punto sono stati introdotti seguenti nuovi methods:

// ritorna il punto di coordinate polari ro, teta relativo al punto in corso

  Punto polare(double ro, double teta) {

    return new Punto(new Punto(x, y), ro, teta);

  }
 
 

// ritorna il punto di coordinate x,y relative al punto in corso

  Punto relativo(double xr, double yr) {

    return new Punto(x+xr, y+yr);

  }
 
 

// ritorna il perimetro del triangolo costituito dal punto in corso ed altri due punti

  double perimetro(Punto Pb, Punto Pc) {

    return new Linea(this, Pb).lunghezza() + new Linea(Pb, Pc).lunghezza() + new Linea(Pc, this).lunghezza();

  }
 
 

// ritorna l'area del triangolo costituito dal punto in corso ed altri due punti

// applicando la formula di Erone applicata al perimetro

  double area(Punto Pb, Punto Pc) {

// calcola il semiperimetro (perimetro/2)

    double p=perimetro(Pb, Pc)/2;

    return Math.sqrt(p*(p- new Linea(this, Pb).lunghezza())*(p- new Linea(Pb, Pc).lunghezza())*(p- new Linea(Pc, this).lunghezza()));

  }
 
 

// verifica se il punto in corso e' interno ad un triangolo

  boolean interno(Punto Pa, Punto Pb, Punto Pc) {

    return (Math.abs(area(Pa, Pb) + area(Pb, Pc) + area(Pc, Pa) - Pa.area(Pb, Pc)) < .0000001);

  }
 
 

// Verifica se il punto in corso e' interno ad una circonferenza data

  boolean interno(Circonferenza C) {

    return new Linea(C.centro, this).lunghezza() < C.raggio;

  }

Per la class Linea sono stati introdotti seguenti nuovi methods:

// angoolo in radianti nel pianoo XY rispetto oall'asse delle ascisse X.

// L'angolo ritornato varia da -PI/2 a 2*PI ed è un fatto importante perchè, di

// solito gli angoli tra rette vengono espressi da -PI/2 a PI/2 senza poter

// rappresentare così se il verso della linea in questione è da P1 a P2 o viceversa

  double angoloXY() {

    double ang=0;

    if ((deltaX()<0) && (deltaY()<0)) ang= Math.atan(m)+Math.PI;

    if ((deltaX()<0) && (deltaY()>=0)) ang= Math.atan(m)+Math.PI;

    if ((deltaX()>=0) && (deltaY()<0)) ang= Math.atan(m);

    if ((deltaX()>=0) && (deltaY()>=0)) ang= Math.atan(m);

    return ang;

  }
 
 

// circonferenza tangente alla linea in corso con centro in un punto qualsiasi

  Circonferenza tangente(Punto centro) {

    return new Circonferenza(centro, perp(centro).lunghezza());

  }

Le frequenti modifiche alle classi già presentate sono dovute al continuo miglioramento della loro struttura. E da tener presente, inoltre, che tali articoli non sono già belli e confezionati, ma vengono scritti dall'autore di volta in volta.
Basta, comunque, farsi il download dei files newGraphics.java e vectors4.java e sostituirli a quelli già downloadati in precedenza.

Buon divertimento!

Domenico De Riso deriso@infomedia.it
 
 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it