Week 1

2D Computer graphics using Java2D

cc

This material is licensed under the Creative Commons BY-NC-SA license, which means that you can use it and distribute it freely so long as you do not erase the names of the original authors. If you do changes in the material and want to distribute this altered version of the material, you have to license it with a similar free license. The use of the material for commercial use is prohibited without a separate agreement.

Authors: Johan Talboom, Etiënne Goossens

The course is maintained by Technische Informatica Breda

Java2D

Java2D is een verzameling van klassen om binnen java met 2D graphics te werken. Deze API bestaat uit een aantal klassen in de java.awt.graphics namespace, en veel functionaliteiten zijn gemakkelijk te benaderen via de Graphics2D klasse. Een Graphics2D object slaat intern een aantal attributen op die bepalen hoe getekend gaat worden, zoals welke kleur gebruikt gaat worden. Daarnaast is er een uitgebreide Shape-library die gebruikt kan worden om verschillende vormen te combineren. Deze lessen gaan dieper in op het gebruik van Java2D, maar er is online natuurlijk ook veel te vinden.

Er wordt gebruik gemaakt van Java2D in combinatie met JavaFX, maar de concepten die behandeld worden zijn ook gemakkelijk toe te passen binnen JavaFX. Daarnaast zijn deze concepten ook toepasbaar binnen andere omgevingen, zoals de SurfaceView binnen android, of het canvas binnen C#.net

Alle code voor deze week kun je vinden op github, in het Java2D_week1 project.

Makkelijk gebruiken

Java2D is te gebruiken via een Graphics2D object. Dit object komt intern uit java en JavaFX, en kan gebruikt worden om op verschillende dingen te tekenen, zoals een Canvas. Om op een Canvas te tekenen kunnen we de volgende code gebruiken:

public class HelloJava2D extends Application {

    private Canvas canvas;
    @Override
    public void start(Stage primaryStage) throws Exception {
        this.canvas = new Canvas(640, 480);
        draw(new FXGraphics2D(canvas.getGraphicsContext2D()));
        primaryStage.setScene(new Scene(new Group(canvas)));
        primaryStage.setTitle("Hello Java 2D");
        primaryStage.show();
    }

    public void draw(FXGraphics2D graphics) {
        // teken
    }

    public static void main(String[] args) {
        launch(HelloJava2D.class);
    }
}

helloworld

De bovenstaande code maakt gebruik een standaard JavaFX applicatie implementatie. In plaats van het aanmaken van een component dat we al kennen uit OGP1 zoals Button, maken we nu gebruik van een andere soort JavaFX component, namelijk Canvas. Voor het omzetten van JavaFX code naar 2D Graphics, wordt er gebruik gemaakt van een externe library namelijk FXGraphics2D. Deze library moet je toevoegen als library in IntelliJ. De methode draw wordt aangeroepen als de applicatie opstart. In deze methode kunnen alle graphics berekeningen uitgevoerd worden.


Lijnen

line

Java2D werkt met een Carthesisch Coördinatenstelsel. In het kort betekent dit dat er gebruik wordt gemaakt van een X- en een Y-as. De oorsprong van het coördinatenstelsel ligt standaard linksboven in het venster, waarbij de Y-as naar beneden loopt. Dit is dus gespiegeld ten opzichte van het wiskundige assenstelsel dat normaal gebruikt wordt om bijvoorbeeld grafieken te tekenen. Standaard is de eenheid een pixel, dit betekent dus dat punt (200,100) 200 pixels naar rechts, en 100 pixels naar beneden ligt ten opzichte van de linkerbovenhoek. Dit is een standaard die in veel grafische APIs gebruikt wordt.

Om een lijn te tekenen kan de draw() methode in het FXGraphics2D object gebruikt worden. Deze methode wil een Shape als parameter, een Line2D is een voorbeeld van een Shape. De Line2D klasse heeft geen public constructor, we zullen gebruik moeten maken van een van de subklassen van Line2D, zoals Line2D.Double. Deze kunnen we direct aan de draw methode meegeven, voor de volgende code.

public class HelloLine extends Application {

    private Canvas canvas;
    @Override
    public void start(Stage primaryStage) throws Exception {
        this.canvas = new Canvas(640, 480);
        draw(new FXGraphics2D(canvas.getGraphicsContext2D()));
        primaryStage.setScene(new Scene(new Group(canvas)));
        primaryStage.setTitle("Hello Line");
        primaryStage.show();
    }

    public void draw(FXGraphics2D graphics) {
        graphics.draw(new Line2D.Double(200, 100, 500, 200));
    }

    public static void main(String[] args) {
        launch(HelloLine.class);
    }
}

Deze code zal een lijn tekenen van de coördinaat (200,100) naar (500,200).

Opgave 1-1. Een huis

Schrijf een programma dat een huis tekent. Het huis bestaat uit een basis met een puntdak, en een deur. Dit is een lijntekening en is dus een simpele vorm om te tekenen met lijnen.

