Week 2

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

Shapes

Naast het tekenen van lijnen, kunnen we ook allerlei andere vormen tekenen. Al deze vormen erven over van de Shape klasse en kunnen we met dezelfde draw methode tekenen. In de Java2D library zitten al een aantal klassen die deze shape klasse implementeren.

shapes

Je kunt deze vormen tekenen met de Graphics.draw(Shape shape) of de Graphics.fill(Shape shape) methoden. De draw methode tekent een lijn in de vorm van de Shape die is opgegeven en de fill methode vult de vorm op. Deze lijn of opvulling kun je een kleur geven met de ```setColor` methode, maar kunnen we ook een andere opvulling geven. Hierover meer in de hoofdstukken over Strokes en Paints.

Daarnaast kun je aan shapes nog een aantal vragen stellen. Zo kun je bijvoorbeeld kijken of een punt binnen de shape is met de contains(Point2D point) methode. Deze methode kun je bijvoorbeeld gebruiken om te kijken of de gebruiker op een vorm heeft geklikt. Daarnaast kun je ook een bounding-rectangle opvragen met de getBounds() methode. Dit is de rechthoek die de volledige shape omlijnt.


Paths

Een speciale shape is een Path2D. Een Path is een combinatie van lijnen die gebruikt kunnen worden veel verschillende vormen te maken. De Path klasse wordt geïmplementeerd door de GeneralPath klasse, dus deze kunnen we gebruiken. Een generalpath werkt als een soort pen. Je kunt de pen over het canvas bewegen om zo een vorm te tekenen.

GeneralPath

public void draw(FXGraphics2D g2d) {

    GeneralPath path = new GeneralPath();
    path.moveTo(100, 100);
    path.lineTo(200,100);
    path.lineTo(100,200);
    path.closePath();

    g2d.setColor(Color.green);
    g2d.fill(path);
    g2d.setColor(Color.black);
    g2d.draw(path);
}

De GeneralPath klasse heeft de volgende methoden:

Om een vorm te vullen, moet je deze altijd afsluiten met closePath, anders kan de inkt weglekken en zou het hele scherm gevuld worden.

Het is niet altijd voordehandliggend welk gedeelte van de shape gevuld wordt, zeker als de lijnen van het pad elkaar kruisen. Bekijk de volgende voorbeeldcode met de lijnen die hieruit komen:

myShape

myShape = new GeneralPath();
myShape.moveTo(-2f, 0f);
myShape.quadTo(0f, 2f, 2f, 0f);
myShape.quadTo(0f, -2f, -2f, 0f);
myShape.moveTo(-1f, 0.5f);
myShape.lineTo(-1f, -0.5f);
myShape.lineTo(1f, 0.5f);
myShape.lineTo(1f, -0.5f);
myShape.closePath();

Als we deze vorm gaan opvullen, krijgen we een probleem, welke onderdelen worden er nu gevuld? In java kunnen we kiezen uit 2 verschillende manieren van vullen:

Opgave 2-1. De maan

Maak een programma die een maan tekent door middel van een generalpath en door middel van CSG.

moon


Areas en Constructive Solid Geometry

Een andere manier van ‘t maken van vormen is Constructive Solid Geometry. Dit is in het kort het combineren van 2 vormen om een nieuwe vorm te maken. Dit kunnen we doen met 3 operaties:

Vereniging (add)

add

Door de vereniging te nemen van 2 vormen, krijg je een nieuwe vorm met een combinatie van beide vormen. Er komt 1 nieuwe vorm uit, en de lijnstukken die tussen de 2 vormen in zitten, vallen weg.

Verschil (subtract)

subtract

Geeft de eerste vorm, waar de tweede vorm als een hap uitgenomen is. Dit kan gaten opleveren in de vorm. Let op de volgorde van de vormen, het verschil tussen vorm A en B, en B en A is anders.

Doorsnede (intersect)

intersect

Geeft alleen de ruimte die overlapt in beide vormen.

Exclusieve of (xor)

xor

Geeft alleen de ruimte die of in de een, of in de andere vorm zit, maar niet allebei. Is gelijk aan Verschil(Vereniging(A, B), Doorsnede(A, B))

Gebruik van CSG

