14. Eenvoudige animaties

Tekenen van een sinus

Sommige figuren zijn vrij eenvoudig te beschrijven door middel van een wiskundige vergelijking. Misschien heb je bijvoorbeeld wel eens een sinusoide gezien:

Deze figuur is de grafiek van de vergelijking: y = sin x

.

Je kunt deze figuur niet één op één overnemen in een applet. De breedte van deze figuur is namelijk 2π (ongeveer 6,3) terwijl het appletvenster enkele honderden pixels breed zal zijn.

Verder loopt een sinusoide tussen -1 en +1, maar de hoogte van het appletvenster is minstens 100 pixels hoog.

Bovendien bevinden de grootse waarden in een grafiek zich gewoonlijk aan de bovenkant, terwijl in het appletvenster de grootste waarde juist beneden getekend worden.

We zullen dus een drietal zaken moeten omrekenen:

  • De normale uitslag van de sinus (van -1 tot +1) gaan we vermenigvuldigen met de halve hoogte van het venster, zodat de volledige hoogte van het venster benut wordt.
  • Het sinus-traject van 2π vermenigvuldigen we met een bepaalde factor om dit traject naar de breedte van het appletvenster te krijgen.
  • Om de grootse waarden boven te krijgen gaan we de figuur "omklappen". We trekken de  berekende waarden af van de halve vensterhoogte (die we hier uitslag noemen).

 Bovendien worden pixels natuurlijk berekend in hele getallen (type int). Dus moet de sinuswaarde worden gecast met:

y = uitslag - (int)sinus;

Het programma maakt gebruik van de variabelen lastX en lastY, wat staat voor "vorige x" en "vorige y". We tekenen namelijk geen punten, maar lijntjes tussen de punten van de sinusoide. Vandaar dat telkens na het tekenen van een lijntje lastX en lastY de waarde van x en y krijgen.

Het programma ziet er zo uit:

// Een sinusoide getekend in een applet
import java.applet.*;
import java.awt.*;
public class Sinusoide extends Applet {
   public void paint (Graphics g) {
      setBackground (Color.white);
      Dimension d = getSize();
      int x, y, breedte = d.width, hoogte = d.height;
      int uitslag = hoogte / 2;
      int lastX = 0, lastY = uitslag;
      double traject = 2 * Math.PI;
      double factor = traject / breedte;
      for (x = 1; x <= breedte; x++) {
         double sinus = Math.sin (x * factor) * uitslag;
         y = uitslag - (int)sinus;
         g.drawLine (x, y, lastX, lastY);
         lastX = x; lastY = y;
      }
   }
}

En hier is de werkende applet:

   

Meer ruimte en kleur: een vlag

Onze sinusoide is misschien interessant voor wiskundigen. Als vrolijke illustratie is hij wat minder geschikt. We kunnen proberen de figuur wat meer "body" te geven, bijvoorbeeld zoiets:

Waarschijnlijk de beste manier om deze figuur te maken is met de klasse Polygon, die voorkomt in hoofdstuk 13.

Er zijn een aantal manieren zijn om de punten van een polygon te bepalen. Wij doen dat in deze cursus met behulp van twee arrays, een met horizontale en een met verticale coördinaten, wat de volgende vorm heeft:

       g.drawPolygon (horArray, vertArray, arrayLengte);

De punten van één van de drie kleurenbanen zien er ongeveer zo uit:

Deze punten (gedefinieerd in arrays van x- en y-coördinaten) worden door drawPolygon / fillPolygon met elkaar verbonden. De coördinaten van deze punten gaan we bepalen met een sinus-vergelijking.

Het programma maakt gebruik van de methode vulArrays, waar de twee arrays gevuld worden.

int vulArrays (int h[], int v[], int xStart, int br,
                             int yStart, int ho, int step)

De methode is van het type int. We laten hem de lengte afgeven van het deel van de arrays dat gevuld wordt met coördinaten. De arrays zijn namelijk nogal ruim gedeclareerd. We gebruiken ze meestal maar gedeeltelijk.