huis

Je kunt eerst het huis tekenen op ruitjespapier om gemakkelijk alle coördinaten af te leiden


Transformaties

Het standaard coördinatenstelsel in java2D heeft de oorsprong in de linkerbovenhoek van het scherm zitten, waarbij de Y-as naar beneden gaat. Soms is dit echter onhandig met het tekenen, en is het bijvoorbeeld handiger om dit assenstelsel aan te kunnen passen. Dit kan bij computer graphics met 3 verschillende acties.

Combineren van transformaties

Door verschillende transformaties achter elkaar uit te voeren kunnen we deze transformaties combineren. Hierbij is de volgorde van groot belang. Het is bijvoorbeeld belangrijk om eerst de oorsprong goed te zetten, voordat er om de oorsprong gedraaid wordt. Door de wiskunde achter het combineren van deze transformaties (Zie volgende week), staan deze transformaties in de omgedraaide volgorde. Je moet de regels code dus van onder lezen. Om bijvoorbeeld de oorsprong van het coördinatenstelsel in het midden van het venster te zetten en de Y-as om te draaien zodat Y positief omhoog gaat, kunnen we de volgende code gebruiken:

graphics.translate(this.canvas.getWidth()/2, this.canvas.getHeight()/2);
graphics.scale(1,-1);

Parametrische vergelijkingen

Een van de dingen die we met lijnen kunnen maken zijn grafieken en parametrische vergelijkingen. Een simpele grafiek zou bijvoorbeeld kunnen zijn y = sin(x). Deze grafiek kunnen we tekenen door een x-variabele te laten lopen, en hierbij de y-coördinaat te berekenen. Hierna zetten we een puntje op locatie x,y. Het probleem hierbij is echter, dat de waarden voor y tussen -1 en 1 liggen. Hiervoor moeten we dus gaan schalen. Daarnaast is het mooier om de continuïteit van de grafiek te tekenen door lijnen te tekenen tussen het huidige punt en het vorige punt. Hierdoor krijgen we 1 lijn. Hiervoor kunnen we de volgende code gebruiken:

public void draw(FXGraphics2D graphics) {

    graphics.translate(this.canvas.getWidth()/2, this.canvas.getHeight()/2);
    graphics.scale( 50, -50);

    graphics.setColor(Color.red);
    graphics.drawLine(0,0,1000,0);
    graphics.setColor(Color.green);
    graphics.drawLine(0,0,0,1000);
    graphics.setColor(Color.black);

    double resolution = 0.1;
    double lastY = Math.sin(-10);

    for(double x = -10; x < 10; x += resolution)
    {
        float y = (float)Math.sin(x);
        graphics.draw(new Line2D.Double(x, y, x-resolution, lastY));
        lastY = y;
    }
}

probleem

Deze code schaalt het scherm met een factor 50, tekent een assenstelsel, en maakt hierna de grafiek y = sin(x), waarbij x van -10 tot 10 loopt. Bij het uitvoeren van deze code, zien we echter een probleem ontstaan, de lijnen zijn ook opgeschaald en erg dik geworden. Dit is op 2 manieren op te lossen; door de lijndikte kleiner te maken tot 1/50, of door de schaling niet te doen met de scale methode maar door alleen de coördinaten te vermenigvuldigen met een schalingsfactor. De tweede manier heeft in dit geval de voorkeur. Dit levert de volgende code op:

public void draw(FXGraphics2D graphics) {

    graphics.translate(getWidth()/2, getHeight()/2);
    graphics.scale( 1, -1);

    graphics.setColor(Color.red);
    graphics.drawLine(0,0,1000,0);
    graphics.setColor(Color.green);
    graphics.drawLine(0,0,0,1000);
    graphics.setColor(Color.black);

    double resolution = 0.1;
    double scale = 50.0;
    double lastY = Math.sin(-10);

    for(double x = -10; x < 10; x += resolution)
    {
        float y = (float)Math.sin(x);
        graphics.draw(new Line2D.Double(x*scale, y*scale, (x-resolution)*scale, lastY*scale));
        lastY = y;
    }
}

Let hierbij op dat de schaling pas bij het tekenen toegepast wordt, niet al in de berekeningen. Nu krijgen we wel het gewenste resultaat. Het is nu triviaal om de schaal aan te passen, of een andere formule te gebruiken. Let wel op de resolution variabele, deze geeft de tussenstap tussen pixels aan. Als deze te klein is, wordt het tekenen erg langzaam, te groot en de grafiek berekent niet voor iedere pixel een juiste coördinaat

Opgave 1-2. Grafiek

Schrijf een programma dat de grafiek Y = X³ tekent. Let op de schaalverdeling van de assen, en zorg dat de grafiek goed in beeld te zien is. Het kan in dit geval verstandig zijn om de schaalverdeling op de X en Y as anders te nemen.

Opgave 1-3. Spiraal