In java werkt CSG door middel van de Area klasse. Deze klasse is ook een Shape, en kan gemaakt worden op basis van een shape in de constructor. Door een bestaande Shape in een Area te encapsuleren kun je dus gemakkelijk CSG operaties hierop toepassen, en het resultaat kun je meteen tekenen.

public void draw(FXGraphics2D g2d) {

    Area a = new Area(new Ellipse2D.Double(0,0,100,100));
    Area b = new Area(new Ellipse2D.Double(50,0,100,100));

    Area added = new Area(a);
    added.add(b);

    Area sub = new Area(a);
    sub.subtract(b);

    Area intersect = new Area(a);
    intersect.intersect(b);

    Area xor = new Area(a);
    xor.exclusiveOr(b);

    g2d.translate(25,25);

    g2d.setColor(Color.lightGray);
    g2d.fill(added);
    g2d.setColor(Color.black);
    g2d.draw(a);
    g2d.draw(b);

    g2d.translate(0,150);
    g2d.setColor(Color.lightGray);
    g2d.fill(sub);
    g2d.setColor(Color.black);
    g2d.draw(a);
    g2d.draw(b);

    g2d.translate(0,150);
    g2d.setColor(Color.lightGray);
    g2d.fill(intersect);
    g2d.setColor(Color.black);
    g2d.draw(a);
    g2d.draw(b);

    g2d.translate(0,150);
    g2d.setColor(Color.lightGray);
    g2d.fill(xor);
    g2d.setColor(Color.black);
    g2d.draw(a);
    g2d.draw(b);
}

Het is natuurlijk ook mogelijk om verschillende operaties achter elkaar uit te voeren op een Area, bijvoorbeeld door er een aantal keer verschillende stukjes af te hakken.

Je kunt CSG gebruiken om nieuwe vormen te maken, maar je kunt er nog meer mee doen:

Opgave 2-2. Yin-Yang

Maak een applicatie die het yin-yang symbool tekent door middel van een generalpath en door middel van CSG.

yin-yang


Strokes

De Graphics2D.draw() methode tekent standaard een simpele lijn. Deze lijn noemen we een ‘Stroke’. De stroke kun je instellen met de setStroke(Stroke newStroke) methode. Een van de stroke-klassen is de BasicStroke. Met deze stroke kun je de dikte instellen, het soort afrondingen aan het einde van de lijn en hoe de knikken in de lijnstukken getekend worden. Daarnaast kun je ook een streeppatroon instellen. Dit kan allemaal met de BasicStroke(float width, int cap, int join, float miterlimit, float[] dash, float dash_phase) constructor, maar het is ook mogelijk de laatste opties weg te laten met bijvoorbeeld de BasicStroke(float width, int cap, int join) constructor. De width geeft de breedte van de lijnen aan. De cap en join geven de eindes en tussenstukken van lijnstukken aan.

Caps and Joins

Stroke s = new BasicStroke(4.0f,
                             BasicStroke.JOIN_ROUND,
                             BasicStroke.CAP_ROUND);

Deze waarden zijn integer constanten in de BasicStroke klasse, en kunnen gebruikt worden in de constructor. In het geval van de JOIN_MITER, kun je ook een extra parameter aan de constructor meegeven die de limiet van de miter-join aangeeft. Dit limiet is de diagonale afstand van de miter, dus de afstand tussen de binnenste en buitenste hoek. Standaard staat deze op 10.0f.

dashes Daarnaast is het ook mogelijk een streep-patroon mee te geven. Deze patronen kun je in een array doorgeven en je kunt een verschuiving aangeven in een parameter. In de array staat de lengte van de gekleurde gebieden en hierna de lengte van de nietgekleurde gebieden. Het is dus eigenlijk altijd een array met een even aantal elementen. Door de verschuiving kun je aangeven waar het patroon begint.


Paints

De Graphics2D.fill() methode kan een vorm invullen. Standaard is dit met een enkele kleur, maar dit kun je instellen met de Graphics2D.setPaint(Paint paint) methode. Hier kun je een paint object meegeven, dat bepaalt hoe de vorm gevuld wordt.

Color

De color klasse implementeerd ook het Paint interface en kun je gebruiken om een Shape in een kleur in te kleuren. De kleur kun je aanmaken met alle constructoren van Color, op dezelfde manier als setColor.