Behalve de twee arrays, krijgt deze methode ook nog 5 andere variabelen mee, namelijk:

  • xStart: de afstand vanaf de linkerkant van het venster
  • yStart: de afstand vanaf de bovenkant van het venster
  • br: de breedte van een kleurenband (tussen links en rechts)
  • ho: de hoogte van de kleurenband (de "dikte")
  • step: afstand tussen twee punten waartussen een lijntje wordt getekend.

   

Eerste poging tot animatie

Voor we het programma presenteren, gaan we het eerst nog wat uitbreiden. We gaan de golf naar links laten lopen. Dit kunnen we doen door steeds een klein stukje (extra) toe te voegen aan het getal waarvan de sinus wordt genomen.

De methode vulArrays wordt daardoor iets uitgebreid:

int vulArrays (int h[],int v[],int xStart,int br,
             int yStart,int ho,int step,double extra)

We hebben deze regel maar in tweeën gesplitst omdat hij niet op één regel past (voor Java vormt die splitsing geen probleem).

Het wordt nog geen echte animatie, maar telkens als er op een knop wordt geklikt laten we een nieuw beeld tekenen, zodat we een soort vertraagd filmpje krijgen.

Het programma ziet er zo uit:

// Een vlag die opschuift door het klikken op knop
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
public class Vlag1 extends Applet implements ActionListener {
Button knop = new Button ("klik");
double erbij = 0;

 public void init () {
   setBackground (Color.black);
   add (knop);
   knop.addActionListener (this);
 }

 public void paint (Graphics g) {
   Dimension d = getSize();
   int hoogte = d.height, breedte = d.width;
   int x[ ] = new int[2*breedte], y[ ] = new int[2*breedte];
   for (int kleur=0; kleur<3; kleur++) {
     switch(kleur) {
       case 0: g.setColor (Color.red); break;
       case 1: g.setColor (Color.white); break;
       case 2: g.setColor (Color.blue);
     }
     // De volgende twee regels zijn eigenlijk één:
     int size = vulArrays (x, y, breedte/8, 3 * breedte/4,
                  (1 + kleur) * hoogte/5, hoogte/5, 5, erbij);
     g.fillPolygon (x, y, size);
   }
 }

 // De volgende 2 regels zijn eigenlijk één:
 int vulArrays (int h[ ], int v[ ], int xStart, int br,
            int yStart, int ho, int step, double extra) {
   int teller = 0;
   // bovenste lijn van de band:
   for (int i=xStart; (i<=xStart+br); i += step) {
     h[teller] = i;
     v[teller++] = yStart+(int)(ho*Math.sin(i/(0.2*br)+extra));
   } 
   int n = teller-1;
   // De onderste lijn hoeven we niet te berekenen
   // De waarden zijn dezelfde als de bovenste lijn + ho
    for (int i=h[teller-1]; i>=xStart; i-=step) {
      h[teller] = i;
      v[teller++] = v[n--]+ho;
    }
    return teller;
}

 public void actionPerformed (ActionEvent evt) {
   erbij += 0.7;
   repaint();
 }
}

En zo ziet de applet eruit:

   

Een betere animatie met een "thread"

Als we in het laatste programma de applet voortdurend nieuwe beelden laten tekenen zonder dat er op een knop hoeft te worden geklikt, zal de animatie waarschijnlijk veel te snel gaan.

Maar in dat geval is er nog een tweede probleem: de applet doet netjes wat we hem opdragen: hij blijft maar doorgaan en is lastig te beëindigen. Beide problemen gaan we oplossen met behulp van zogenaamde threads.

Moderne computersystemen (zoals Windows of Linux) kunnen meer taken door elkaar heen doen. Je kunt een bestand van Internet downloaden terwijl je aan het tekstverwerken bent. Dit noemt men wel multitasking.

In een multitasking-systeem worden de taken verricht door verschillende programma's, die verschillende stukken van het geheugen gebruiken.

Een applet kan niet multitasken, maar wel verschillende taken uitvoeren binnen dezelfde geheugenruimte, dat soort afzonderlijke taken noemt men threads. Men noemt dat ook wel multi-threading.

Als we geen speciale maatregelen nemen, zal een applet maar één taak uitvoeren. Bij animaties moeten we aan het systeem duidelijk maken dat het tekenwerk een aparte thread is. Dit kunnen we doen door middel van de klasse Thread.

