Table of Contents

L-System

Laboratorium składa się z dwóch części:

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:

Postać macierzowa

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ą:

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:

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):

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:

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