GradientPaint

Gradientpaint

Een GradientPaint maak je aan met twee punten en twee kleuren. Java zal dan lineair interpoleren over een lijn die tussen deze twee punten loopt. Je kunt in de constructor ook aangeven of deze gradient cyclisch of asyclisch is. Een cyclische gradient zal zichzelf blijven herhalen na de twee punten en een acyclische gradient blijft dezelfde kleur na deze twee punten.

LinearGradientPaint

LinearGradientPaint

Een LinearGradientPaint heeft twee punten en meerdere kleuren. Daarnaast kun je aangeven hoeveel een bepaalde kleur in verhouding gebruikt moet worden. Je kunt hier dus hetzelfde mee als met eenGradientpaint, maar ook meer, door meerdere kleuren toe te voegen. De standaard constructor LinearGradientPaint(float startX, float startY, float endX, float endY, float[] fractions, Color[] colors) zal een acyclische gradientpaint maken die van het startpunt tot het eindpunt de kleuren lineair interpoleert, en daarna een constante kleur blijft. De fractions zijn hierbij een lijst met floating point waarden tussen 0 en 1, in oplopende volgorde, die aangeven waar in de gradient de kleur zich bevind. 0 is (startX,startY), 1 is (endX,endY), 0.5 is precies halverwege.

RadialGradientPaint

RadialGradientPaint

Een RadialGradientPaint is een gradientpaint die in een cirkel vanuit een punt van kleur verloopt. Hierbij kan nog een tweede focuspunt aangegeven worden om het middelpunt te verschuiven. Ook wordt hier een array aan kleuren meegegeven zoals bij een LinearGradientPaint. Om een RadialGradientPaint te gebruiken kun je de RadialGradientPaint(Point2D center, float radius, Point2D focus, float[] fractions, Color[] colors, MultipleGradientPaint.CycleMethod cycleMethod) constructor aanroepen, waar je een centrum, straal en focuspunt kan opgeven, en daarnaast de lijst met kleuren en fractions om de kleuren aan te geven. Ook kun je een cycleMethod opgeven waarmee aangegeven wordt hoe het patroon herhaald wordt.

TexturePaint

Een TexturePaint maakt gebruik van een afbeelding om een shape op te vullen. Deze afbeelding is in de vorm van een BufferedImage, en wordt herhaald. In de constructor van de TexturePaint moet dus een BufferedImage meegeven worden en ook een Rectangle die aangeeft waar de afbeelding geplaatst moet worden. Om een afbeelding in te laden, kunnen we de ImageIO klasse gebruiken. Let erop dat je deze afbeelding maar 1x inlaad, dus in de constructor van je klasse, en niet bij het tekenen.

Stage stage;
@Override
public void start(Stage primaryStage) throws Exception {
    stage = primaryStage;
    javafx.scene.canvas.Canvas canvas = new Canvas(1920, 1080);
    draw(new FXGraphics2D(canvas.getGraphicsContext2D()));
    primaryStage.setScene(new Scene(new Group(canvas)));
    primaryStage.setTitle("Hello Paths");
    primaryStage.show();
}

public void draw(FXGraphics2D g2d) {
    BufferedImage texture;
    try {
        texture = ImageIO.read(getClass().getResource("/images/texture.jpg"));
    } catch (IOException | IllegalArgumentException e) {
        e.printStackTrace();
    }
    g2d.setPaint(new TexturePaint(texture, new Rectangle2D.Double(0,0,400,400)));
    g2d.fill(new Rectangle2D.Double(0,0,1920, 1080));
}

Textures inladen in IntelliJ

Deze afbeeldingen kunnen in IntelliJ gezet worden. In IntelliJ kunnen meerdere mappen gemarkeerd worden als resource. Deze mappen worden hierna in het buildproces meegenomen, en worden ook in de .jar files gezet. Het aanmaken van een resource map bestaat uit 2 stappen. Eerst moet een map aangemaakt worden (met de afbeelding erin), en deze kan hierna gemarkeerd worden als resources root.

IntelliJ IntelliJ

In dit voorbeeld is een map ‘resources’, met hierin een map ‘images’. Door nu de resources map als root te markeren, kunnen we in code alle subfolders openen, relatief ten opzichte van het project. De bovenstaande texture is dus te openen met ImageIO.read(getClass().getResource("/images/texture.jpg")).

