Table of Contents
L-System
Laboratorium składa się z dwóch części:
- szybkie wprowadzenie do grafiki
- generacja obrazów za pomocą gramatyk L-system
Podstawy grafiki (Swing)
Użyjemy biblioteki graficznej Swing, a dokładniej klasy potomnej JPanel.
Umieść w klasie głównej poniższą funkcję main()
public static void main(String[] args) { // write your code here JFrame frame = new JFrame("Grafika"); frame.setContentPane(new DrawPanel()); frame.setSize(1000, 700); frame.setLocationRelativeTo(null); frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); frame.setResizable(true); frame.setVisible(true); }
DrawPanel
DrawPanel() to klasa, której na razie brakuje. Utwórz ją w projekcie i dodaj funkcję paint(Graphics g) z przykładowym kodem wypisującym tekst.
public class DrawPanel extends JPanel { public void paintComponent(Graphics g){ g.setFont(new Font("Helvetica", Font.BOLD, 18)); g.drawString("Hello World", 20, 20); System.out.println("painting"); } }
Zaobserwuj, kiedy na konsoli wypisywane jest painting.
Graphics to kontekst graficzny. Obiekt przechowuje informacje o bieżących atrybutach rysowania (font, kolor) oraz pozwala wywołać funkcje umożliwiające rysowanie wektorów oraz obrazów.
Wypróbuj kilka funkcji do rysowania. Na przykład
Rysowanie linii
g.drawLine(10,10,100,100);
Rysowanie elips
g.setColor(Color.yellow); g.fillOval(100,101,30,30); g.setColor(Color.black); g.drawOval(100,101,30,30);
Rysowanie wieloboków
int x[]={0,120,220,360,240}; int y[]={0,320,200,330,90}; g.fillPolygon(x,y,x.length);
Rysowanie obrazków
Jeżeli chciałbyś wyświetlić obrazek, należy wcześniej go załadować, np. przechowywać jako atrybut klasy. (Nie należy ładować go w funkcji paint).
Image img = Toolkit.getDefaultToolkit().getImage("bird1.jpg"),
albo (ładowanie z zasobów)
BufferedImage img = ImageIO.read(getClass().getResource("/resources/bird1.jpg"));
a następnie wyświetlić w funkcji paint()
g.drawImage(img,0,0,getWidth(),getHeight(),this);
Graphics2D
Graphics2D jest rozszerzoną wersją kontekstu graficznego pozwalającą na bardziej zaawansowane operacje.
Aby uzyskać dostęp do obiektu Graphics2D wystarczy rzutowanie.
public void paintComponent(Graphics g){ Graphics2D g2d= (Graphics2D)g; }
Ważną cechą Graphics2D jest możliwość transformacji (transformacji afinicznej) układu współrzędnych odpowiadających powinowactwom (~ klasy I LO), czyli:
- przesuniecia
- skalowanie
- rotacje
Postać macierzowa
- Wszystkie te operacje mogą być reprezentowane w postaci macierzy transformacji (3×3 dla 2D 4×4 dla 3D).
- Zobacz przykłady na https://en.wikipedia.org/wiki/Affine_transformation#Image_transformation
- Zastosowanie kilku operacji następujących po sobie można uzyskać mnożąc macierze, czyli np. funkcja Grapics2D.rotate() mnoży bieżącą macierz przez macierz rotacji.
- Macierz transformacji można zapisać w zmiennej lokalnej i załadować z powrotem do kontekstu graficznego.
Poniższy kod rysuje 12 linii obracając je o 30 stopni
// zachowaj macierz przekształcenia AffineTransform mat = g2d.getTransform(); // przesuń początek układu g2d.translate(100,100); // zastosuj skalowanie g2d.scale(.2,.2); // narysuj linie for(int i=0;i<12;i++){ g2d.drawLine(0,0,100,100); g2d.rotate(2*Math.PI/12); } //oddtwórz poprzednie ustawienia transformacji układu współrzędnych g2d.setTransform(mat);
Analogicznie możesz obrócić tekst podczas rysowania…
g2d.translate(200,200); // zastosuj skalowanie g2d.scale(.2,.2); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); Font font = new Font("Serif", Font.PLAIN, 96); g2d.setFont(font); for(int i=0;i<12;i++){ g2d.drawString("Ala ma kota",150,0); g2d.rotate(2*Math.PI/12); }
Możesz ustawić atrybuty linii….
// zachowaj macierz przekształcenia AffineTransform mat = g2d.getTransform(); // przesuń początek układu g2d.translate(200,200); // zastosuj skalowanie g2d.scale(.2,.2); g2d.setStroke(new BasicStroke(50, CAP_ROUND,JOIN_MITER)); for(int i=0;i<12;i++){ //g2d.drawString("Ala ma kota",150,0); g2d.drawLine(0,0,100,100); g2d.rotate(2*Math.PI/12); } //oddtwórz poprzednie ustawienia transformacji układu współrzędnych g2d.setTransform(mat);
Możesz też użyć gradientowego wypełnienia dla wieloboków
Graphics2D g2d= (Graphics2D)g; AffineTransform mat = g2d.getTransform(); GradientPaint grad = new GradientPaint(0,0,new Color(0,255,0),0,100, new Color(0,10,0)); g2d.setPaint(grad); g2d.translate(0,50); g2d.scale(0.7,0.5); int x[]={286,286,223,0}; int y[]={0,131,89,108,}; g2d.fillPolygon(x,y,x.length); g2d.translate(670,0); g2d.scale(-1,1); g2d.fillPolygon(x,y,x.length); g2d.setTransform(mat);
Tło
Możesz ustawić tło wołając metodę setBackground() klasy JPanel
Na przykład
DrawPanel(){ setBackground(new Color(0,0,50)); // setOpaque(true); }
Nie zapomnij na początku paintComponent() wywołać metodę paintComponent() nadklasy
Kształty
Swing definiuje hierarchię kształtow Shape z różnymi figurami geometrycznymi: https://docs.oracle.com/javase/8/docs/api/java/awt/Shape.html
Interesowały nas będą:
Line2DorazRectangle2D(zwracane przez funkcje getBounds2D())
Poniższy kod definiuje klasę DemoPanel. W konstruktorze generuje 100 losowych linii oraz 100 losowych kolorów. Następnie rysuje je w funkcji paintComponent().
public class DemoPanel extends JPanel { List<Shape> shapes = new ArrayList<>(); List<Color> colors = new ArrayList<>(); DemoPanel(){ Random rand = new Random(); for(int i = 0; i<100; i++){ shapes.add(new Line2D.Double(rand.nextDouble() *500,rand.nextDouble()*500,rand.nextDouble()*500,rand.nextDouble()*500)); colors.add(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255))); } for(int i = 0; i<10; i++){ System.out.println(shapes.get(i).getBounds2D()); } } protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; g2.setStroke(new BasicStroke(5, CAP_ROUND,JOIN_MITER)); for(int i = 0; i<shapes.size(); i++){ g2.setColor(colors.get(i)); g2.draw(shapes.get(i)); } } }
L-system
L-system (system Lindenmayera) to zapisany w postaci formalnej gramatyki zbiór reguł umożliwiających generację grafiki. Można o nim przeczytać w Wikipedii: https://en.wikipedia.org/wiki/L-system
Obejmuje on:
- Aksjomat (początkowe słowo)
- Zbiór reguł produkcji
Generacja słów odbywa się w kolejnych iteracjach, podczas których wszystkie symbole nieterminalne w bieżącym słowie są zastępowane następnikiem zastosowanej reguły produkcji.
Wynikowe słowo można następnie zinterpretować graficznie, np. jako instrukcje dla „żółwia” (turtle graphics), co pozwala tworzyć złożone struktury, takie jak fraktale czy modele roślin.
Interpretacja symboli (symbolami są pojedyncze znaki):
+– zwiększenie kąta obrotu o zadany przyrost-– zwiększenie kąta obrotu o zadany przyrost[– zapis pozycji pozycji i kąta żółwia na stosie]– odczyt pozycji pozycji i kąta żółwia ze stosuF– przesunięcie w przód o zadaną odległośćinne litery– mogą działać analogicznie, jakFlub być ignorowane jako polecenie dla żółwia (w zależności od wybranej opcji). Wtedy służą wyłącznie do przepisywania słów.
Klasa LSystem
Zadeklaruj klasę LSystem z atrybutami obejmującymi aksjomat, reguły produkcji oraz parametrami kontrolującymi generację grafiki:
public class LSystem { String name="unknown"; String axiom = ""; Map<Character,String> rules = new HashMap<>(); double stepLength = 1.0; double angleIncrement = 90; int iterations = 1; boolean allLetterForward=true; public void addRule(char lhs, String rhs) { ... } public void addRule(String lhs, String rhs) { ... } /** * Rozwija (przepisuje) słowo current w kolejnych iteracjach zaczynając od axiom * * @param iterations * @return przepisane słowo */ public String rewrite(int iterations) { String current = getAxiom(); for (int i = 0; i < iterations; i++) { StringBuilder next = new StringBuilder(); for (char c : current.toCharArray()) { // zastosuj regułę dopasowaną do znaku //lub przepisz znak, jeśli nie znajdziesz reguły // ... } current = next.toString(); } return current; } public String rewrite(){ return rewrite(iterations); } String toJson(){ // Zastosuj Gson } static LSystem fromJson(String json){ // Zastosuj Gson }
Dodaj settery/ gettery dla atrybutów…
Wykorzystaj poniższy kod do generacji obiektów klasy LSystem oraz zapisu w formacie JSON. Sprawdź, jak wygląda przepisane słowo. Czy reguły produkcji są poprawnie stosowane.
LSystem ls = new LSystem(); ls.setName("Sierpinski triangles"); ls.setAxiom("F-G-G"); ls.addRule('F', "F-G+F+G-F"); ls.addRule('G', "GG"); ls.setStepLength(6); ls.setAngleIncrement(120); ls.setIterations(3); ls.setAllLetterForward(true); var ex = ls.rewrite(); System.out.println(ex); String js = ls.toJson(); new PrintStream("sierpinski-triangles.json","UTF-8").append(js);
Klasa Scene
Klasa Scene zawiera dane do wyświetlania (linie oraz ich kolory). Zawiera także metodę, która zamienia symbole słowa na ich reprezentację graficzną.
Na przykład, słowo reprezentujące trójkąty Sierpińskiego (po 3 iteracjach) zostanie zamienione na sekwencję poleceń dla żółwia (ruchy do przodu i obroty).
F-G+F+G-F-GG+F-G+F+G-F+GG-F-G+F+G-F-GGGG+F-G+F+G-F-GG+F-G+F+G-F+GG-F-G+F+G-F+GGGG-F-G+F+G-F-GG+F-G+F+G-F+GG-F-G+F+G-F-GGGGGGGG-GGGGGGGG
public class Scene { List<Line2D> lines = new ArrayList<>(); List<Color> colors = new ArrayList<>(); void addLine(double x1, double y1, double x2, double y2) { // dodaj Line2D.Double } Rectangle2D getBounds(){ // oblicz sumę (unię) wszystkich bbox dla linii return bounds; } /** * Wypełnia lines i colors wykonując polecenia dla żółwia * @param commands - rozwinięte słowo * @param step - wielkość kroku do przodu * @param angleIncrement - wielkość o którą zmienia się kat dla znaków + oraz - * @param allLettersForward - czy wszystkie litery powodują przesunięcie żółwia, czy tylko F */ void render(String commands, double step, double angleIncrement,boolean allLettersForward){ double x=0; // połozenie żółwia double y=0; // połozenie żółwia double angle=0; // kąt w radianach Stack<double[]> stack = new Stack<>(); // stos przechowujący tablice {x,y,angle} lines.clear(); for (char c : commands.toCharArray()) { // pamietaj o konwersji kąta na radiany // angle += Math.toRadians(angleIncrement); } }
Docelowa klasa DrawPanel
Uzupełnij kod. Podczas rysowania obraz umieszczany jest centralnie na ekranie.
Kolejne etapy to:
- Przesunięcie układu współrzednych okna na srodek
- Obrót (ze względu na przykłady gramatyk)
- Skalowanie - w wyniku wcześniejszego zastosowania obrotu raczej dzielimy szerokość przez wysokość (i wysokość przez szerokość)
- Translacja do środka sceny
public class DrawPanel extends JPanel { Scene scene = new Scene(); LSystem ls; public DrawPanel(LSystem ls) { this.ls=ls; setBackground(Color.BLACK); // możesz też ustawić bitmapę jako tło } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (scene.lines.isEmpty()) { String commands = ls.rewrite(); scene.render(commands, ls.getStepLength(), ls.getAngleIncrement(),ls.isAllLetterForward()); System.out.printf("Wygenerowaneo %d linii",scene.lines.size()); } // Wycentruj i wyskaluj obraz // Przenieś układ współrzędnych na środek panelu g2.translate(getWidth() / 2.0, getHeight() / 2.0); // Rotacja - dopasowanie do przykładów g2.rotate(-Math.PI / 2); // Ustal granice sceny Rectangle2D bounds = scene.getBounds(); // Skalowanie – dopasowanie sceny do panelu z marginesem // Zamieniona wysokość i szerokość ze wzgledu na obrót int margin = 20; double scale = Math.min(...); g2.scale(scale, -scale); // Przesunięcie środka sceny do (0, 0) double cx = bounds.getCenterX(); double cy = bounds.getCenterY(); g2.translate(-cx, -cy); // Rysowanie zmieniaj kolory i rysuj linie }
W klasie Main
Wczytaj obiekt klasy LSystem z pliku JSON. Nastepnie wyświetl go (utwórz JFrame i DrawPanel). W pasku tytułowym ramki wyświetl nazwę L-systemu.
Zadanie domowe
- Zademonstruj działanie programu na kilku (3-5) przykładach specyfikacji L-systemów. Możesz skorzystać z przykładów na stronie https://fedimser.github.io/l-systems.html
- Rozbuduj definicję klasy LSystem o specyfikację kolorów dla poszczególnych symboli. Nie możesz bezpośrednio zapisywać obiektów Color formacie JSON. Użyj np. stringów typu
red,yellowi mapy string→kolor.
- Przygotuj własną specyfikację LSystem, np. rysującą drzewo, choinkę
- Opcjonalnie, możesz rozszerzyć specyfikację o URL bitmapy jako tła
- Prześlij kod, pliki JSON oraz wygenerowane bitmapy

