====== 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 (3x3 dla 2D 4x4 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ą:
* ''Line2D'' oraz
* ''Rectangle2D'' (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 shapes = new ArrayList<>();
List 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
===== 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 stosu
* ''F'' -- przesunięcie w przód o zadaną odległość
* ''inne litery'' -- mogą działać analogicznie, jak ''F'' lub być ignorowane jako polecenie dla żółwia (w zależności od wybranej opcji). Wtedy służą wyłącznie do przepisywania słów.
{{ :pz1:l-system.png?direct&400 |}}
==== 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 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 lines = new ArrayList<>();
List 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 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'', ''yellow'' i 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