Opgave 2-3. Kleuren

Teken een programma dat 13 vierkanten naast elkaar tekent met alle kleuren die standaard in java zitten: black, blue, cyan, darkGray, gray, green, lightGray, magenta, orange, pink, red, white, yellow.

Opgave 2-4. GradientPaint

Teken een programma dat een rechthoek tekent over het gehele scherm met een RadialGradientPaint. Kies hier zelf een aantal kleuren voor uit, maar leg het centrum van de RadialPaint in het midden van het scherm (met getWidth()/2 en getHeight()/2). Zet hierna een MouseDrag listener aan het Canvas toe (met de canvas.setOnMouseDragged()), en leg het focuspunt van de RadialPaint op de locatie van de muis.


Transformeren van shapes

Naast het transformeren van het hele venster, is het ook mogelijk om een shape los te transformeren, en hier een nieuwe shape van te maken. Op deze manier kunnen we gemakkelijk een enkel object verplaatsen, zonder het hele canvas te verplaatsen of de shape zelf aan te passen. Dit is straks erg handig voor het laten animeren van objecten.

Om een complete transformatie voor te stellen kunnen we de AffineTransform klasse gebruiken. Een AffineTransform is een klasse die een (combinatie van) transformaties op kan slaan, en deze ook kan toepassen op vormen en punten. Intern slaat de AffineTransform een matrix op, die we kunnen manipuleren met de verschillende methoden. Voor standaard 2D transformaties wordt een 3x3 matrix gebruikt, maar omdat de onderste rij eigenlijk altijd [0, 0, 1] is, wordt deze weggelaten in de AffineTransform klasse, en slaat deze dus een 3x2 matrix op. Daarom dat je in de verschillende methoden die direct de matrix aanpassen ook maar 6 parameters ziet. Deze matrices kunnen we zelf samenstellen Matrices Deze matrices kun je zelf berekenen en in een AffineTransform zetten door middel van de constructor. Stel dat je een shear wil doen over de X-as met een factor 2 dan kun je de matrix matrix gebruiken. De onderste rij gebruiken we niet, dus we kunnen deze in java invullen met de constructor new AffineTransform(1,0,2,1,0,0);. Om deze nu met elkaar te combineren, kunnen we de volgende code gebruiken:

AffineTransform tx = new AffineTransform();
tx.translate(10,10);
tx.concatenate(new AffineTransform(1,0,2,1,0,0));
tx.scale(0.5, 0.5);

In deze code worden 2 AffineTransforms gecombineerd, en worden ze verder aangepast. De translate, rotate en scale methoden werken op een transform door, en worden dus automatisch gecombineerd. Let hierbij ook weer op de volgorde van transformeren.

Een AffineTransform bevat een matrix, die steeds aangepast wordt. De translate(x1, y2) methode maakt bijvoorbeeld de matrix matrix. Door deze matrix te vermenigvuldigen met de matrix matrix, krijg je de gecombineerde matrix, matrix (ga dit na). Ditzelfde principe werkt ook voor bijvoorbeeld een schaling. Hierij is de volgorde erg belangrijk. Als eerst geschaald wordt, en hierna verplaatst, wordt ook de verplaatsing meegeschaald. We zien dit in de vermenigvuldiging:

scale1

scale2

Dit kan gebruikt om objecten in een lokaal stelsel op te slaan, en hierna te transformeren naar de juiste positie in de wereld. Hierover meer in het volgende hoofstuk

Gebruiken van transformaties - Shape transformeren

De AffineTransform heeft ook een createTransformedShape() methode. Hiermee kun je een shape transformeren, om ‘m op een andere plek te zetten. Op deze manier kun je dus gemakkeljk een AffineTransform gebruiken om een Shape te verplaatsen zonder de shape aan te passen. Dit doe je met de createTransformedShape(Shape shape) methode in de AffineTransform. Op deze manier kun je gemakkelijk ieder object een eigen positie, rotatie en schaal geven, en deze flexibel aanpassen. We krijgen dan dus de volgende code:

class Renderable
{
    private Shape shape;
    private Point2D position;
    private float rotation;
    private float scale;