Van de klasse Thread declareren we eerst een object (bijvoorbeeld myThread) met:

    Thread myThread;

Daarna kunnen we myThread initialiseren. Dit doen we in de methode start van de applet, waarvan we een nieuwe versie maken.  Dat heet overriding (zie ook deel 11) Zoals we hebben gezien in deel 5, gaat de methode start elke keer lopen dat de applet wordt opgestart.

public void start( ) {
    if (myThread == null) {
        myThread = new Thread (this);
        myThread.start ( );
    }
}

De regel myThread = new Thread (this) creëert een volledige thread. Alleen moet deze thread nog gaan lopen. Daartoe wordt de methode run van de thread aangeroepen met myThread.start ( ).

Een applet heeft echter geen methode run().

Die methode kunnen we introduceren via de interface Runnable. Zoals we eerder (in deel 11) zagen, kun je met een interface methoden introduceren die een klasse zelf niet bezit.

Deze methode run( ) laten we er zo uitzien:

public void run () {
    for (; ;) {
        try {
            repaint();
            myThread.sleep (vertraging);
            extra += 0.2;
        }
        catch (InterruptedException e) { };
    }
}

De expressie for (; ;) is een for-lus met niet ingevulde velden, dat wil zeggen: hij blijft onbeperkt doorlopen.

Verder kan de lus onderbroken worden. Vandaar de try-catch-constructie ( zie ook hoofdstuk 10).

We laten telkens een nieuw beeld maken met repaint(), waarna de uitvoering van de thread enige tijd tot stilstand komt door de methode sleep, die 60/1000 seconde wacht. in die tijd kunnen eventueel andere taken verricht worden.

Daarna zorgt de verhoging extra += 0.2; ervoor dat de sinusoide in de volgende tekening iets verschoven is.

Bij het beëindigen van de thread ten slotte wordt de methode stop() aangeroepen, die ervoor zorgt dat zolang de thread niet actief is, hij geen beroep meer doet op het systeem.

Hier is het hele programma:

// Een wapperende vlag
import java.applet.*;
import java.awt.*;
public class Vlag2 extends Applet implements Runnable {
Thread myThread;
int vertraging = 60;
double extra;

 public void init () {
   setBackground (Color.black);
 }

 public void run () {
   for (;;) {
     try {
       repaint();
       myThread.sleep (vertraging);
       extra +=0.2;
     }
     catch (InterruptedException e) {};
   }
 }

 public void start () {
   if (myThread == null) {
     myThread = new Thread (this);
     myThread.start();
   }
 }

 public void stop () {
   if (myThread != null) {
     myThread.stop();
     myThread = null;
   }
 }

 public void paint (Graphics g) {
   Dimension d = getSize();
   int hoogte = d.height, breedte = d.width;
   int x[] = new int[2*breedte], y[] = new int[2*breedte];
   for (int kleur=0; kleur<3; kleur++) {
     switch(kleur) {
       case 0: g.setColor (Color.red); break;
       case 1: g.setColor (Color.white); break;
       case 2: g.setColor (Color.blue);
     }
     int size = vulArrays (x, y, breedte/8, 3*breedte/4,
              (1 + kleur) * hoogte/5, hoogte/5, 5, extra);
     g.fillPolygon (x, y, size);
   }
 }

 int vulArrays (int h[], int v[], int xStart, int br,
           int yStart, int ho, int step, double extra) {
   int teller = 0;
   for (int i=xStart; (i<=xStart+br); i += step) {
     h[teller] = i;
     v[teller++] = yStart+(int)(ho*Math.sin(i/(0.3*br)+extra));
   } 
   int n = teller-1;
   for (int i=h[teller-1]; i>=xStart; i-=step) {
     h[teller] = i;
     v[teller++] = v[n--]+ho;
   }
   return teller;
 }
}

En dit is hoe onze applet eruitziet:

   

"Dubbele buffering"

Waarschijnlijk heb je gemerkt dat onze laatste animatie er nog niet helemaal perfect uitziet. Hij oogt wat flikkerend.

We kunnen het beeld ergens in het geheugen opbouwen en dit geheugenbeeld in één keer naar het beeldscherm-geheugen schrijven, wat een veel rustiger beeld geeft.

We gaan een beeld in het geheugen declareren met:

Image beeld;

Ook hebben we daar een grafische omgeving (gBuf) bij nodig om straks in dat beeld te kunnen tekenen.

Graphics gBuf;

Dit waren alleen nog maar de declaraties. In paint gaan we ze allebei initialiseren:

if (beeld == null) {
   beeld = createImage (breedte, hoogte);
   gBuf = beeld.getGraphics();
}

Een van de oorzaken van de flikkering is dat we met repaint() het beeld geheel "schoonmaken". Dat gaan we nu anders doen. Zoals we al zagen in hoofdstuk 11, kunnen we de methode update() overriden, zodat die niet langer het beeldscherm leegmaakt.

public void update (Graphics g) {
   paint(g);
}

Wel vullen we het geheugenbeeld met zwart door er een zwarte rechthoek in te tekenen:

     gBuf.setColor (Color.black);
     gBuf.fillRect (0, 0, breedte, hoogte);

Ook de rest wordt natuurlijk niet op beeldscherm gezet, maar eerst in het tijdelijke geheugenbeeld getekend.

       gBuf.fillPolygon (x, y, Size);

Pas als het beeld in het geheugen geheel klaar is, zetten we het in één keer naar het beeldscherm-geheugen met:

if (beeld != null)
   g.drawImage (beeld, 0, 0, null);

Hier is dan het volledige programma:

// Een wapperende vlag met "dubbele buffering"
import java.applet.*;
import java.awt.*;
public class Vlag3 extends Applet implements Runnable { private Thread thread;
int vertraging = 60;
double extra = 0;
Image beeld;
Graphics gBuf;

public void run () {
   for (;;) {
      try {
         repaint();
         thread.sleep (vertraging);
         extra += 0.2;
      }
      catch (InterruptedException e) { };
   }
}

public void start () {
   if (thread == null) {
      thread = new Thread (this);
      thread.start();
   }
}

public void stop () {
   if (thread != null) {
      thread.stop();
      thread = null;
  }
}

public void paint (Graphics g) {
   Dimension d = getSize();
   int breedte = d.width, hoogte = d.height;
   if (beeld == null) {
      beeld = createImage (breedte, hoogte);
      gBuf = beeld.getGraphics();
   }
   gBuf.setColor (Color.black);
   gBuf.fillRect (0,0,breedte,hoogte);
   int x[] = new int[2*breedte], y[] = new int[2*breedte];
   for (int kleur=0; kleur<3; kleur++) {
      switch(kleur) {
         case 0: gBuf.setColor (Color.red); break;
         case 1: gBuf.setColor (Color.white); break;
         case 2: gBuf.setColor (Color.blue);
      }
      int size = vulVlagArray
      (x, y, breedte/8, 3*breedte/4, (1+kleur)*hoogte/5, hoogte/5, 5, extra);
      gBuf.fillPolygon (x, y, size);
   }
   if (beeld != null)
      g.drawImage (beeld, 0, 0, null);
   }
   int vulVlagArray
   (int h[], int v[], int xStart, int br, int yStart, int ho, int step, double extra) {
   int teller = 0;
   for (int i=xStart; (i<=xStart+br); i += step) {
      h[teller] = i;
      v[teller++] = yStart+(int)(ho*Math.sin(i/(0.3*br)+extra));
   } 
   int n = teller-1;
   for (int i=h[teller-1]; i>=xStart; i-=step) {
      h[teller] = i;
      v[teller++] = v[n--]+ho;
   }
   return teller;
}

 public void update (Graphics g) {
   paint(g);
 }
}

   

En zo ziet onze animatie er tenslotte uit:

   

Opdracht 14.1

In hoofdstuk 6 hebben we een cirkel getekend die pulseert als we klikken op een knop. Maak een (niet flikkerende) animatie van een pulserende cirkel.

Opdracht 14.2

Maak een lichtkrant waarin een tekst "Lang leve Java" (of een andere tekst van 10-20 tekens) in grote letters continu van rechts naar links over het beeldscherm beweegt.

Opdracht 14.3

Breng in 14.2 twee knoppen aan, waarmee je kunt kiezen om de tekst naar links of naar rechts te laten bewegen.

(c) 2003-2008, Thomas J.H.Luif