Kordused, rekursiooni baas, fraktal
Ühe pargi keskel olnud
plaan, kuhu see park ilusti üles joonistatud. Kõik puud ja teed olnud ilusti
peal, samuti omal ka pargi plaan. Küll väiksemalt ja lihtsamalt, kuid siiski
võis tähtsamaid teid ja ojasid täiesti ära tunda. Ning pisikese täpina oli
sealgi näidatud plaani asukoht pargis. Nõndaviisi on see kui pildi sisse minek.
Samasugust nähtust võib tähele panna, kui kaameramees filmib ning filmi peale
jääb muu hulgas ka ekraan, kus parasjagu salvestatavat materjali näidatakse.
Nii satub tekkinud pilt üha uuesti ja uuesti ringlema ning vaatajale tundub
nagu ta saaks jälgida pikka koridori, kus avaneksid üha järgmised ja järgmised
uksed ning kõigis neis oleks üks ja sama sisu. Iga korraga ainult üha
väiksemalt ja väiksemalt, kuni meie silm või aparaadi eraldusvõime neid enam
üksteisest eraldada ei suuda. Samasuguse tulemuse võib saada ka lihtsamate
koduste vahenditega, kui üksteise vastu asendada kaks peeglit. Kumbki näitab teisele
saabuvat pilti tagasi ning tekkiv koridor võib välgukiirusel väga pikaks
kasvada. Olen näinud mitutteist peeglit üksteise seest paistmas ning see polnud
kindlasti mitte veel võimaluste tipp. Paremate peeglite, suurema valgustuse
ning põhjalikuma katsetamise juures saanuks tekkiva koridori pikkust veel hulga
maad suurendada.
Kui pargis olnuks
selliseid plaane neli, sel juhul oleks ka plaani peal selliseid plaane neli
ning iga pisikese plaani peal neli täppi tähistamas kaartide asukohti. Ja mitut
salvestatavat pilti näitavat ekraani filmides tunduks nagu eesolev koridor
muudkui hargneks ja hargneks ning kaugustesse kasvaval rägastikul ei paistagi
lõppu tulema.
Mis on varem ekraanil,
maastikul või paberi peal olemas, seda saab ka ise luua ning oma soovi kohaselt
muuta. Kunstnikud ning teadlased on saanud niimoodi kauneid joone ja pinna
vahepealseid kujundeid - fraktaleid. Arvuti võimaldab meil aidata korduvaid
kujundeid uuesti ja uuesti välja joonistada ilma, et peaksime oma sõrmi tuhandete
väikeste joonekeste tõmbamiseks kulutama. Ning ega käsitsi korralikust
printerist selgemat tulemust ikkagi ei õnnestu saada. Ainult, et arvuti tarvis
tuleb jooned kõigepealt välja arvutada, alles siis võime hakata mõtlema nende
paberile kandmisele. Seetõttu tuleb hakkama saada mõnede matemaatiliste
arvutustega, kuid juhul kui need tunduvad ületamatutena, võib valemid võtta
juba töötavatest näidetest. Väärtusi suurendades ja vähendades ning käske
lisades ja eemaldades peaks olema võimalik pea igasugused vähegi
ettekujutatavad joonistused kokku kombineerida. Koridori moodustavate üksteise
sisse tulevate ringide või ristkülikute joonistamise eeskiri oleks küllalt
lihtne: tuleb neid senikaua üksteise otsa lükkida, kuni sisemised nii väikseks
muutuvad, et sinna sisse pole enam mõistlik ega võimalik midagi paigutada.
|
import java.awt.*; import java.applet.Applet; public class Koridor
extends Applet{ public
void paint(Graphics g){ int
x=100, y=100, laius=100, korgus=100;
while(laius>5){ g.drawRect(x, y, laius, korgus); laius=laius/2; korgus=korgus/2; x=x+laius/2; y=y+laius/2; } } } |
Niiviisi anti algul koridori ukse joonistamisel ette selle vasak ja ülemine
serv arvatuna pildi nullpunktist. Igal järgmisel korral joonistatakse ukse
sisse järgmine, mõõtmeid pidi poole väiksem uks (ehk praegusel juhul ristkülik.
Vasakut serva nihutakse edasi veerandi esialgse laiuse võrra. Nii paistab, et
järgmine jääb eelmise sisse keskele. Kui nihutaksime sisemist vähem, siis
tunduks, nagu seisaksime suure pika koridori suhtes viltu.
Iga sammuga ühekaupa
niimoodi kujundeid teise sisse või peale joonistada saab sellise tsükliga
ilusti. Lihtsalt tuleb iga ringi alguses ette anda uued koordinaadid, mille
järgi kujund välja joonistada. Iga sammuga mitme sisemise tüki joonistamine aga
läheb praegusel viisil keerukaks. Siit aitab meid välja rekursiooniks nimetatud
vahend, kus iseeneslikult kasvava kujundi loomine usaldatakse ühele
alamprogrammile, kust siis seda sama vajaduse korral uuesti välja kutsutakse.
Eelnev näide päistaks selle lähenemise valguses välja nii:
import java.applet.Applet;
import java.awt.*;
public class Koridor2 extends Applet{
void
joonistaKoridor(Graphics g, int x, int y, int laius, int korgus){
g.drawRect(x,
y, laius, korgus);
if(laius>5)joonistaKoridor(g, x+laius/4, y+korgus/4, laius/2,
korgus/2);
}
public
void paint(Graphics g){
joonistaKoridor(g, 100, 100, 100, 100);
}
}
Nagu näha, muutus kood pigem lühemaks ning mis tähtsam - kergemini
soovikohaselt muudetavaks. Loodud joonistamise käsku võib mitmelt poolt välja
kutsuda ning kui on soovi omale hargnevad koridorid luua, siis tuleb vaid paar
käsku ümber ja juurde teha.
import java.applet.Applet;
import java.awt.*;
public class Koridor3 extends Applet{
void
joonistaKoridor(Graphics g, int x, int y, int laius, int korgus){
g.drawRect(x, y, laius, korgus);
if(laius>15){
joonistaKoridor(g, x+laius/8,
y+korgus/4, laius/4, korgus/2);
joonistaKoridor(g, x+laius*5/8, y+korgus/4, laius/4, korgus/2);
}
}
public
void paint(Graphics g){
joonistaKoridor(g, 100, 100, 200, 200);
}
}
Kujundid ei pea üksteise sees mitte sama pidi olema. Küllalt lihne ning samas ilus on korduvalt veidi keeratuna hulknurki üksteise sisse joonistada. Hea ettekujutusvõime korral võib niimoodi jääda mulje kasvavast tornist või sügavusse pürgivast august. Kirjeldus:
Esimene kolmnurk joonistatakse nii, et tema kaugus rakendi servadest oleks 10 punkti. Järgmine kolmnurk joonistatakse eelmise sisse nõnda, et tema nurgapunktide leidmiseks liigutakse kümnendik mööda kolmnurga külge edasi. Matemaatiliselt leitakse uus asukoht võttes lähema punkti koordinaatidest üheksa kümnendikku ning liites sellele kaugema punkti ühe kümnendiku. Abimuutujatena kasutatakse siin jääki ja nihet, mis peavad oma väärtustes kokku andma ühe. Kõigepealt leitakse uued punktid ning seejärel omistatakse uute väärtused (ax1, ay1) joonistamisel kasutatavatele väärtustele(x1, y1). Sellline vaheetapp on vajalik, kuna algseid koordinaate läheb ka pärast uute leidmist vaja. Kõigepealt arvutatakse punkti 1 uus asukoht punktide 1 ning 2 vahelt. Kui aga pärast hakatakse kolmanda punkti uut asukohta arvutama punktide 1 ja 3 vahelt, siis peab punkti 1 vana asukoht teada olema, et leitud punkt algse joone peale satuks. Arvutatakse reaalarvudega, vaid joonistamisel teisendatakse täisarvulisteks ekraanipunktideks.
public class Kolmnurgad extends Applet{
public
void paint(Graphics g){
Dimension suurus=getSize();
double
x1=10, y1=suurus.height-10,
x2=suurus.width-10, y2=suurus.height-10,
x3=suurus.width/2, y3=10;
double
ax1, ay1, ax2, ay2, ax3, ay3;
int
kordustearv=20;
double
nihe=0.1, jaak=1-nihe; //osa külje pikkusest
for(int
i=0; i<kordustearv; i++){
g.drawLine((int)x1, (int)y1, (int)x2, (int)y2);
g.drawLine((int)x2, (int)y2, (int)x3, (int)y3);
g.drawLine((int)x3, (int)y3, (int)x1, (int)y1);
try{Thread.sleep(100);}catch(Exception e){} //viivitus
ax1=x1*jaak+x2*nihe;
ay1=y1*jaak+y2*nihe;
ax2=x2*jaak+x3*nihe;
ay2=y2*jaak+y3*nihe;
ax3=x3*jaak+x1*nihe;
ay3=y3*jaak+y1*nihe;
x1=ax1; y1=ay1;
x2=ax2; y2=ay2;
x3=ax3; y3=ay3;
}
}
}
Järgnevas näites asutakse
kolmnurka kasvatama. Võrdhaarsele kolmnurgale antakse ette pikema külje kaks
otspunkti. Nende abil arvutatakse kolmas nurk, mis paigutatakse pikema külje
keskkohast küljega risti võetuna poole küljepikkuse kaugusele. Külgede kohale
tõmmatakse jooned ning juhul, kui tekkinud lühem külg oli vähemalt 10 punkti
pikk, võetakse see uue kolmnurga pikimaks küljeks ning arvutatakse selle järgi
uued küljed. Kuna iga korraga lähevad tekkivad kolmnurgad väiksemaks, siis
ühest hetkest alates on mõistlik joonistamine ära lõpetada. Kui loodava
kolmnurga kõrguseks poleks mitte pool vaid kaks aluseks võetavat külge, siis
tuleksid uued kolmnurgad järjest suuremad ning tuleks teiselt poolt leida
ülempiir, millest suuremat kujundit pole enam mõistlik joonistada.
Joonistamise eest
hoolitseb meetod joonistaPuu, millele antakse joonistuse sihtkoha graafiline
kontekst ning joone otspunktide koordinaadid. Joonistamisega saab meetod
hakkama sõltumata sellest, millises asukohas ning millise kaldega joon sinna
ette antakse.
Matemaatilise poole
seletus. Eelkirjeldatud kohas asuva kolmanda punkti asukoha leidmiseks tuleks
kõigepealt leida pikema külje keskpunkt ning sealt edasi liikuda küljega risti
poole külje pikkuse ulatuses. Üks võimalus külje keskkoha leidmiseks on
arvutada nihe (vektor) ühest punktist teise (x=x2-x1; y=y2-y1), leida sellest
pool ning lisada esimese punkti koordinaatidele juurde (x=x1+(x2-x1)/2;
y=y1+(y2-y1)/2). Edasi tuleks leitud punktile otsa liita punktidevahelise
sirgega risti minev nihe. Nihkevektori keeramiseks saab kasutada seost
(x’=x*cos(a)-y*sin(a); y’=x*sin(a)+y*cos(a)) ehk täisnurkse vastupäeva nihke
korral cos(a)=0, sin(a)=1 ning (x’=-y;
y’=x) ning poole punktidevahelise kauguse pikkusega ning punktidevahelise
sirgega risti oleva nihke saab ((y2-y1)/2; (x2-x1)/2) ning sealtkaudu tulevad
kokku ka x3 ja y3 arvutamise valemid. Kuna arvutil on suunatud y-telg alla,
tavamatemaatikas aga üles, siis on märgid telje suuna muutmise eesmärgil
vastupidiseks muudetud.
Ootamiskäsk Thread.sleep
on vahele pandud lihtsalt seetõttu, et joonistamisel oleks näha, millises
järjekorras kolmnurgad ekraanile tekivad ning millal üks joonistuspuu valmis
saab ning teisega alustatakse. Kui see käsk välja kommenteerida, siis valmib
pilt tunduvalt kiiremini.
import java.applet.Applet;
import java.awt.*;
public class Puu extends Applet{
double
kaugus(int x1, int y1, int x2, int y2){
return
Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1));
}
void
joonistaPuu(Graphics g, int x1, int y1, int x2, int y2){
int x3=x1+(x2-x1)/2+(y2-y1)/2;
int y3=y1+(y2-y1)/2-(x2-x1)/2;
g.drawLine(x1, y1, x2, y2);
g.drawLine(x1, y1, x3, y3);
g.drawLine(x3, y3, x2, y2);
try{Thread.sleep(500);}catch(Exception e){}
if(kaugus(x1, y1, x3, y3)>10){
joonistaPuu(g, x1, y1, x3, y3);
joonistaPuu(g, x3, y3, x2, y2);
}
}
public
void paint(Graphics g){
joonistaPuu(g,
130, 290, 170, 290);
}
}
Kuna korduva joonistamise
puhul läheb punktidevaheliste nihkevektorite arvutamist ning keeramist sageli
vaja, siis koostati selle tarbeks klass, mis matemaatilised arvutused enese
sisse peidab ning programmeerijale jääb vaid hoolitseda sisulise poole eest.
Väärtused saab sellele klassile anda vaid isendi loomisel (samuti nagu klassi java.lang.String
puhul) ning iga muundamise puhul luuakse uus isend. Andmeid hoitakse ja
arvutatakse täpsuse huvides reaalarvudena, kuid välja antakse joonistamise
tarbeks täisarvudena. Meetodid on kahe nihke liitmiseks (pluss), teguriga korrutamiseks (korda), pikkuse
arvutamiseks (pikkus) ning täisnurga jagu vastupäeva keeramiseks (keera). Kuna
punkte tasandil võib andmete poolest samastada nullpunktist nendeni jõudvate
vektoritega, siis sobivad klassi meetodid mõnel puhul ka punktidega ümber
käimiseks.
public class Tasandinihe{
/**
* Nihke
koordinaatide väärtused. Piiritleja final rea ees näitab, et
*
väärtusi pärast algset omistamist enam muuta ei saa.
*/
final
double x, y;
public
Tasandinihe(double ux, double uy){
x=ux;
y=uy;
}
/**
* Nihe
arvutatakse etteantud kahe punkti koordinaatide järgi
*/
public
Tasandinihe(double x1, double y1, double x2, double y2){
x=x2-x1;
y=y2-y1;
}
int X(){
return
(int)x;
}
int Y(){
return
(int)y;
}
double
pikkus(){
return
Math.sqrt(x*x+y*y);
}
/**
* Vahe
samast punktist lähtuvate nihete otspunktide vahel.
*/
double
kaugus(Tasandinihe t1){
return
t1.miinus(this).pikkus();
}
/**
*
Väljastatakse nihke väärtuse ja teguri korrutis.
* Nihe
ise jääb muutmata.
*/
Tasandinihe korda(double tegur){
return
new Tasandinihe(x*tegur, y*tegur);
}
/**
*
Väljastatakse käesoleva ning parameetrina antud nihke summa.
* Mõlema
nihke enese väärtus jääb muutmata.
*/
Tasandinihe pluss(Tasandinihe t1){
return
new Tasandinihe(t1.x+x, t1.y+y);
}
Tasandinihe miinus(Tasandinihe t1){
return
this.pluss(t1.korda(-1));
}
/**
* Suund
keeratakse täisnurga jagu vastupäeva.
*/
Tasandinihe keera(){
return
new Tasandinihe(-y, x);
}
}
joonistamine näeks loodud Tasandinihkeklassi abil välja nii nagu allpool
toodud programmis Puu2. Leitakse kaks nihet: üks ühest punktist teise ning
teine nihe esimesega risti. Edaspidi saab neid kahte kasutada x ning y
ühikutena uues loodud koordinaatteljestikus, kus x-teljeks oleks kahe etteantud
punkti vaheline sirge.
Tasandinihe nx=p2.miinus(p1);
tähendab, et nihke x-ühiku leiame, kui lahutame teise punkti
koordinaatidest esimese punkti koordinaadid.
Eelmisega risti oleva y-ühiku saame aga x-i täisnurga jagu vastupäeva
keerates.
Tasandinihe ny=nx.keera();
Edasi võib loodud lõike ühikutena
kasutades leida joonistamise tarvis vajaliku(d) punkti(d). Niiviisi sõltubki
arvutatava joonise asukoht ja suurus vaid etteantud punktidest.
Tasandinihe p3=p1.pluss(nx.korda(0.5).pluss(ny.korda(-0.3)));
import java.applet.Applet;
import java.awt.*;
public class Puu2 extends Applet{
void
joonistaPuu(Graphics g, Tasandinihe p1, Tasandinihe p2){
Tasandinihe nx=p2.miinus(p1);
Tasandinihe ny=nx.keera();
Tasandinihe p3=p1.pluss(nx.korda(0.5).pluss(ny.korda(-0.3)));
g.drawLine(p1.X(), p1.Y(), p2.X(), p2.Y());
g.drawLine(p1.X(), p1.Y(), p3.X(), p3.Y());
g.drawLine(p3.X(), p3.Y(), p2.X(), p2.Y());
try{Thread.sleep(500);}catch(Exception e){}
if(p1.kaugus(p3)>10){
joonistaPuu(g, p1, p3);
joonistaPuu(g, p3, p2);
}
}
public
void paint(Graphics g){
joonistaPuu(g, new Tasandinihe(30, 290), new Tasandinihe(270, 290));
}
}
Et loodav joonis enam puu
moodi oleks, selleks peaks seal peale oksakohtade ka oksi endid olema. Nii
tuleb kaks punkti juurde arvutada ning iga kujund luuakse juba viie punkti
abil: algsed kaks oksa algul, järgmised kaks oksa lõpul ning veel üks, millest
alates uut oksapaari juurde arvutada. Joone tõmbamist läheb päris palju vaja,
selle tarvis sai uus alamprogramm loodud. Kui y suund kohe vastupidiseks
keerata, siis võib edaspidi harjumuspärast matemaatilist tava järgida, et see
telg ikka üles suunatud on. Muidugi veel viisakam oleks suunda alles
joonistamisel arvutada.
import java.applet.Applet;
import java.awt.*;
public class Puu3 extends Applet{
void
joonistaPuu(Graphics g, Tasandinihe p1, Tasandinihe p2){
Tasandinihe nx=p2.miinus(p1);
Tasandinihe ny=nx.keera().korda(-1); //y-suund vastupidiseks
Tasandinihe p3=p1.pluss(ny.korda(3));
Tasandinihe p4=p2.pluss(ny.korda(3));
Tasandinihe p5=p1.pluss(nx.korda(0.5).pluss(ny.korda(3.3)));
joon(g,
p1, p2);
joon(g,
p1, p3);
joon(g,
p2, p4);
joon(g,
p3, p5);
joon(g,
p4, p5);
try{Thread.sleep(500);}catch(Exception e){}
if(p1.kaugus(p3)>10){
joonistaPuu(g, p3, p5);
joonistaPuu(g, p5, p4);
}
}
void
joon(Graphics g, Tasandinihe t1, Tasandinihe t2){
g.drawLine(t1.X(), t1.Y(), t2.X(), t2.Y());
}
public
void paint(Graphics g){
joonistaPuu(g, new Tasandinihe(130, 290), new Tasandinihe(170, 290));
}
}
Et kasutajale rohkem pildi kujundamise võimalusi anda, selleks võib algsed
punktid pärida tema käest, samuti lasta muuta muid parameetreid nagu okste
pikkus ja kalle ning joonistamise kiirus.
import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
public class Puu4 extends Applet implements
MouseListener{
int vajutusenr;
Tasandinihe hiir1=new Tasandinihe(130, 300),
hiir2=new Tasandinihe(170, 300);
Scrollbar
ooteaeg=new Scrollbar(Scrollbar.HORIZONTAL, 100, 100, 0, 500);
//väärtus, nupupikkus, vähim, suurim
Scrollbar
pikkus=new Scrollbar(Scrollbar.HORIZONTAL, 200, 100, 0, 500);
Scrollbar
kalle=new Scrollbar(Scrollbar.HORIZONTAL, 200, 100, 0, 500);
Label
ooteajasilt=new Label("Aeglustus joonistamisel:");
Label
pikkusesilt=new Label("Lüli pikkus:");
Label
kaldesilt=new Label("Okste kalle:");
public
Puu4(){
setLayout(new BorderLayout());
Panel
p1=new Panel(new GridLayout(3, 2));
p1.add(pikkusesilt);
p1.add(pikkus);
p1.add(kaldesilt);
p1.add(kalle);
p1.add(ooteajasilt);
p1.add(ooteaeg);
add(p1,
BorderLayout.SOUTH);
addMouseListener(this);
}
public
void mousePressed(MouseEvent e){
vajutusenr++;
if(vajutusenr==1){
hiir1=new Tasandinihe(e.getX(), e.getY());
}
if(vajutusenr==2){
hiir2=new Tasandinihe(e.getX(), e.getY());
repaint();
vajutusenr=0;
}
}
public
void mouseReleased(MouseEvent e){}
public
void mouseClicked(MouseEvent e){}
public
void mouseEntered(MouseEvent e){}
public
void mouseExited(MouseEvent e){}
void
joonistaPuu(Graphics g, Tasandinihe p1, Tasandinihe p2){
Tasandinihe nx=p2.miinus(p1);
Tasandinihe ny=nx.keera().korda(-1); //y-suund vastupidiseks
Tasandinihe p3=p1.pluss(ny.korda(pikkus.getValue()/100.0));
Tasandinihe p4=p2.pluss(ny.korda(pikkus.getValue()/100.0));
Tasandinihe p5=p1.pluss(nx.korda(0.5).pluss(ny.korda(
pikkus.getValue()/100.0+kalle.getValue()/1000.0
)));
joon(g,
p1, p2);
joon(g,
p1, p3);
joon(g,
p2, p4);
joon(g,
p3, p5);
joon(g,
p4, p5);
try{Thread.sleep(ooteaeg.getValue());}catch(Exception e){ }
if(p1.kaugus(p3)>10){
joonistaPuu(g, p3, p5);
joonistaPuu(g, p5, p4);
}
}
void
joon(Graphics g, Tasandinihe t1, Tasandinihe t2){
g.drawLine(t1.X(), t1.Y(), t2.X(), t2.Y());
}
public void
paint(Graphics g){
joonistaPuu(g, hiir1, hiir2);
}
public
static void main(String argumendid[]){
Frame
f=new Frame("Puu joonistamine");
f.add(new Puu4());
f.setSize(300, 400);
f.setVisible(true);
}
}
Joonistamisel ei pruugi
kõik lülid olla sugugi ühesugused. Nende ehitus võib sõltuda suurusest,
kaugusest juurest kui ka näiteks juhuse läbi. Nii võib luua küllalt usutavaid
pärisasjade analooge nagu lumehelves, riigipiir või kasvõi seesama puu. Ka
kõverjoone tõmbamise algoritmid kasutavad sarnast lähenemist, sest tunduvalt
odavam on meeles pidada kolme või nelja punkti asukohta kui joone iga punkti
asukohta. Liiatigi erinevad ekraanide ja printerite joonistustihedused
piisavalt, et ühe tarvis võib etteantud tihedusega punktide meeldejätmine
tunduda raiskamisena, teisel puhul aga jääb tulemus silmnähtavalt konarlik.
Rekursiivselt aga punkte ühendavaid sirgeid lühemateks lõikudeks jagades võib
igal korral uute joonte loomise siis lõpetada, kui ollakse jõutud
joonistustäpsuse tasemele. Sealt edasi arvutamine enam paremat tulemust ei saa
anda.
Ka võib kasutajal lubada
ette joonistada, milline peaks üks lüli välja nägema ning millistesse
kohtadesse selle peal võiksid uued kinnitada. Nii oleks tulemuseks töövahend
kunstniku tarvis. Kuid lihtsalt silmarõõmu võib sellistest joonistest küllaga
saada, pakkudes vaatajale järelemõtlemist, millal ja kus võib jälle midagi
kusagilt välja kasvama hakata.
Üheks rekursiooni näiteks on murdjoone loomine. Olgu siis rakenduseesmärgiks lihtsalt kujundi servade kaunistamine või proovitagu läbi kapillaarsoonte võimalikku paiknemist nahaalusel pinnal. Kui pika joone sisse lisada jõnks ning edasi iga tekkinud joone sisse veel jõnks, siis ongi suudetud üheülbalise sirjoone asemel luua märgatavalt paindlikum kujund. Kavandatava algoritmina näeks joone jagamine väiksemateks osadeks välja järgnevalt.
public void
murdJoon(Graphics g, int x1, int y1, int x2, int y2){
if(kaugus(x1, y1, x2,
y2)>pikimaJoonePikkus){
//leiab joone keskkoha lähedale uue
punkti ning selle
//abil tõmbab kaks joont
} else {
g.drawLine(x1, y1, x2, y2);
}
}
Kuidas just uus punkt leitakse ning kuidas edasised jooned tehakse, selle tarvis on variante palju. Üks neist on toodud allpool. Joone keskoha võib leida otspunktide aritmeetilise keskmise abil.
int kx=(x1+x2)/2;
int ky=(y1+y2)/2;
Keskkoha lähedase punkti kaugus keskpunktist võiks sõltuda joone pikkusest. Et mida pikem joon, seda kaugemale võib nihke paigutada. Pika algse joone puhul pole väikest nihet kuigivõrd näha. Lühikese algjoone puhul aga sama pikk nihe võib loodavad jooned algsest hoopis pikemaks muuta ning juhul kui joone murdmine läheb kordusesse, võib kogu lugu sootuks tsüklisse sattuda. Katsete tulemusena aga paistis, et kui loodav punkt tuleb algsest keskkohast mõlemat telge pidi mõlemas suunas kuni 0,2 algse joone pikkuse kaugusele, siis pole joone liigset väljavenitatust karta. Samas aga on muutus piisav, et silmaga sirgjoont ja murdjoont eristada.
int x3=kx+(int)((Math.random()*k*0.4)-k*0.2);
int y3=ky+(int)((Math.random()*k*0.4)-k*0.2);
Kui uus keskpunkt valmis arvutatud, siis tuleb hoolitseda, et algsetest otspunktidest loodud uue punktini joon saaks loodud. Olgu siis lihtsalt tõmmatuna või võetakse nüüd ette sama algoritm mis ennegi ja püütakse uue joone liiga suure pikkuse korral see omakorda osadeks jagada.
murdJoon(g, x1, y1, x3, y3);
murdJoon(g, x3, y3, x2, y2);
Eelneva algoritmi põhjal veidi pikem koodinäide, mille käivitamisel peaks ka tulemus näha olema. Kui paint'is öeldakse
murdJoon(g, 10, 20, 150, 200);
siis asutakse etteantud punktide vahele joont tõmbama. Ning iga kord, kui loodud joon tuleb pikem kui määratud pikima joone pikkus ehk 20 jagatakse joon uuesti kaheks jupiks. Lühema kahe punkti vahelise kauguse puhul veetakse joon lihtsalt ekraanile ning edasi lühemaks ei jagata.
import
java.applet.Applet;
import
java.awt.*;
public
class Murdjoon2 extends Applet{
int pikimaJoonePikkus=20;
/**
* Alamprogramm väljastab kahe punkti
vahelise kauguse
*/
public double kaugus(int x1, int y1, int x2,
int y2){
int dx=x2-x1;
int dy=y2-y1;
return Math.sqrt(dx*dx+dy*dy);
}
public void murdJoon(Graphics g, int x1, int
y1, int x2, int y2){
double k=kaugus(x1, y1, x2, y2);
if(k>pikimaJoonePikkus){
int kx=(x1+x2)/2;
int ky=(y1+y2)/2;
int
x3=kx+(int)((Math.random()*k*0.4)-k*0.2);
int
y3=ky+(int)((Math.random()*k*0.4)-k*0.2);
murdJoon(g, x1, y1, x3, y3);
murdJoon(g, x3, y3, x2, y2);
} else {
g.drawLine(x1, y1, x2, y2);
}
}
public void paint(Graphics g){
murdJoon(g, 10, 20, 150, 200);
}
public static void main(String[]
argumendid){
Frame f=new Frame();
f.add(new Murdjoon2());
f.setSize(300, 300);
f.setVisible(true);
}
}
Igal joonistusel uus murdjoon
Eelnenud näites tuli igal joonistuskorral asuda joont uuesti välja arvutama. Tahtes aga masinat liigsest nuputamisest säästa ning mis tähtsamgi - loodud joont ikka ja jälle uuesti vaadata, tuleb mõeldud punktid meelde jätta. Et loodavate punktide hulk pole ette teada, siis on nende hoidmiseks massiivi asemel mugavam kasutada nimistut, näiteks standardpaketis kättesaadavat LinkedListi. Ning kuna siinses näites joone lühim pikkus ei muutu, siis võib kõik vahepunktid algul välja arvutada ning edasi vaid sobival hetkel pilt mälus paiknevate andmete põhjal välja joonistada. Punkti andmete hoidmiseks võib kasutada java.awt paketis asuvat klassi Point - vahendit mis juba olemas, ei pea hakkama enam uuesti looma. Ka punktide vahelise kauguse leidmiseks on juba käsklus olemas, Point-isendi käsklus distance teatab sobiva väärtuse. Tsükliga küsitakse nimistust ükshaaval välja punktipaarid. Kui punktide vaheline kaugus ületab lubatud pikima, siis leitakse keskkoha lähedale uue punkti koordinaadid nii nagu eelmiseski näites. Loodud p3 asetatakse endise p2 kohale ning LinkedList hoolitseb juba ise, et ülejäänud elemendid nimistus edasi liigutataks. Joonistamisel piisab punktipaaride näidatavate ekraanikoordinaatide vahele jooned tõmmata ning murdjoon ongi ekraanil.
import
java.applet.Applet;
import
java.awt.*;
import
java.util.*;
public
class Murdjoon4 extends Applet{
LinkedList punktid=new LinkedList();
int pikimaJoonePikkus=5;
public Murdjoon4(){
punktid.add(new Point(10, 10));
punktid.add(new Point(200, 300));
lisaVahePunktid();
}
public void lisaVahePunktid(){
int koht=0;
while(koht+1<punktid.size()){
Point p1=(Point)punktid.get(koht);
Point p2=(Point)punktid.get(koht+1);
double kaugus=p1.distance(p2);
if(kaugus>pikimaJoonePikkus){
Point p3=new Point(
(p1.x+p2.x)/2+(int)((Math.random()-0.5)*0.4*kaugus),
(p1.y+p2.y)/2+(int)((Math.random()-0.5)*0.4*kaugus)
);
punktid.add(punktid.indexOf(p2), p3);
if(p1.distance(p3)<=pikimaJoonePikkus){
koht=koht+1;
}
} else {
koht=koht+1;
}
}
}
public void paint(Graphics g){
for(int i=0; i<punktid.size()-1; i++){
Point p1=(Point)punktid.get(i);
Point p2=(Point)punktid.get(i+1);
g.drawLine(p1.x, p1.y, p2.x, p2.y);
}
}
public static void main(String[]
argumendid){
Frame f=new Frame();
f.add(new Murdjoon4());
f.setSize(300, 300);
f.setVisible(true);
}
}
Joon püsib ka akna suuruse muutmisel
Järgnevalt näide, mille alusel peaks õnnestuma ehitada isearenevaid maailmu nii mängude kui õpisimulatsioonide tarbeks. Nii nagu astronoomid väidavad, et mida pole võimalik märgata, seda ei pruugi ka olemas olla, kehtib sarnane järeldus seda enam ka arvutimaailma kohta. Sugugi ei pruugi kõiki erijuhte kohe programmi töö algul välja mõelda. Kui täpsustused suudetakse vajalikul hetkel tehe piisavalt kärmesti, pole kasutajal kuigivõrd võimalusi otsustamiseks, et mudelil kaugvaate korral miskit viga oleks. Ning võimalus igas soovitud detailis pisiasjadeni välja minna jätab vaatajale uskumuse, et kõik ongi lõpuni viimistletud. Kui kontrollid tulevad majapidamist üle vaatama ning kõikjal kuhu vaadata valitseb kord ja puhtus, ei saa neil jorisemiseks põhjust olla. Olgugi, et võibolla lihtsalt keegi jälgib kontrollide teekonda ning hoolitseb, et iga võimalik vaadeldav punkt õnnestuks selleks ajaks korda teha, kui kontroll oma suurima kiiruse abil saaks sinna jõuda.
Või kõrvutuseks veel näide muinasjutuvestjast. Hea vestja suudab väljamõeldud maailma kõikide kohtade ja erijuhtude kohta vastuseid anda, ehkki ta ei pruugi olla algul kõike lõpuni välja mõelnud. Kui ta suudab hoolitseda, et loodavad paigad, ühendusteed ja sündmused eelnevatega vastuollu ei lähe, siis võib jutustaja kasvõi koos kuulajatega uusi lugusid ja kohti välja mõelda. Ikka on huvitav kuulata, kaasa mõelda ja meenutada.
Võrreldes eelmise näitega ei saa praegusel juhul kõiki punkte rakenduse töö algul välja mõelda, vaid tuleb kohti vastavalt kasutaja liikumisele juurde mõelda. Kui kilomeetritepikkune murdjoon kohe sentimeetripikkuste lõikude kaupa välja arvutada, siis kuluks mälu kõvasti ning joonistamine muutub lootusetult aeglaseks. Juba ainuüksi ühe kilomeetri peale tuleks sada tuhat punkti, pikema maa peale seda enam.
Väljamõeldud võlumaailma aluseks on Eestimaa väga ligikaudne rannajoon - nii umbes Euroopa ilmakaardilt vaadatuna. Ning kes siinsest kandist rohkem ei tea, võib nähtud pilti täiesti uskuma jääda - eriti kuna uuesti samasse kohta vaatama tulles rannajoon viimati vaadatuga võrreldes ikka samal kohal paikneb.
Kasutajaliidesesse tulid juurde nupud, et oleks võimalik nii igasse ilmakaarde kui üles ja allapoole liikuda. Horisontaalsuunas on arvestatud, et üks ühik võrdub ligikaudu ühe kilomeetriga. Kõrguse puhul aga lihtsalt muudetakse suurendust iga sammu juures sama koefitsiendi jagu. Punkti andmete hoidmiseks kasutatakse endise täisarvulise Point'i asemel Point2D.Double-t mis võimaldab asukohti tunduvalt täpsemalt meelde jätta. Täisarvude puhul oleks siinse mõõtkava juures ühele punktile vastav üks kilomeeter, mis aga oleks külade ja linnade loomise soovi korral ilmselt liiga suur mõõtühik.
Maailmakoordinaatidest ekraanikoordinaatide arvutamiseks loodi eraldi funktsioon nagu ikka selliste arvutuste puhul tavaks. Punkti ekraanile joonistamisel arvestatakse nii punkti enese maailmakoordinaate, vaataja asukohta, suurendust kui akna suurust ja sealt tulenevat ekraani keskkoha koordinaadi väärtust. Koht, mis paikneb vaatamisel akna keskel, jääb sinna ka suurendamise või vähendamise korral.
int ekraaniX(double maailmaX){
return
ekeskx+(int)((maailmaX-vx)*suurendus);
}
Edasi kommenteeritud rakenduse kood.
import
java.applet.Applet;
import
java.awt.*;
import
java.awt.geom.*;
import
java.util.*;
import
java.awt.event.*;
public
class Murdjoon6 extends Applet implements ActionListener{
/** Ülesliikumisnupp. Vaataja koordinaadid
vähenevad */
Button yles=new Button("Üles");
/** Allaliikumisnupp. Vaataja liigub lõuna
suunas */
Button alla=new Button("Alla");
/** Nupp läände liikumiseks */
Button vasakule=new
Button("Vasakule");
/** Nupp itta liikumiseks */
Button paremale=new
Button("Paremale");
/**
* Pilt suureneb liigutakse allapoole.
* Nähtava osa joonele arvutatakse vajadusel
punkte juurde.
*/
Button suurenda=new
Button("Suurenda");
/**
* Suurenduskordaja väheneb. Näiliselt
liigutakse maapinnast kaugemale.
*/
Button vahenda=new
Button("Vähenda");
/**
* Kindlaksmääratud rannajoone punktide
loetelu maailmakoordinaatides.
*/
LinkedList punktid=new LinkedList();
/**
* Nähtav pikkus ekraanipunktides, millest
alates asutakse joont poolitama.
*/
double pikimJoonEkraanil=30;
/**
* Vaataja asukoha x maailmakoordinaatides.
*/
double vx=286;
/**
* Vaataja asukoha y maailmakoordinaatides.
*/
double vy=120;
/**
* Vaataja algne samm maailmakoordinaatides.
*/
double vsamm=5;
/**
* Ekraani keskkkoha x.
*/
int ekeskx;
/**
* Ekraani keskkkoha y.
*/
int ekesky;
/**
* Koefitsient näitamaks, mitu ekraanipunkti
vastab ühele
* maailmakoordinaatides ühikule.
*/
double suurendus=1;
/**
* Suhe, mille jagu suurenduskoefitsient
suureneb või väheneb
* alla või üles liikumisel.
*/
double suurenduskordaja=1.1;
/**
* Kujunduse, kuularite ja algväärtuste
paikasättimine.
*/
public Murdjoon6(){
add(yles);
add(alla);
add(vasakule);
add(paremale);
add(suurenda);
add(vahenda);
yles.addActionListener(this);
alla.addActionListener(this);
vasakule.addActionListener(this);
paremale.addActionListener(this);
suurenda.addActionListener(this);
vahenda.addActionListener(this);
looAlgneJoon();
}
/**
* Algse rannajoone punktide sättimine
maailmakoordinaatides.
*/
void looAlgneJoon(){
punktid.add(new Point2D.Double(186, 249));
punktid.add(new Point2D.Double(198, 180));
punktid.add(new Point2D.Double(170, 197));
punktid.add(new Point2D.Double(129, 155));
punktid.add(new Point2D.Double(129, 61));
punktid.add(new Point2D.Double(219, 21));
punktid.add(new Point2D.Double(267, 27));
punktid.add(new Point2D.Double(270, 6));
punktid.add(new Point2D.Double(352, 17));
punktid.add(new Point2D.Double(455, 33));
}
/**
* Liigutamine vastavalt nupuvajutustele.
*/
public void actionPerformed(ActionEvent e){
double samm=vsamm/suurendus;
if(e.getSource()==yles) {vy-=samm;}
if(e.getSource()==alla) {vy+=samm;}
if(e.getSource()==vasakule){vx-=samm;}
if(e.getSource()==paremale){vx+=samm;}
if(e.getSource()==suurenda){
suurendus*=suurenduskordaja;
}
if(e.getSource()==vahenda){suurendus/=suurenduskordaja;}
repaint();
}
/**
* Lisatavate punktide arvutus. Kui kahe
punkti vaheline joon ekraanil
* kipub tulema suurem määratud väärtusest,
siis leitakse
* joone keskkoha lähedale uus punkt ning
tõmmatakse algse joone
* otspunktidest jooned sellesse punkti.
*/
public void lisaVahePunktid(){
double
pikimaJoonePikkus=pikimJoonEkraanil/suurendus;
int koht=0;
while(koht+1<punktid.size()){
Point2D.Double
p1=(Point2D.Double)punktid.get(koht);
Point2D.Double
p2=(Point2D.Double)punktid.get(koht+1);
double kaugus=p1.distance(p2);
if(kaugus>pikimaJoonePikkus &&
(kasSees(p1.x, p1.y) || kasSees(p2.x, p2.y))){
Point2D.Double p3=new Point2D.Double(
(p1.x+p2.x)/2+((Math.random()-0.5)*0.4*kaugus),
(p1.y+p2.y)/2+((Math.random()-0.5)*0.4*kaugus)
);
punktid.add(punktid.indexOf(p2), p3);
if(p1.distance(p3)<=pikimaJoonePikkus){
koht=koht+1;
}
} else {
koht=koht+1;
}
}
}
/**
* Maailmakoordinaatide teisendus
ekraanikoordinaatideks, arvestatakse
* suurendust ja kasutaja asukohta.
*/
int ekraaniX(double maailmaX){
return
ekeskx+(int)((maailmaX-vx)*suurendus);
}
/**
* Maailmakoordinaatide teisendus ekraanikoordinaatideks.
*/
int ekraaniY(double maailmaY){
return
ekesky+(int)((maailmaY-vy)*suurendus);
}
/**
* Kontroll, kas etteantud
maailmakoordinaatidega punkt mahub
* ekraanil vaatevälja.
*/
boolean kasSees(double maailmaX, double maailmaY){
int ex=ekraaniX(maailmaX);
int ey=ekraaniY(maailmaY);
return ex>0 &&
ex<getWidth() && ey>0 && ey<getWidth();
}
/**
* Mälus olevatele andmetele vastavalt
koostatakse ekraanile pilt.
* Nähtava ala piires palutakse punkte luua niivõrd,
et pikim
* näha olev joon ei ületaks määratud
pikkust.
*/
public void paint(Graphics g){
lisaVahePunktid();
ekeskx=getWidth()/2;
ekesky=getHeight()/2;
for(int i=0; i<punktid.size()-1; i++){
Point2D.Double p1=(Point2D.Double)punktid.get(i);
Point2D.Double
p2=(Point2D.Double)punktid.get(i+1);
g.drawLine(ekraaniX(p1.getX()),
ekraaniY(p1.getY()),
ekraaniX(p2.getX()),
ekraaniY(p2.getY()));
}
}
/**
* Käivitus käsurealt.
*/
public static void main(String[]
argumendid){
Frame f=new Frame();
f.add(new Murdjoon6());
f.setSize(300, 300);
f.setVisible(true);
}
}
Algne üksikute punktidega rannajoon |
Rannajoon pärast esimest punktide lisamist |
Lääneranniku nihutus pildi keskele |
Suurendus koos üksikute lisandumistega |
Lähivaade |
Algsest säbrulisema üldplaan vaatepaigas. |
Et illustreerimise mõttes oli pikima joone pikkuseks määratud 30 ekraanipunkti, siis paistab algne suurte nurkadega joon selgelt välja. Kui suurimaks lubatud pikkuseks panna aga näiteks kaks punkti, siis pole kasutajal kuigivõrd võimalust märgata, et algne nähtav kaart polegi kõikide võimalike hiljem vaadatavate kohtade pealt veel olemas. Et vaatama asumisel vastavad kohad kohe luuakse, võiks mulje jäädagi täiuslik.
Fraktali omaduste demonstreerimisel võibki lubada üha peenemaks minevat arvutust. Mingil hetkel kipub niimoodi Double 14st komakohast täpsusel väheks jääma ning tuleks leida miski täpsem võimalus koordinaatide arvutamiseks. Olgu selleks siis java.math.BigDecimal soovitud arvu komakohtade talletamiseks või mõni omaloodud vastava oskusega objekt. Kusjuures suuremat komakohtade arvu on vaja talletada vaid lähemal vaatlusel tekkivate punktide korral.
Kui eesmärgiks aga tegeliku keskkonna piisavalt tõetruu jäljendamine, siis võiks koos suurendusega muutuda ka tekkivate kujundite omadused. Kui simuleeritakse õhusõidukiga Eestimaa kohal lendamist, siis tõenäoliselt pole põhjust välja arvutada vähem kui sentimeetrise läbimõõduga objekte. Samas ei pruugi nähtav ja täienev osa piidruda sugugi vaid rannajoonega.
Virtuaalse
Eestimaa täiendus
· Täienda rannajoont, lisa Eestimaale ka ida- ja lõunapiir.
· Nähtavad jooned võivad jaguneda mitmesse kogumisse. Lisa eraldi kogumina Võrtsjärve rannajoon.
· Iga kogumi juures on lisaks punktile kirjas ka vastava joone värv.
· Sinise värviga tähista tähtsamad jõed. Ka neile mõeldakse lähemal vaatamisel välja käänakud.
· Jõgede ja rannajoone käänakute juures ei lähe joone pikkus väiksemaks kui 1 meeter.
· Alates suurendusest 1 ekraanipunkt = 10 meetrit hakatakse välja mõtlema ning näitama puid. Kord loodud puud jäävad samadele kohtadele.
Naerunägu
· Joonista naeratav nägu, kelle kummaski silmas oleks samuti naerunägu.
· Joonise suurendamisel ilmub igast silmast jälle uus naerunägu välja.
· Lisaks eelmisele saab kasutaja panna joonise pidevalt suurenema ning määrata näo kallet.
Viisnurgad
· Ekraanile joonistatakse viisnurk.
· Üha väiksemad erivärvilised seest täidetud viisnurgad on üksteise sees, kusjuures sisemise viisnurga tipp läheb välimise viisnurga serva keskkohta.
· Üksteise sees on kuni 100 viisnurka. Kerimisribaga saab määrata, millise koha peal sisemise viisnurga nurk välimise serva puutub.