    public Renderable(Shape shape, Point2D position, float rotation, float scale)
    {
        this.shape = shape;
        this.position = position;
        this.rotation = rotation;
        this.scale = scale;
    }

    public void draw(FXGraphics2D g2d)
    {
        g2d.draw(getTransformedShape());
    }

    public Shape getTransformedShape()
    {
        return getTransform().createTransformedShape(shape);
    }

    public AffineTransform getTransform()
    {
        AffineTransform tx = new AffineTransform();
        tx.translate(position.getX(), position.getY());
        tx.rotate(rotation);
        tx.scale(scale,scale);
        return tx;
    }
}

Deze code tekent een object op een bepaalde positie, met een lokale rotatie en schaal. Deze attributen zijn in code gemakkelijk aan te passen, waardoor zo’n object gemakkelijk verplaatst, gedraait of geschaalt kan worden. Let bij het gebruik van deze code erop dat het object waar je een shape van maakt om zijn oorsprong draait, dus gebruik bijvoorbeeld een new Rectangle(-50,-50,100,100) voor een blok dat om zijn middelpunt draait. Deze coördinaten noemen we coördinaten in de lokale ruimte, de ‘object space’. Om dit object te gebruiken kun je de volgende code gebruiken:

public class HelloRenderable extends Application {

    Stage stage;
    ArrayList<Renderable> renderables = new ArrayList<>();

    @Override
    public void start(Stage primaryStage) throws Exception {
        stage = primaryStage;
        javafx.scene.canvas.Canvas canvas = new Canvas(1920, 1080);
        draw(new FXGraphics2D(canvas.getGraphicsContext2D()));
        primaryStage.setScene(new Scene(new Group(canvas)));
        primaryStage.setTitle("Hello transforms");
        primaryStage.show();
    }

    HelloRenderable()
    {
        renderables.add(new Renderable(new Rectangle2D.Double(-50,-50,100,100), new Point2D.Double(400,400), 0.25f * (float)Math.PI, 0.75f));
        renderables.add(new Renderable(new Rectangle2D.Double(-50,-50,100,100), new Point2D.Double(600,400), -0.25f * (float)Math.PI, 1.75f));
    }

    public void draw(FXGraphics2D g2d) {

        for(Renderable r : renderables)
            r.draw(g2d);

    }
}

Gebruiken van transformaties - Camera

cameracamera

In veel applicaties kunnen we het viewport verplaatsen en in of uitzoomen op een wereld. Denk hierbij aan bijvoorbeeld een applicatie waar een wereld van boven bekeken wordt, of een sidescrolling game. Dit doen we door een transformatie uit te voeren op het gehele venster. Dit kan in Java door middel van de Graphics2D.setTransform(AffineTransform transform) methode. Dit werkt op dezelfde manier als dat we in Les 1 hebben gedaan, maar nu kunnen we een AffineTransform object meegeven in plaats van losse translate, rotate en scale methoden te gebruiken. We kunnen hiervoor een camera object definiëren die een AffineTransform genereerd

class Camera
{
    private Point2D target;
    private float zoom;
    ...
}

We kunnen nu spreken van een aantal verschillende coördinatenstelsels

spaces

Door nu transformaties op een slimme manier te combineren, kunnen we transformeren van object space naar screen space. We hebben dus een object in model space, dit wordt door een object-transformatie omgezet naar world space, en door de camera-transformatie omgezet naar screen space.

Gebruiken van transformaties - Inverse

Als een camera gebruikt wordt om het scherm te scrollen, wordt het bepalen van de wereld-coördinaten van een muisklik een stuk lastiger. Er moet rekening gehouden worden met de positie, zoom en eventueel rotatie van de camera. Om dit op te lossen kunnen we de inverse van de cameratransformatie gebruiken. Als we de inverse van de cameratransformatie toepassen op de locatie van de muiscursor, krijgen we de wereld-coördinaten van dat punt. De AffineTransform klasse heeft hier de methode inverseTransform(Point2D source, Point2D destination) voor. Deze methode kun je op verschilende manieren gebruiken:

Point2D mousePosition = .....;
Point2D transformed1 = new Point2D.Double();
//manier 1, zet de getransformeerde waarde in een andere variabele
cameraTransform.inverseTransform(mousePosition, transformed1);