Schrijf een programma dat een spiraal tekent. Voor een spiraal kun je de formules gebruiken in het poolcoördinaten-stelsel. Door de formule Ø = n × r te gebruiken, krijg je een spiraalfiguur. hierin is n een constante de afstand tussen de spiraal aan te passen, en r de afstand tussen de oorsprong en het punt. Je kunt bijvoorbeeld n = 1 voor nemen. Om hierna van poolcoördinaten naar carthesische te gaan kun je de sinus en cosinus gebruiken:

x = r × cos(Ø)
y = r × sin(Ø)

De formule Ø = n × r omgeschreven wordt dus

x = n × Ø × cos(Ø)
y = n × Ø × sin(Ø)

waarbij n gebruikt kan worden om de dichtheid van de spiraal in te stellen


Kleuren

De Graphics klasse slaat op met welke kleur getekend gaat worden. Deze kleur kun je veranderen waarna alle opvolgende teken-commandos met deze kleur getekend zullen worden. De kleur kun je aanpassen met de setColor(Color color) methode. Kleuren kunnen op verschillende manieren aangemaakt worden via de Color klasse: new Color(float r, float g, float b)

Maakt een kleur aan met rood, groen en blauw waarden. De parameters liggen tussen 0 en 1. new Color(1.0f, 1.0f, 1.0f) geeft dus een witte kleur.

Door nu een kleur aan te maken en deze in het Graphics2D object te zetten, kun je bijvoorbeeld lijnen tekenen met deze kleur. Door steeds nieuwe kleuren te maken kunnen we verschillende lijnen tekenen met verschillende kleuren:

public class HelloJava2D extends Application {

    private Canvas canvas;
    @Override
    public void start(Stage primaryStage) throws Exception {
        this.canvas = new Canvas(640, 480);
        draw(new FXGraphics2D(canvas.getGraphicsContext2D()));
        primaryStage.setScene(new Scene(new Group(canvas)));
        primaryStage.setTitle("Hello Java 2D");
        primaryStage.show();
    }

    public void draw(FXGraphics2D g) {
        for(int i = 0; i < 500; i++) {
            g2d.setColor(Color.getHSBColor(i/500.0f, 1, 1));
            g2d.drawLine(i, 10, i, 100);
        }
    }

    public static void main(String[] args) {
        launch(HelloJava2D.class);
    }
}

Deze code zal dus 500 lijnen tekenen, met ieder een andere kleur op basis van het HSB model. De hue verloopt hierbij van 0° tot 360°, waardoor je het hele kleurenspectrum te zien krijgt.

Opgave 1-4. Regenboog

Schrijf een applicatie die een regenboog tekent, waar aan de linkerkant van de regenboog rood zit, en de rechterkant ook weer rood, met alle kleuren van het hue-spectrum ertussenin.

Om dit aan te pakken, moet je de coördinaten op de binnenste en de buitenste boog berekenen en hiertussen een lijn tekenen met een bepaalde kleur. Het is niet heel erg als er witte lijnen tussen sommige van de segmenten zitten, doordat je lijnen niet dicht genoeg op elkaar zitten. Om deze punten te berekenen kun je weer gebruik maken van poolcoordinaten en carthesische coördinaten. Je kunt hiervoor de onderstaande code gebruiken. Hierin zijn radiusBinnen en radiusBuiten de binnen en buitenradius (afmeting) van de regenboog, en hoek de hoek in de boog die je wilt berekenen.

float x1 = radiusBinnen * Math.cos(hoek);
float y1 = radiusBinnen * Math.sin(hoek);
float x2 = radiusBuiten * Math.cos(hoek);
float y2 = radiusBuiten * Math.sin(hoek);

Voorbeeldoutput:

rainbow


Eindopdracht week 1

Opgave 1-5. Spirograaf

spirograaf

Een spirograaf is een instrument dat gebruikt kan worden om patronen te tekenen. Het werkt door een tandwiel dat binnen een ander tandwiel draait. Voor meer informatie zie spirograaf.

Maak een java-applicatie om verschillende spirograaf figuren te tekenen. Dit is eigenlijk vergelijkbaar met een parametrische vergelijking.

Voor inspiratie, zie ook nathanfriend.io/inspirograph/. Het gaat dus niet om de animatie, maar alleen om de uiteindelijke figuur. Zorg dat je applicatie configureerbaar is om verschillende (of misschien zelfs willekeurige) figuren te tekenen. Maak ook gebruik van kleuren.

Voor het tekenen van de spirograaf kun je gebruik maken van de generieke formules

x = a × cos(b × φ) + c × cos(d × φ)
y = a × sin(b × φ) + c × sin(d × φ)

Dit zijn versimpelde formules, op wikipedia staan net iets andere formules, deze kun je ook gebruiken maar zijn iets complexer om te implementeren, maar wel gemakkelijker te configureren. Door de variabelen a,b,c en d aan te passen kun je een andere figuur te maken.

Einde van week 1