//manier 2, door null mee te geven, returned de methode een nieuw point2D
Point2D transformed2 = cameraTransform.inverseTransform(mousePosition, null);

// manier 3, je kunt ook het punt in-place transformeren zonder nieuwe punten aan te maken
cameraTransform.inverseTransform(mousePosition, mousePosition);

Door nu de positie van de muis op te vragen (zie Muisinteractie), kan gemakkelijk berekend worden welk object aangeklikt is, in combinatie met de transformatie van de camera.

Opgave 2-5. Spiegelen

Maak een applicatie die een assenstelsel tekent, de lijn Y = 2.5 × X, en een vierkant van 100x100 met het middelpunt op (0, 150). Teken hierna dit vierkant, maar dan gespiegeld over de lijn Y = 2.5 × X. Voor spiegelen kun je de AffineTransform klasse gebruiken om je vierkant te spiegelen. De AffineTransform is een abstractie van een matrix, met een constructor waar je deze matrix in kunt vullen. Let op de volgorde van de parameters


Muisinteractie

De muis werkt in Java event-driven, op basis van de EventHandler<> generic interface. Je kunt aan een component (zoals de canvas), een EventHandler koppelen, door middel van de setOn.... methoden. Om bijvoorbeeld te kijken of er geklikt is, kun je gebruik maken van de setOnMouseClick methode. Deze methode accepteert een instantie van een EventHandler interface implementatie, die we ook met een lambda kunnen implementeren.

Een voorbeeld:

public class HelloMouse extends Application {
	Stage stage;
	@Override
	public void start(Stage primaryStage) throws Exception {
		stage = primaryStage;
		javafx.scene.canvas.Canvas canvas = new Canvas(1920, 1080);
		draw(new FXGraphics2D(canvas.getGraphicsContext2D()));

		canvas.setOnMouseDragged(e ->
		{
			position = new Point2D.Double(e.getX(), e.getY());
			draw(new FXGraphics2D(canvas.getGraphicsContext2D()));
		});

		primaryStage.setScene(new Scene(new Group(canvas)));
		primaryStage.setTitle("Hello Mouse");
		primaryStage.show();
	}
	public Point2D position = new Point2D.Double(100,100);

	public void draw(FXGraphics2D g2d)
	{
		g2d.setBackground(Color.white);
		g2d.clearRect(0,0,1920,1080);
		g2d.setStroke(new BasicStroke(20));
		g2d.draw(new Rectangle2D.Double(position.getX()-50, position.getY()-50, 100, 100));
	}

}

Deze code zal een canvas aanmaken met een MouseDragged event listener erop, en zodra gesleept (met de muisknop ingedrukt) wordt, wordt de positie van een rectangle aangepast.

Van het MouseEvent kunnen een aantal eigenschappen opgevraagd worden. Zo kun je met de getX() en getY() opvragen waar de muiscursor is, met getClickCount() hoevaak er is geklikt (dus om dubbelkliks te detecteren). Met de getButton() methode kun je opvragen welke knop is ingedrukt. Deze waarde kun je vergelijken met MouseButton.PRIMARY, MouseButton.SECONDARY of MouseButton.MIDDLE.


Eindopdracht week 2

Opgave 2-6. Blokken Slepen

Maak een applicatie om blokken te slepen. Let erop dat als je een blokje sleept, het blokje niet verspringt.

Om dit programma te maken, kun je het beste een nieuwe klasse maken zoals de Renderable klasse, waarin je de kleur, positie en shape van het blokje opslaat. Deze objecten kun je in de init() methode van BlockDrag, en in de draw methode van BlockDrag teken je deze. In de mousePressed, mouseDragged methode deze array te inspecteren en te manipuleren kun je de blokken verplaatsen

Uitdaging: Voeg een camerasysteem toe. Door met de rechtermuisknop te slepen kun je het complete scherm verslepen, met het scrollwieltje kun je de camera inzoomen en uitzoomen, en met de linker muisknop kun je een blok verslepen. Daarnaast moet het slepen natuurlijk ook werken als de camera verschoven of ingezoomed is. Let erop dat het ‘t gemakkelijkst is om de camerapositie en zoom los op te slaan, en iedere keer dat getekend wordt de cameraAffineTransform te berekenen.

Block Dragger

Einde van week 2