Tallinna Pedagoogikaülikool Informaatika osakond Graafika ja muusika programmeerimine Jaagup Kippar Tallinn 2003 Eessõna Käesolev konspekt sisaldab näiteid ja seletusi mitmete graafika ning muusika programmeerimisega seotud valdkondade kohta. Lugejalt eeldatakse varasemat programmeerimiskogemust vähemasti Java põhikursuse konspektis kirjutatu tasemel. Koostamisel kasutati enamjaolt TPÜ programmeerimispraktikumides läbi lahendatud näiteid ning kursustel läbitud teemasid, kuid piisava süvenemise korral peaksid teemad olema mõistetavad keskkoolirahvalegi. Juhendajad võiks siit leida materjale ka põhikooli programmeerimisringide tarbeks. Sugugi ei pea rakendust enese tarbeks kohandamisel kohe lõpuni mõistma. Ükshaaval parameetreid muutes ning lisandusi tehes tekib paratamatult ka üldpildist mõningane ettekujutus. Samas on püütud näited vähemalt teemade algul koostada võimalikult lühidad, et soovijad suudaksid kõiges näpuga järge ajada ning iga koodirea eesmärki mõista. Tahtes omaloodud rakendust lõpuni usaldada ning suuta igale ilmnevale veale põhjus leida ning see parandada, peab kogu toimuvast ülevaade olema. Teemade lõpus paiknevad ülesanded on mõeldud enam enesekontrolliks, kuid on samas praktikumides või eksamitöödena läbi lahendatud ning peaksid sobima ka arvutitundides ettevõtmiseks. Ülesande punktid on püütud teha vähemalt kolme raskustaseme jaoks, kusjuures esimene võiks olla pea igale teemaga kokkupuutunule jõukohane. Mitmedki ülesanded on mõeldud ideede ärgitamiseks ning on tänuväärne, kui õpetaja või lahendaja ise suudab selle põhjal hetkel huvitava ning vajaliku ülesande sõnastada. Konspekti valmimisele kaasa aitamise eest tänan TPÜ üliõpilasi, kes on näiteid kasutades ning ülesandeid lahendades andnud põhjust täiendusi ja parandusi sisse viia. Tänud Kaur Männikole, kelle loodud MIDI redaktor sobis parajasti vastavat teemat illustreerima, nii et konspekti koostajal polnud põhjust paremat looma hakata. Ülejäänu osas on pea täielikult tegu autori originaalkoodiga. Vastuseks varemgi esitatud küsimusele, et kas siin kirjutatud materjali tohib igaüks oma tarbeks või koolitundides või muul viisil kasutada vastan julgesti jah. Leian, et kõik need, kes tahavad, suudavad ja viitsivad eestikeelset õppematerjali tarvitada, väärivad piisavalt austust, et mitte hakata oma rahva seas piirangute pärast kemplema. Samas olen tänulik teadete kohta, kus siinse konspekti materjalid kasutust on leidnud. Head koodikirjutamist! Jaagup Kippar Sisukord Eessõna 2 Sisukord 3 Graafiku koostamine. 7 Üksikud ekraanipunktid. 7 Nihutus ja keeramine 7 Mõõtkava. 8 Pideva joonega graafik 9 Komponendi suuruse arvestamine 9 Ülesandeid 13 Graafikakomponendid 13 Valmiskomponendid 13 Aken 13 Tekstiväli 14 Valik 14 Märkeruudud 15 Menüü 15 Kerimispaneel 16 Paigutushaldurid 16 FlowLayout 16 GridLayout 17 BorderLayout 18 Paneel paigutamisel. 18 Absoluutsete koordinaatidega paigutus. 19 Omaloodud paigutushaldur 20 Kuularid 20 Tekstikuular 23 Klahvikuular 23 Fookusekuular 24 Hiirekuular 25 Hiire liikumise kuular 26 Graafikakomponendi loomine 27 Hulknurk 27 Komponendi kasutamine 28 Andmete ülekanne 29 Kopeerimine 29 Andmete vedamine (Drag and Drop) 31 Trükkimine 33 Lühike näide 33 Komponendi trükkimine 33 Trükitava ala suuruse muutmine 34 Lisavõimalused 35 Kokkuvõte 35 Ülesandeid 35 Graafilise liidesega võrgurakendused 36 Trips-traps-trull 36 Kirjeldus 36 Tutvustavad pildid 36 Serveripoole lähtekood seletustega 38 Kliendipoole lähtekood seletustega 40 Edasiarendusvajadused 44 Programmitekst 44 Jututoa graafiline klient. 47 Lihtsaim komplekt. 47 Lihtne server 48 Tahvel 49 Täiendatud tahvel 50 Kood 52 Jututoa tahvliga klient. 53 Registreeritud kasutajatega server. 56 Kolmas arendusring. 58 Lühidokumentatsioon 62 Kahe tahvliga klient. 63 Ülesandeid 69 Juhitav animatsioon 70 Taustateave 70 Joonistamine 70 Taust liikumise ajal mälus. 71 Lõim liikumisel 72 Sirelasemäng 73 Liikuv taust 73 Taust koos lilledega 75 Liigutatav putukas 78 Nektarit imev putukas 80 Püüdlus terviku poole 83 Edasiarendusvõimalused 90 Ülesandeid 91 Graphics2D 92 Joone omadused 92 Punktiirjoon 92 Joonistusala piiramine 93 Venitamine, keeramine 93 Värviüleminek 94 Muster 95 Värviüleminekuga tekst 95 Kujundi äärejooned 96 Äärejoonte äärejooned 97 Kujundi koostamine 97 Tehted kujunditega 97 Ülesandeid 98 Swing 99 HTML-kujundus 99 Sisemised raamid 100 Värvivalija 100 Failinime valija 101 Puud 103 Paigutus 104 Jaotuspaneel 104 Valikupaneel 105 Tööriistariba 105 Ennistamine 106 Dialoogiaknad 108 Tabel 110 Kujundatava tekstiga paneel 111 Veebiseilur 111 Kokkuvõte 113 Ülesandeid 113 Kolmemõõtmeline graafika 116 Kuubi keeramine 117 Nihutamine 117 Liigutamine 118 Interpolaatorid 119 Kaks kuupi 120 Muud kujundid 120 Taust 121 Vaatekoha muutmine 121 Kiri 122 Tasapind 122 Joonemassiiv 123 Valgus 123 Materjal 124 Muster 125 Töö käigus kujundite lisamine 125 Ruumimängu põhi 126 Tehniline seletus 128 Kokkuvõte 131 Ülesandeid 132 Rekursiivne joonistamine 133 Plaan plaani peal 133 Lõputu koridor 133 Teineteises peituvad hulknurgad 134 Kolmnurgad üksteise seljas 135 Kasutaja soovitud puu 138 Murdjoon 140 Virtuaalne Eestimaa 143 Ülesandeid 147 Maatriksarvutused joonistamisel 149 Klass maatriksarvutuste tarvis. 151 Keeramine 153 Nihutamine 155 Keeramine ümber määratud punkti. 156 Pööramine ümber telgede. 158 Ülesandeid 159 Pildioperatsioonid 160 Pildifaili loomine 160 Ekraanipildi kopeerimine 161 Pildi muutmine 161 Värvide tugevus 162 Värviarvutus maatriksiga 163 Põhivärvide vahetamine 164 Pilt mustvalgeks 164 Põhivärvi lisamine 165 Varju loomine 165 Piirjoonte hägustamine 166 Nihutamine 166 Pildi koostamine 167 Lainetus 169 Liikuv lainetus 170 XOR tehe joonistamisel. 172 Matemaatiline taust. 172 Mustvalge katsetus. 172 Värviline XOR 173 Liikumine 173 Kujundite kattumine liikumisel 174 Liikumine omaette lõimes 175 Ülesandeid 175 Muusika 177 Klippide mängimine 177 MIDI 177 Üksik noot 178 Kromaatiline heliredel 178 Helikõrguse ujumine 178 Pillide loetelu 179 Rajad 179 Kordamine 180 MIDI faili mahamängimine 180 MIDI faili loomine 182 Saateautomaat 183 MIDI redaktor 189 Ülesandeid 197 Digitaalheli 198 Lihtne piiks 198 Siinusekujuline laine 199 Tõusev heli 200 Kahebaidine kvant 201 Digitaalheli redaktor. 202 Märgistusala 202 Kopeerimine ja kleepimine 203 Teine ring 205 Kolmas ring 212 Kokkuvõte 223 Ülesandeid 223 Lõppsõna 224 Graafiku koostamine. Joonistuskäsud, ekraanikoordinaadid, maailmakoordinaadid, skaala, ümardamine Mõnikord tavatsetakse öelda, et üks pilt on rohkem väärt kui tuhat sõna. Ei pruugi see alati kehtida, kuid joonistest võib vahel kasu küll olla. Kui andmed ei muutu, siis kannatab mõne vastavaotstarbelise programmiga pildid valmis teha ning tekstile juurde liita. Kui aga andmed muutuvad või ei olda olemasoleva programmi poolt pakutava väljundiga rahul, siis on põhjust ise koodilõik kirjutada, mis andmed jooniseks muundab. Nagu programmide ja ka muude toimingute puhul, alustame lihtsamast ja liigume keerulisema suunas. Näited tehakse pideva ruutfunktsiooni tarvis, kuid sarnased arvutused tuleb ka muul puhul ette võtta, kui oma andmetele vastavalt püüame miskit ekraanile paigutada. Üksikud ekraanipunktid. Joonistamisel samastatakse graafiku matemaatilised maailmakoordinaadid ning arvuti ekraanikoordinaadid. Funktsiooni väärtus arvutatakse täisarvuliste x-ide juures ning vastavale x ja y kombinatsiooniga määratud kohale joonistatakse täpike. import java.awt.*; import java.applet.*; public class Graafik1 extends Applet{ public void paint(Graphics g){ for(int x=0; x<100; x++){ g.drawOval(x, x*x, 1, 1); } } } Nihutus ja keeramine Üksühene arvutuskoordinaatide ja ekraanipunktide vastavus võib küll mugav koodiks kirjutada olla ning esmase pildi selle abil ka saab, kuid selline telgede asetus võib arvutigraafikast kaugemal seisvale vaatajale harjumatu ning võõrastav tunduda. Samuti tekivad probleemid x-i ja y-i negatiivsete väärtuste juures, sest neid pole lihtsalt näha. Veidi mugavam peaks olema järgmine lähend: Üks ühik graafikul vastab endiselt ekraanipunktile. Joonist aga nihutati x-teljel 100 punkti võrra paremale ja y-teljel 200 punkti võrra allapoole ning y-telje kasvamise suund keerati vastupidiseks. Ekraani y-punkti arvutamisel lahutatakse kahesajast maha graafikul oleva igreki väärtus. Kui y=0, siis joonistatakse täpp rakendi y-koordinaadile 200. Kui y>0, siis tuleb 200-y kahesajast väiksem ning täpp ekraanil järelikult kõrgemal. Negatiivse y puhul ületab 200-y kahtsadat ning tulemuseks on täpp nullpunktist (kahesajast) allpool. import java.awt.*; import java.awt.geom.*; import java.awt.image.*; import java.applet.*; public class Graafik2 extends Applet{ public void paint(Graphics g){ for(int x=-100; x<100; x++){ g.drawOval(x+100, 200-x*x, 1, 1); } } public static void main(String argumendid[]){ Frame f=new Frame("Ruutfunktsiooni graafik"); f.add(new Graafik2()); f.setSize(250, 250); f.setVisible(true); } } Mõõtkava. Koodi sisse trükitud konstandid on asendatud muutujatega. Nii on vähe suurema programmi puhul kergem näha, millega tegu, sest muutujal on nimi. Samuti, kui sama suurust on kasutatud mitmes kohas, siis muutmisvajaduse korral saab väärtuse määrata ühes kohas ning jääb ära oht, et kusagilt midagi paika muutmata jääb. Põhjalikud programmeerijad soovitavad koguni kõik väärtused (v.a. 0 ja 1) asendada tähenduslike nimedega muutujatega. Siis teistel koodi lugejatel rohkem lootust aru saada. Ning mõnikord on mõistlik needki numbrid sõnadega asendada. Arvutustehe on toodud välja eraldi funktsioonina. Nii on teda kergem mitmest kohast vajadusel välja kutsuda, samuti muuta ja parandada kui vajadust peaks olema. Koefitsent näitab, mitu ekraanipunkti vastab ühele ühikule graafikul. Kui koefitsent on võrdne ühega, siis graafiku- ning ekraanipunktide arv kattub. Ühest suurema kordaja korral venitatakse ekraanil graafik vastava telje suunas välja. Kui kordaja on 0 ja 1 vahel, siis ekraanijoonist vähendatakse. Ning kui kordaja on alla nulli, siis joonistatakse ekraanil maailmakoordinaatidele vastupidises suunas. Siin näites y koefitsent -0.5 tähendab, et ekraani- ning graafikuühikud on eri suundades (miinusmärk ees) ning ekraanipunkte on poole vähem kui punkte graafikul. Nii on võimalik kiirelt kasvav ruutfunktsioon ekraanile paremini ära mahutada. import java.awt.*; import java.applet.*; public class Graafik3 extends Applet{ double minx=-10, maxx=10, samm=1; double koefitsientx=3, koefitsienty=-0.5; //mitu ekraanipunkti vastab ühele ühikule joonisel //miinus vahetab suuna int xnihe=100, ynihe=200; public void paint(Graphics g){ for(double x=minx; x<=maxx; x=x+samm){ g.drawOval(xnihe+(int)( x *koefitsientx), ynihe+(int)(f(x)*koefitsienty), 1, 1); } } double f(double x){ //funktsioon eraldi välja, siis kergem //kasutada ja muuta return x*x; } public static void main(String argumendid[]){ Frame f=new Frame("Ruutfunktsiooni graafik"); f.add(new Graafik3()); f.setSize(250, 250); f.setVisible(true); } } Pideva joonega graafik Endiste täppide asemel ühendatakse nüüd punktid omavahel joontega. Kui jooned piisavalt lühikesed on, siis jääb vaatajale mulje, nagu oleks tegemist kõveraga. Kõigepealt tuleb välja arvutada esimese punkti asukoht. Seejärel leida teise punkti asukoht ning kahe esimese punkti vahele tõmmata joon. Siis jäetakse viimatiarvutatud punkt meelde, arvutatakse järgmine, tõmmatakse taas joon ning jäetakse punkt meelde. Kuni ollakse jõudnud arvutatava lõigu lõpuni. import java.awt.*; import java.applet.*; public class Graafik4 extends Applet{ public void paint(Graphics g){ int vanax=90; int vanay=100; for(int x=-10; x<=10; x++){ int uusx=x+100; int uusy=200-x*x; g.drawLine(vanax, vanay, uusx, uusy); vanax=uusx; vanay=uusy; } } public static void main(String argumendid[]){ Frame f=new Frame("Ruutfunktsiooni graafik"); f.add(new Graafik4()); f.setSize(250, 250); f.setVisible(true); } } Komponendi suuruse arvestamine Head programmid pidavat suutma arvestada ressurssidega, mis neile kättesaadavad on. Et kui mäluga arvutis kitsas, siis hoitakse enam andmeid kettal ja lihtsalt arvutatakse mõnevõrra kauem. Või kui ekraanipinda vähem kasutada, siis jäetakse nähtavale vaid tähtsaimad lõigud. Püüame ka siin niimoodi teha. Raami suuruse muutmisel arvutatakse pilt uuesti-käivitatakse paint. Nagu näha, seal on töö mitmeks alalõiguks jagatud ning vaid otsesed joonistuskäsud paint'i sisse jäänud. Suurenduseks tarvilikud konstandid leitakse eraldi alamprogrammis, samuti paigutatakse joonise servadesse koordinaadid. Ka maailmakoordinaatide ja ekraanikoordinaatide teisendamiseks on omaette funktsioon loodud. Joonise tarvis arvutatakse välja kõigepealt vasakpoolseim x ja y. Edasi igal järgmisel korral tõmmatakse joon eelmisest punktist uude punkti ning jäetakse uus punkt eelmisena meelde. public void paint(Graphics g){ leiaKonstandid(); joonistaKoordinaadid(g); int vanax=ekraaniX(minx); int vanay=ekraaniY(f(minx)); for(double x=minx; x<=maxx; x=x+samm){ int uusx=ekraaniX(x); int uusy=ekraaniY(f(x)); g.drawLine(vanax, vanay, uusx, uusy); vanax=uusx; vanay=uusy; } } Joonistamisel arvestatakse ekraanipinna suurust ning nii x- kui y-teljel funktsiooni suurimaid ja vähimaid väärtusi. Selle järele arvutatakse koefitsendid. Siin näites antakse ette x-i vähim ja suurim väärtus ning funktsiooni kõvera punktid arvutatakse iga kahekümnendiku arvutuspiirkonna pikkuse tagant. Et funktsioon ega arvutusvahemik praegu programmi töö ajal ei muutu, saab suurimad ja vähimad väärtused leida juba rakenduse töö algul. double minx=-10, maxx=10, samm=(maxx-minx)/20; double miny=minY(), maxy=maxY(); Suurima y-i leidmiseks käiakse lihtsalt kogu funktsioon arvutussammu pikkuste lõikude tagant läbi ning jäetakse meelde suurim väärtus. Tegu pole küll matemaatiliselt päris korrektse lähenemisega, sest võib juhtuda, et kuhugi arvutuskoha vahele satub piik, kus funktsiooni väärtus erineb tunduvalt kõrval asuvatest väärtustest, kuid harilike funktsioonide puhul peaks sellisest lähenemisest piisama. double maxY(){ double max=f(minx); for(double x=minx; x<=maxx; x+=samm){ if(f(x)>max)max=f(x); } return max; } Kui vähimad ja suurimad väärtused olemas, siis edasi tasub leida muud joonistamisel tarvilikud kordajad. Ning põhiliseks määrajaks on sel korral kasutaja ette antud komponendi suurus, mille annab pärida getSize abil. Suurenduskordaja leidmiseks arvutatakse nii x- kui y-suunal väärtuste ulatus maailmakoordinaatides. Edasi leitakse, mitu ekraanikoordinaati selle ala peale jagub. Et ei tekiks jagamist nulliga, on ära määratud vähim ulatus, millest väiksemaks ei või maailmakoordinaatide vahemik minna. Samuti jäetakse ekraanil mõningane servaruum, et joonte otsad päris äärteni välja ei läheks. Ka leitakse funktsiooni nähtavale osale keskpunkt, et oleks võimalik joonis ekraanil suhteliselt ühtlaselt paigutada. void leiaKonstandid(){ korgus=getSize().height; laius=getSize().width; ulatusx=maxx-minx; ulatusy=maxy-miny; if(ulatusxmax)max=f(x); } return max; } void leiaKonstandid(){ korgus=getSize().height; laius=getSize().width; ulatusx=maxx-minx; ulatusy=maxy-miny; if(ulatusx0){ Component c=kest.getComponent(0); c.setBounds(20, 20, 60, 60); } } public void addLayoutComponent(String nimi, Component c){} public void removeLayoutComponent(Component c){} public Dimension preferredLayoutSize(Container kest){ return new Dimension(100, 100); } public Dimension minimumLayoutSize(Container kest){ return preferredLayoutSize(kest); } } Kuularid Sündmuste tulemusena millegi juhtumiseks tuleb sündmuse registreerijalt (ehk allikalt) saata teade sündmuse vastuvõtjale. Ühe sündmuse (näiteks nupuvajutuse) teadet saab saata mitmele vastuvõtjale, muuhulgas ka iseendale. Et allikas vastuvõtjale teate saadaks, selleks peab olema vastuvõtja end allika juures registreerinud vastavat tüüpi sündmuse kuulajaks. Kuulajaks saab end registreerida isend, kelle klass realiseerib vastava sündmusetüübi kuulamiseks loodud liidest. Näiteks hiirevajutuse kuulamiseks peab klass realiseerima liidest MouseListener. Liidese realiseerimine aga tähendab seda, et klassis oleks kirjeldatud kõigi liideses deklareeritud meetodite käivitamisel tehtavad tegevused. Näiteks MouseListeneri realiseerijal peavad olema kõik liideses kirjeldatud meetodid: nii hiire vajutamise, ülestõstmise, komponendi piirkonda sisenemise kui komponendist väljumise kohta. Kui soovitakse, et mõnel juhul ei reageeritaks, siis tuleb seegi arvutile selgeks teha, s.t. vastava meetodi sisuks kirjutada tühjad sulud. import java.awt.*; import java.awt.event.*; public class Hiir1{ public static void main(String argumendid[]){ Frame f=new Frame("Hiireraam"); f.setSize(200, 300); f.setVisible(true); f.addMouseListener(new HiireKuular1(f)); } } class HiireKuular1 implements MouseListener{ Frame raam; public HiireKuular1(Frame uusraam){ raam=uusraam; } public void mousePressed(MouseEvent e){ //suurendab raami laiust 10 ühiku võrra Dimension suurus=raam.getSize(); raam.setSize(suurus.width+10, suurus.height); } public void mouseReleased(MouseEvent e){} public void mouseClicked(MouseEvent e){} public void mouseEntered(MouseEvent e){} public void mouseExited(MouseEvent e){} } Et tühje sulge peaks vähem kirjutama, selleks on loodud adapterklassid liideste jaoks, mis defineerivad enam kui ühe meetodi. Vastava liidese adapterklass realiseerib liidest, jättes meetodi kehaks tühjad sulud, s.t. tehes meetodi käivitamisel mitte midagi. Kui me nüüd oma kuulari loome vastava adapteri alamklassina, siis piisab meil vaid üle katta meile vajalik meetod. Näiteks oma hiirekuulari loomisel katame üle vaid hiire vajutamisel käivitatava meetodi. Muude hiiresündmuste puhul kasutatakse adapterklassi meetodeid ning kuna seal midagi teha ei paluta, siis meile näib, nagu ei reageeritakski nendele sündmustele. import java.awt.*; import java.awt.event.*; public class Hiir2{ public static void main(String argumendid[]){ Frame f=new Frame("Hiireraam"); f.setSize(200, 300); f.setVisible(true); f.addMouseListener(new HiireKuular2(f)); } } class HiireKuular2 extends MouseAdapter{ Frame raam; public HiireKuular2(Frame uusraam){ raam=uusraam; } public void mousePressed(MouseEvent e){ Dimension suurus=raam.getSize(); raam.setSize(suurus.width+10, suurus.height); } } Komponent võib ka ise saata teateid enese juures juhtunud sündmustest ning neid siis töödelda. Sel juhul pole eraldi klassi (ega isendit) kuulari jaoks vaja luua. Piisab vaid, kui komponendi alamklass ise realiseerib vastava sündmuse kuulamiseks vajalikku liidest. Liidese realiseerimiseks peab aga meetodi keha olema kõikidel liideses kirjeldatud meetoditel. Sellest siin need tühjad meetodid, et programm teaks, et näiteks hiire sisenemise korral ei tule tal midagi teha. import java.awt.*; import java.awt.event.*; public class Hiir3 extends Frame implements MouseListener{ public Hiir3(){ addMouseListener(this); } public void mousePressed(MouseEvent e){ Dimension suurus=getSize(); setSize(suurus.width+10, suurus.height); } public void mouseReleased(MouseEvent e){} public void mouseClicked(MouseEvent e){} public void mouseEntered(MouseEvent e){} public void mouseExited(MouseEvent e){} public static void main(String argumendid[]){ Frame f=new Hiir3(); f.setSize(200, 300); f.setVisible(true); } } Kui aga ka komponendi alamklassis tahetakse kasutada adapteri võimalusi ning leitakse, et tühjade meetodite kirjutamine on aja ja ruumi raiskamine, siis tuleb veidi keerulisemalt hakkama saada. Kuna java keeles on võimalik pärida ainult ühelt eellaselt, siis üheaegselt nii Frame kui adapteri klassis paiknevat koodi pole võimalik pärida. Selgem ning kindlam on kirjutada eraldi adapteri alamklass ning ta konstruktorile anda parameetriks osuti komponendile nagu esimestes näidetes. Siis saab selle osuti kaudu komponendi tegevust juhtida. Versioonist 1.1 alates aga loodi võimalus sama probleemi ka lühemalt lahendada. Alljärgnevas näites luuakse adaptrile nimetu alamklass, kus kaetakse üle tema meetod, siis luuakse isend ning pannakse ta teateid kuulama. Meetod windowClosing kutsutakse välja siis, kui kasutaja vajutab raami sulgemise nupule. Selle tulemusena lõpetatakse programmi töö (System.exit). import java.awt.*; import java.awt.event.*; import java.awt.*; import java.awt.event.*; public class Raam4 extends Frame { public Raam4(){ addWindowListener( new WindowAdapter(){ public void windowClosing(WindowEvent e){ System.exit(0); } } ); } public static void main(String argumendid[]){ Frame f=new Raam4(); f.setSize(200, 300); f.setVisible(true); } } Tekstikuular Tekstikuulari liideses on kirjeldatud vaid üks meetod: textValueChanged. See meetod käivitatakse kuulajatel siis, kui allika (ehk tekstivälja või tekstiala) sees olev tekst on muutunud. Siin näites on rakend ühtlasi esimese tekstiala kuulariks. Kui esimeses tekstialas ehk allikas teksti muudetakse, siis selle tulemusena pannakse teise tekstiala sisuks esimese sisu koopia, kusjuures kõik tähed muudetakse väiketähtedeks. import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Tekstikuular extends Applet implements TextListener{ TextArea tekstiala1=new TextArea(3, 30); TextArea tekstiala2=new TextArea(3, 30); public Tekstikuular(){ add(tekstiala1); add(tekstiala2); tekstiala1.addTextListener(this); } public void textValueChanged(TextEvent e){ tekstiala2.setText(tekstiala1.getText().toLowerCase()); } } Klahvikuular Klahvikuular peab realiseerima liidest KeyListener ning temale saadetakse teated keyPressed, keyReleased ning keyTyped. Siin näites pööratakse tähelepanu vaid allavajutamise juhule. KeyEvent isendi meetod getKeyCode annab tulemuseks täisarvu, mis vastab klahvi koodile. Siis kontrollitakse, millise klahviga on tegemist ning toimitakse vastavalt sellele. Konstant KeyEvent.VK_LEFT tähendab näiteks noolt vasakule. Käsklus repaint() keyPressed meetodi lõpus käsib ekraani üle joonistada, s.t. kustutab vaja joonise ning käivitab siis eraldi lõimena meetodi paint. Sellise toimimise korral on ring õiges kohas ekraanil ka näiteks pärast rakendi nihutamist või teiste programmide alla sattumist. import java.applet.Applet; import java.awt.event.*; import java.awt.*; public class Klahvikuular2 extends Applet implements KeyListener{ int x=100, y=100; public Klahvikuular2(){ addKeyListener(this); } public void paint(Graphics g){ g.drawOval(x-10, y-10, 20, 20); } public void keyPressed(KeyEvent e){ int kood=e.getKeyCode(); if(kood==KeyEvent.VK_LEFT)x--; if(kood==KeyEvent.VK_RIGHT)x++; if(kood==KeyEvent.VK_UP)y--; if(kood==KeyEvent.VK_DOWN)y++; repaint(); } public void keyReleased(KeyEvent e){} public void keyTyped(KeyEvent e){} public static void main(String argumendid[]){ Frame f=new Frame("Klahvikuular"); f.add(new Klahvikuular2()); f.setSize(300, 300); f.setVisible(true); } } Fookusekuular Fookusekuular registreerib fookuse saabumise ning eemaldumise teated, s.t. nende sündmuste toimumisel käivitatakse vastavad meetodid. Siin näites lihtsalt omistatakse tõeväärtusmuutujale väärtus tõene või väär vastavalt sellele, kas rakend on fookuses või mitte. Seejärel palutakse ekraan üle joonistada. Ülejoonistamine sõltub muutuja väärtusest. Kui komponent on fookuses, siis joonistatakse esiplaanivärviga rakend üle. Kui aga komponent fookuses pole, siis jäetakse pind värvimata. import java.applet.Applet; import java.awt.event.*; import java.awt.Graphics; public class Fookusekuular extends Applet implements FocusListener{ boolean fookuses=true; public Fookusekuular(){ addFocusListener(this); } public void paint(Graphics g){ if(fookuses){ g.drawRect(0, 0, getSize().width-1, getSize().height-1); } } public void focusGained(FocusEvent e){ fookuses=true; repaint(); } public void focusLost(FocusEvent e){ fookuses=false; repaint(); } public static void main(String[] argumendid){ Frame f=new Frame("Fookuseraam"); f.add(new Fookusekuular()); f.setSize(200, 200); f.setVisible(true); } } Hiirekuular Hiire vajutuste ning hiire liikumise registreerimiseks on kummagi jaoks omaette kuular. Nii on kasulik seetõttu, et hiire liigutamisel tuleb teateid tunduvalt rohkem kui vajutamisel. Liigutamisel tuleb registreerida piisavalt palju punkte, et nende abil oleks võimalik kõverjoonelist liikumisteed ette kujutada. Järgnevas näites joonistatakse hiire vajutamise kohale punane ring, ülestõstmise kohale sinine ning kuid hiir väljub komponendi piirkonnast, siis joonistatakse viimase pind valge värviga üle. Vajutamise juures kirjutatakse ka, mitu korda selles punktis on hiireklahvi lühikeste vahedega vajutatud. Andmed selle kohta saab hiirevajutussündmuse puhul väljakutsutava meetodi MouseEvent tüüpi parameetrist. import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Hiirekuular extends Applet implements MouseListener{ public Hiirekuular(){ addMouseListener(this); } public void mousePressed(MouseEvent e){ Graphics g=getGraphics(); g.setColor(Color.white); g.fillRect(e.getX()-15, e.getY()-15, 30, 30); g.setColor(Color.red); g.drawOval(e.getX()-15, e.getY()-15, 30, 30); g.drawString(e.getClickCount()+"",e.getX(), e.getY()); } public void mouseReleased(MouseEvent e){ Graphics g=getGraphics(); g.setColor(Color.blue); g.drawOval(e.getX()-5, e.getY()-5, 10, 10); } public void mouseExited(MouseEvent e){ Graphics g=getGraphics(); g.setColor(Color.white); g.fillRect(0, 0, getSize().width, getSize().height); } public void mouseEntered(MouseEvent e){} public void mouseClicked(MouseEvent e){} } Hiire liikumise kuular Pildi joonistamiseks on vaja registreerida nii hiire vajutusi kui liikumist. Et pääseda hiirekuulari praeguses programmis mitte vaja minevate sündmuste töötlemisest, selleks loon oma kuulari klassi MouseAdapter alamklassina, mis samaaegselt realiseerib MouseMotionListener liidest (meeldetuletuseks: korraga võib laiendada vaid ühte klassi, kuid liideseid realiseerida kuitahes mitu). Hiire allavajutamisel jäetakse meelde vajutuse asukoht. Liigutamisel aga tõmmatakse joon eelmise punkti ning hetkel hiire asukohaks oleva punkti vahele. import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Joonistus extends Applet{ public Joonistus(){ JoonistuseKuular kuular=new JoonistuseKuular(this); addMouseListener(kuular); addMouseMotionListener(kuular); } } class JoonistuseKuular extends MouseAdapter implements MouseMotionListener{ Joonistus peremees; int vanax, vanay, uusx, uusy; public JoonistuseKuular(Joonistus j){ peremees=j; } public void mousePressed(MouseEvent e){ vanax=e.getX(); vanay=e.getY(); } public void mouseDragged(MouseEvent e){ uusx=e.getX(); uusy=e.getY(); Graphics g=peremees.getGraphics(); g.drawLine(vanax, vanay, uusx, uusy); vanax=uusx; vanay=uusy; } public void mouseMoved(MouseEvent e){} } Järgnev näide erineb eelmisest selle poolest, et pilt joonistatakse enne mällu ning alles sealt ekraanile. Sellisel juhul saab pildi ka pärast selle ekraanilt kadumist uuesti sinna tekitada. Pilt luuakse mällu paint-meetodi esmakordsel väljakutsel. Meetod update on üle kaetud, et joonistamine ilusamini välja näeks. Vaikimisi update enne joonistab komponendi taustavärviga üle ning alles siis joonistab sinna peale pildi. Kui aga paluda update'l kohe pilt joonistada, siis aitab see vältida vilkumist. import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Joonistus2 extends Applet{ Image pilt; public Joonistus2(){ Joonistuse2Kuular kuular=new Joonistuse2Kuular(this); addMouseListener(kuular); addMouseMotionListener(kuular); } public void paint(Graphics g){ if(pilt==null)pilt=createImage(getSize().height, getSize().width); g.drawImage(pilt, 0, 0, this); } public void update(Graphics g){ paint(g); } public static void main(String[] argumendid){ Frame f=new Frame("Joonistus"); f.add(new Joonistus2()); f.setSize(300, 300); f.setVisible(true); } } class Joonistuse2Kuular extends MouseAdapter implements MouseMotionListener{ Joonistus2 peremees; int vanax, vanay, uusx, uusy; public Joonistuse2Kuular(Joonistus2 j){ peremees=j; } public void mousePressed(MouseEvent e){ vanax=e.getX(); vanay=e.getY(); } public void mouseDragged(MouseEvent e){ uusx=e.getX(); uusy=e.getY(); Graphics g=peremees.pilt.getGraphics(); g.drawLine(vanax, vanay, uusx, uusy); vanax=uusx; vanay=uusy; peremees.repaint(); } public void mouseMoved(MouseEvent e){} } Graafikakomponendi loomine Omatehtud jooniste ning komponentide aluseks sobib lõuend (Canvas). Ta sobib lihtsaks joonistamiseks, kuid samas saab ta panna ka teateid vastu võtma, s.t. näiteks hiirevajutusele reageerima. Kui oled kord komponendi loonud, siis saad seda tervikuna kasutada seal kus parajasti vaja on. Kui oled komponendi tööga rahul, siis võid tarvitada teda ilma sisemisse ehitusse süvenemata. Alati ei pea komponendi loomisel kõike otsast tegema, vaid võib kasutada juba varem olemas olevaid tükke. Samuti võib luua varemvalmistatud komponendile alamklassi ning seal soovitud meetodid muuta. Nii võib kerge vaevaga lisada tekstialale võimaluse, et ta väljastaks oma sees oleva ridade arvu või paneks lisatavatele ridadele tühikud ette. Kui aga tahetakse sündmused ja kujundused täiesti ise määrata, siis tuleb aluseks võtta tühi pind ehk lõuend. Hulknurk Siin näites joonistatakse lõuendile soovitud nurkade arvuga hulknurk. Nurkade arvu saavad väljapoolsed muuta vaid meetodi abil. Iga muutmisega kaasneb uus joonistamine. import java.awt.*; public class Nurgad extends Canvas{ protected int nurkadearv; public Nurgad(){ nurkadearv=3; } public Nurgad(int uusarv){ nurkadearv=uusarv; } public void muudaNurkadeArv(int uusarv){ nurkadearv=uusarv; repaint(); } public void paint(Graphics g){ int korgus=getSize().height; int laius=getSize().width; double nurgavahe=2*Math.PI/(double)nurkadearv; int raadius=Math.min(korgus, laius)/3; int keskx=laius/2; int kesky=korgus/2; int vanax=keskx; int vanay=kesky+raadius; int uusx, uusy; for(int i=1; i<=nurkadearv; i++){ uusx=keskx+(int)(raadius*Math.sin(i*nurgavahe)); uusy=kesky+(int)(raadius*Math.cos(i*nurgavahe)); g.drawLine(vanax, vanay, uusx, uusy); vanax=uusx; vanay=uusy; } } } Komponendi kasutamine Loodud komponenti saab kasutada seal, kus vaja hulknurki joonistada. Siin näites pannakse rakendi ekraanile üheksa hulknurka, nurkade arvuga kolmest üheteistkümneni. import java.applet.Applet; import java.awt.Frame; public class Nurgarakend extends Applet{ public Nurgarakend(){ setLayout(new java.awt.GridLayout(3, 3)); for(int nr=3; nr<12; nr++){ add(new Nurgad(nr)); } } public static void main(String[] argumendid){ Frame f=new Frame("Hulknurkade näidised"); f.add(new Nurgarakend()); f.setSize(200, 200); f.setVisible(true); } } Samuti saab loodud komponendi abil lasta kasutajal valida, mitme nurgaga hulknurka soovib. Selleks panin rakendile kerimisriba ning loodud komponendi. Rakendi panin kerimisriba kuulajaks (AdjustmentListener). Kui kerimisriba määratud koha väärtust muudetakse, siis saadetakse uus väärtus rakendile, kes selle omakorda saadab hulknurka joonistavale komponendile. import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Nurgarakend2 extends Applet implements AdjustmentListener{ Nurgad ng=new Nurgad(); Scrollbar sb=new Scrollbar( Scrollbar.VERTICAL, 3, 2, 2, 20 ); public Nurgarakend2(){ setLayout(new BorderLayout()); add(sb, BorderLayout.WEST); add(ng, BorderLayout.CENTER); sb.addAdjustmentListener(this); } public void adjustmentValueChanged(AdjustmentEvent e){ ng.muudaNurkadeArv(e.getValue()); } public static void main(String[] argumendid){ Frame f=new Frame("Hulknurk"); f.add(new Nurgarakend2()); f.setSize(200, 200); f.setVisible(true); } } Andmete ülekanne Kopeerimine Programmide sees ning ka programmide vahel kasutatakse andmete vahetamiseks mälupuhvrit (clipboard). Sinna saab andmeid paigutada ning sealt vajadusel kopeerida. Kui puhver on operatsioonisüsteemi juures ning sinna pääsevad ligi mitmed programmid, siis saab selle abil nende vahel andmeid vahetada. Et andmeid sobiksid, peavad osapooled aru saama andmete formaadist. Kõige lihtsamaks formaadiks on lihtne tekst, kuid selle kaudu saab kõike vahetada, mida on võimalik baitideks muundada. Järgnevast näitest suurem osa kulub kujundusele, kus luuakse raam, pannakse sinna sisse tekstiväli, tekstiala ning menüü. Kopeerimine ja kleepimine asub meetodis actionPerformed, mis käivitub menüüst valiku tegemisel. Vastavalt menüürea nimele käivitatakse tegevus. Meetodi algul küsitakse juurdepääs operatsioonisüsteemi mälupuhvrile. Clipboard malu = getToolkit().getSystemClipboard(); Kui antakse korraldus Kopeeri, siis võetakse tekstiväljast tekst ning muudetakse StringSelection'iks. Viimatinimetatud klassis on tekst kujul, mida saab mälupuhvrisse panna ning mida teised programmid lugeda mõistavad. StringSelection ss = new StringSelection(tf.getText()); Seejärel öeldakse, et mingu see tekst mälupuhvrisse. Meetodi teiseks parameetriks on ClipboardOwner, kellele saadetakse teade puhvri sisu vahetumisest. Siin näites tegelikult nende teadetega midagi ette ei võeta. clipboard.setContents(ss, ss); Kleepimise puhul võetakse teade mälupuhvrist välja ning pannakse ta tekstialasse. Puhvrist saadakse andmed kätte esialgu tüübina Transferable, mis tuleb seejärel sobivaks tüübiks muundada. Transferable käest on võimalik küsida millisel kujul ta andmeid kannab. Siin aga eeldame, et tegemist on sõnega ning palume tal sellisena need andmed ka välja anda. Transferable andmed = clipboard.getContents(this); String s = (String)(andmed.getTransferData(DataFlavor.stringFlavor)); Tulemuseks on programm, mille abil saab andmeid tekstina programmide vahel vahetada. Selgitust vajab ka ehk menüü loomine. Algul luuakse menüüriba (MenuBar), sinna külge pannakse menüü(d) (Menu) ning viimasesse menüüread (MenuItem). Menüüridadele öeldakse (addActionListener), kellele nende peale vajutamisel teateid saata. import java.io.*; import java.awt.*; import java.awt.event.*; import java.awt.datatransfer.*; public class Tekstikopeering extends Frame implements ActionListener { TextField tf=new TextField(); TextArea ta=new TextArea(); public Tekstikopeering() { super("Kopeerimine"); add(tf, BorderLayout.NORTH); add(ta, BorderLayout.CENTER); MenuBar mb = new MenuBar(); mb.add(looMenyy()); setMenuBar(mb); } Menu looMenyy() { Menu m = new Menu("Parandused"); MenuItem mi = new MenuItem("Lõika"); mi.addActionListener(this); m.add(mi); mi = new MenuItem("Kopeeri"); mi.addActionListener(this); m.add(mi); mi = new MenuItem("Kleebi"); mi.addActionListener(this); m.add(mi); m.addSeparator(); mi = new MenuItem("Puhasta"); mi.addActionListener(this); m.add(mi); return m; } public void actionPerformed (ActionEvent e) { Clipboard malu = getToolkit().getSystemClipboard(); String kask = e.getActionCommand(); if (kask.equals("Kopeeri")) { StringSelection data = new StringSelection(tf.getText()); malu.setContents(data, data); } else if (kask.equals("Puhasta")) { tf.setText(""); } else if (kask.equals("Kleebi")) { Transferable andmed = malu.getContents(this); String s; try { s = (String)(andmed.getTransferData(DataFlavor.stringFlavor)); } catch (Exception viga) { s = viga.getMessage(); } ta.setText(s); } else if (kask.equals("Lõika")) { StringSelection ss = new StringSelection(tf.getText()); malu.setContents(ss, ss); tf.setText(""); } } public static void main (String argumendid[]) { Frame f=new Tekstikopeering(); f.setSize(300, 300); f.setVisible(true); } } Andmete vedamine (Drag and Drop) Lisaks mälupuhvri abil kopeerimisele püütakse andmete ülekannet ka hiirega vedamise abil kasutajale intuitiivsemaks muuta. Enamik meist on tõenäoliselt hiirega Word'i redaktoris sõnu lauses ringi tõstnud või Windows Exploreri aknas faile ühest kataloogist teise lohistanud. Andmete allikaks või suudmeks saab määrata ükskõik millise komponendi, kes on võimeline hiire teateid vastu võtma. Komponendile tuleb määrata sündmus, mille peale ta end andmete allikaks loeb. Sageli on selleks näiteks hiire vajutus ning lohisemine tema peal vähemalt viie punkti ulatuses. Kui andmed on kord liikuma pandud, saab nende "käekäigu" üle teateid andmeveo kuulari abil, kes teatab hiire sattumisest võimaliku vastuvõtja alasse. Andmete vastuvõtjalgi on kuular. Tema saab teateid enesele sattunud andmetega varustatud hiirest ning on võimeline vastavalt nendele teadetele käituma. Ta saab võrrelda pakutavat andmete tüüpi enese poolt vastu võtta suudetavate andmetüüpidega ning sellest kasutajale teadma andma. Kui hiire klahv lastakse vastuvõtja kohal lahti, siis saabub teade drop ning andmed võib vastu võtta. Siin näites luuakse raam kolme sildiga. Ülemised kaks on andmete allikaks ning nende siltide pealt vedama hakkamisel kaasneb andmetena sildi peal olev kiri. Kolmas silt on vastuvõtja. Kui selle peal vabastatakse andmeid kandva kursoriga hiire klahv, siis jääb saabunud tekst sildi sisse. Nii allika kui suudme olen loonud sildi alamklassina. AndmeveoAlguseKuularis on kirjas, mida tuleb teha, kui DragSource poolt loodud DefaultDragGestureRecognizer on märganud, et sildi pealt hakatakse andmeid vedama. Sel puhul võetakse sildi tekst, muutetakse ta Transferable tingimustele vastavaks StringSelection'iks, et teda saaks üle kanda ning siis käivitatakse vedu käsuga startDrag. Parameetriteks on vedamise ajal näidatav kursor, kantavad andmed ning kuular, kellele saadetakse teated andmetega teel toimuva kohta. Suudmel on isend DropTarget, kelle poolt loodud AndmeteSaabumiseKuular saabuvate andmetega tegeleb. Kui suudme kohal lastakse lahti andmehulk, saab selle tüüpide sobivuse korral vastu võtta. Esialgu küsitakse meetodi parameetrilt Transferable-tüüpi andmed, sealt oodatud kujul Objectina ning lõpuks tuleb nad kasutatavale kujule muudada. Siis võib nendega edasi toimida, siin näites andmete sees paiknev tekst oma sildile paigutada. import java.awt.*; import java.awt.dnd.*; import java.awt.datatransfer.*; public class Allikas extends Label { private DragSource dragSource; public Allikas(String s) { setText(s); dragSource = DragSource.getDefaultDragSource(); dragSource.createDefaultDragGestureRecognizer( this, DnDConstants.ACTION_COPY, new AndmeveoAlguseKuular()); } class AndmeveoAlguseKuular implements DragGestureListener { public void dragGestureRecognized(DragGestureEvent e) { Transferable andmed = new StringSelection( getText() ); e.startDrag(DragSource.DefaultCopyDrop, andmed, new AndmeveoKuular()); } } class AndmeveoKuular implements DragSourceListener { public void dragDropEnd(DragSourceDropEvent e) { } public void dragEnter(DragSourceDragEvent e) { } public void dragOver(DragSourceDragEvent e) { } public void dragExit(DragSourceEvent e) { } public void dropActionChanged (DragSourceDragEvent e) { } } } import java.awt.*; import java.awt.dnd.*; import java.awt.datatransfer.*; import java.io.*; public class Suue extends Label { private DropTarget dropTarget; public Suue(String s) { setText(s); dropTarget = new DropTarget(this, DnDConstants.ACTION_COPY, new AndmeteSaabumiseKuular(), true); } class AndmeteSaabumiseKuular implements DropTargetListener { public void dragOver(DropTargetDragEvent e) { } public void dropActionChanged(DropTargetDragEvent e) { } public void dragExit(DropTargetEvent e) { } public void dragEnter(DropTargetDragEvent e) { } public void drop(DropTargetDropEvent e) { try{ e.acceptDrop(DnDConstants.ACTION_COPY); Object data = e.getTransferable(). getTransferData(DataFlavor.stringFlavor); setText(data.toString()); }catch(Exception ex){ ex.printStackTrace(); } } } } import java.awt.*; public class Vedamine{ public static void main(String argumendid[]){ Frame f=new Frame("Andmeveo raam"); f.setLayout(new GridLayout(3, 1)); f.add(new Allikas("Karu")); f.add(new Allikas("Rebane")); f.add(new Suue("Vea siia!")); f.setSize(200, 100); f.setVisible(true); } } Trükkimine Lühike näide Lihtsaks trükkimiseks tuleb luua liidest Printable realiseeriv klass, kus meetodis print öeldakse, kuidas tuleb trükkida. Printerisse joonistamine käib samuti graafilise konteksti Graphics abil nagu ekraanile või mällugi joonistamine. Meetodi print parameetrina tuleva PageFormat'i abil saab teada, kui suure trükitava lehega tegemist on ning millisele osale lehest on võimalik joonistada. Enamasti on lehe serva määratud vaikimisi selline osa, kuhu joonistada ei saa. Joonistuskõlbuliku osa alguse x- koordinaadi annab getImageableX(). Samuti saab PageFormat'i käest küsida y-koordinaati ning joonistatava ala kõrgust ja laiust. Siin näites transleeritakse graafilise konteksti nullkoht joonistatava ala algusesse. Kolmanda parameetrina tulev leheküljenumber näitab, millist lehekülge soovitakse trükkida. Isendil on täiesti võimalik mitmele leheküljele trükkida. Lihtsalt tuleb meetodis print igale leheküljenumbrile vastavalt reageerida. Kui vastava numbriga lehekülge ei soovita trükkida, peab meetod tagastama väärtuse Printable.NO_SUCH_PAGE, muul juhul Printable.PAGE_EXISTS. Trükkimise käivitamiseks luuakse isend tüübist PrinterJob, määratakse, milline Printable oskusega isend trükitöö ära teeb ning siis palutakse trükkima hakata. import java.awt.print.*; import java.awt.*; public class Trykk1{ public static void main(String argumendid[]) throws PrinterException{ PrinterJob pj=PrinterJob.getPrinterJob(); pj.setPrintable(new Trykitoo1()); pj.print(); } } class Trykitoo1 implements Printable{ public int print(Graphics g, PageFormat pf, int lk) throws PrinterException{ if(lk>0) return Printable.NO_SUCH_PAGE; g.translate((int)pf.getImageableX(), (int)pf.getImageableY()); g.drawOval(10, 10, 200, 200); return Printable.PAGE_EXISTS; } } Komponendi trükkimine Kuna nii ekraanile kui printerisse joonistab klassi Graphics järglane, siis saab joonistamisel kasutada sama meetodit. Siin näites joonistatakse mõlemasse meetodi paint abil. Programm loob ekraanile nupu ning omaloodud komponendi Kiri2. Nupule vajutades trükitakse Kiri2 printerisse. Trükkimine on korraldatud nii, et vajutamise peale trükitakse Kiri2 tüüpi isendit 2 lehekülge, kummalegi lehele joonistatakse tema kujutis ning lehe alla kirjutatakse lehekülje number. Klassi PrinterJob meetod printDialog kutsub välja dialoogiakna, kust kasutaja saab määrata printerit ning väljastatavate lehekülgede numbreid ja koopiate arvu. import java.awt.*; import java.awt.event.*; import java.awt.print.*; class Kiri2 extends Canvas implements Printable { public void paint(Graphics g) { g.setColor(Color.black); int W = (int)getSize().getWidth(); int H = (int)getSize().getHeight(); g.drawRect(1, 1, W-3, H-3); g.drawString("Tere!", W/2, H/2); } public int print(Graphics g, PageFormat pf, int lk) throws PrinterException { if (lk >= 2) { return Printable.NO_SUCH_PAGE; } g.translate((int)pf.getImageableX(), (int)pf.getImageableY()); g.setColor(Color.black); paint(g); g.drawString("lk nr. "+(lk+1), 100, (int)pf.getImageableHeight()-50); return Printable.PAGE_EXISTS; } } public class Trykk2 extends Panel implements ActionListener { Kiri2 kiri=new Kiri2(); Button b = new Button("Tryki"); public Trykk2() { b.addActionListener(this); add(b); kiri.setSize(100, 50); add(kiri); } public void actionPerformed(ActionEvent e) { PrinterJob pj = PrinterJob.getPrinterJob(); pj.setPrintable(kiri); try { if(pj.printDialog()) pj.print(); } catch (Exception PrintException) { } } public static void main(String s[]) { Frame f = new Frame("Trykkimisraam"); f.add("Center", new Trykk2()); f.pack(); f.setSize(400,300); f.show(); } } Trükitava ala suuruse muutmine Trükitava ala suurust lehel saab ka ise muuta. Sel juhul tuleb PageFormat'i käest küsida Paper tüüpi isend, ja siis sinna määrata soovitud suurusega joonistusala. Edasi määrata PageFormat'ile vastav paber ning paluda PrinterJob'il vastava PageFormat'i järgi trükkida. Siin näites lubatakse trükkida lehel servast serva. import java.awt.print.*; import java.awt.*; public class Trykk3{ public static void main(String argumendid[]) throws PrinterException{ PrinterJob pj=PrinterJob.getPrinterJob(); PageFormat pf=pj.defaultPage(); Paper p=pf.getPaper(); p.setImageableArea(0, 0, p.getWidth(), p.getHeight()); pf.setPaper(p); pj.setPrintable(new Trykitoo3(), pf); pj.print(); } } class Trykitoo3 implements Printable{ public int print(Graphics g, PageFormat pf, int lk)throws PrinterException{ if(lk>0) return Printable.NO_SUCH_PAGE; g.drawOval(0, 0, 300, 200); return Printable.PAGE_EXISTS; } } Trükitava ala suurust saab lasta ka kasutajal dialoogiakna abil määrata. Sellise akna manab ekraanile pageDialog. public class Trykk3a{ public static void main(String argumendid[]) throws PrinterException{ PrinterJob pj=PrinterJob.getPrinterJob(); pj.setPrintable(new Trykitoo3a(), pj.pageDialog(pj.defaultPage())); pj.print(); } } Lisavõimalused Printable liidese abil saab trükkida ühesuguse suurusega lehekülgi. Kui peaks aga vaja olema ühte trükitavasse dokumenti kokku panna mitmesuguseid (näiteks püsti- ning põikiformaadis) lehti, siis tuleb algul panna kirjutatavatest lehtedest kokku Book ning seda trükkima hakata. Kokkuvõte Graafikakomponendid aitavad lihtsustada kasutajaga suhtlemist. Kümmekonda awt-paketis olevat komponenti juhitakse operatsioonisüsteemi poolt, nad näevad välja nii nagu vastavas operatsioonisüsteemis tavaks. Mõnikümmend swing-komponenti lisavad võimalusi. Need näevad välja igal pool ühtemoodi, töötavad suhteliselt aeglasemalt, kuid neid on kergem pilkupüüdvaks kujundada. Komponentidega juhtunud sündmuste töötlemiseks tuleb luua vastava sündmuse kuular ning kuularil lasta end sündmuse allika juures registreerida. Komponent võib ka ise olla enesega juhtunud sündmuste kuulariks. Lisaks kuularitele saab vajadusel ka uusi sündmusi ja komponente ise luua. Ülesandeid Paigutamine · Paiguta BorderLayout-i abiga nupp ülaserva ning tekstiala keskele. · Jaga ülaserv võrdselt kolme nupu vahel. · Paiguta allserva ühte ritta valik (choice) ja kerimisriba nii, et valik võtab tema jaoks hädavajaliku ruumi, riba aga ülejäänu. · Allserva teise ritta lisa märkeruut ning kolm üheskoos töötavat raadionuppu. Püüdmine · Hiirega vajutamise kohale joonistatakse ristkülik · Hiirevajutuse tulemusena hüppab ristkülik suvalisse kohta · Hiirega ristküliku tabamisel hüppab viimane suvalisse kohta. · Tekstiväljades loetakse, mitu tabamust on pihta, mitu mööda läinud. · Kasutajal on võimalik valida, kas tal tuleb püüda ruutu või ringi. Diagrammikomponent · Loodavale komponendile joonistatakse etteantud arvu kõrgune tulp. · Tulpade kõrgused antakse komponendile ette massiiviga. · Joonistamisel leitakse koefitsiendid nii, et suurima tulba pikkus oleks 80% komponendi kõrgusest. Graafilise liidesega võrgurakendused Võrguprotokoll, lõimed, klient, server, kuularid, dokumenteerimine, vektorgraafika Trips-traps-trull Kirjeldus Võrguprogrammi näiteks on siin kokku pandud Trips-traps-trulli mäng. Serveripoole ülesanne on kaks mängijat kohale oodata, siis kordamööda teineteisele käiguõigus anda ning saadetud käsud edasi saata. Samuti on serveripoole otsustada, kumba mängijat tähistatakse nulli ja kumba ristiga. Otsus on tehtud lihtne: esimene saabunud mängija saab märgiks X, teine 0. Server ise on koostatud nii, et kui üks paar on omaette lõime abil mängima saadetud, siis asub serveri peaprogramm ise järgmist kasutajat ootama ning paari kokku saamisel paneb need taas omavahel mängima. Edasine klientide vaheline vestlus on juba nende otsustada. Server seda otseselt ei määra ega kontrolli. Vaid kirjutamise järg antakse mängijatele kordamööda. Kui keegi mängijatest saadab lõputunnuse, siis lõpetatakse neid mängijaid teenindava lõime töö. Kliendi pool ühendab end serveriga ning asub sealt tulevaid teateid ootama. Kui öeldakse, millise sümboliga klient mängib, siis jäetakse see meelde. Kui teatatakse toimunud käigust, siis joonistatakse selle tulemus nuppudest moodustatud mängulauale. Kui oli tegemist mängu alguse teatega või vastase käiguga, siis muudetakse lubatud käigunupud aktiivseks, et kasutaja saaks sobiva valida. Vajutuse peale muudetakse nupud taas külmunuks, saadetakse nupule vastav käik serverisse ning edasi tegutsetakse vastavalt saabuvatele teadetele. Lõpetusnupule vajutades saadetakse lõpetusteade, mille peale server teab selle edasi saata mängupartnerile ning samas ka nende mängijatega suhtleva lõime sulgeda. Saadetavad käsud näevad välja järgnevad: Kuju Tähendus .margiksX Teate saanud klient teab edaspidi, et tema mängib ristidega. Kui olnuks .margiks0, tuleks nullidega mängida .kaikX5 Mängija X käis nupu 5. .kaik03 tähendanuks, et mängija 0 vajutas nuppu 3. .kaikVaba Teate saanud klient võib oma käiguga alustada. .ots Mäng on lõppenud Tutvustavad pildid Järgnevalt on mõned pildid ühest konkreetsest mängust. Masinas on käivitatud serverprogramm, üks klient käsurealt ning teine veebiseilurist. Serverisse on jõudnud mõlemad kliendid ühenduda (sellest teatavad serveri ekraanil sõnad Esimene ja Teine). Klient on serverist teateid kuulava lõime tööle saanud. Seiluriaknas olev klient jõudis ühenduda esimesena. Temale tuli kõigepealt teade .margiksX Oota paarilist, mille peale inimene teab, et tal tuleb partnerit oodata. Partneri saabumisel antakse sellele teada .margiks0 ning seejärel esimesele ühendunule .kaikVaba. Selle peale vabastatakse esimese kliendi nupud lukust ning tal on vaba voli käia. Mängijal on vaba voli valida üheksa mängunupu ning Stopp- nupu vahel. Nagu serveri konsoolilt näha, käis X-iga mängija kõigepealt nupule nr. 0 ning seejärel 0-ga mängija nupule 1. D:\Kasutajad\jaagup\java>java Tripsuserver Esimene Teine .kaikX0 .kaik01 Sama tulemus paistab ka mängulaudu vaadates. Saabuvad teated on lihtsalt kontrolli mõttes ülal asuvasse tekstivälja paigutatud. Nupud on aga samuti õigesti märgitud. Nii võib mäng jätkuda kuniks mängijad seda soovivad. Praeguse näite puhul veel võite ei loeta ning tulemusi ei kontrollita. Kes leiab, et ta enam midagi arukat käia ei taha ega oska, võib vajutada stopp. Selle peale katkestatakse serveris selle mängu lõim ning ka kaaslasele saadetakse teade lõpetamisest. Nupule ilmub kiri Start ning kui sellele vajutada, algab mängijate kohtumine otsast peale. Serveripoole lähtekood seletustega import java.io.*; import java.net.*; Võrguprogrammide puhul alati vajalikud paketid public class Tripsuserver{ Programmi töö käigus vajalikud konstandid. Kui nende väärtused ühte kohta kirjutada, siis on vajadusel nende väärtusi kergesti võimalik muuta. Näiteks kui juhtub vastava värati peal juba mõni programm jooksma, siis tuleb siia mõni vaba number määrata. Kui tegemist on käsurealt käivitatava kliendiga ning server jookseb kohalikust masinast väljaspool, siis tuleb vastava serveri nimi siia kirjutada, et klient teaks sinna ühenduda. Rakendi puhul pole siia märgitud masina nimi tähtis, sest rakend saab alati ühenduda vaid serverisse kust ta ise pärit ning selle aadress küsitakse töö käigus. static final int pordinr=3001; static final String masin="localhost"; static final String lopp=".ots"; Serveri main-meetod ei tegele lihtsuse mõttes eriolukordade töötlemisega vaid laseb veateated lihtsalt konsoolile trükkida. Selleks on kiri throws Exception meetodi päises. public static void main(String argumendid[]) throws Exception{ Asutakse väratit kuulama ServerSocket ss=new ServerSocket(Tripsuserver.pordinr); Jätkatakse igaveses tsüklis, st. senikaua kuni serverirakendus CTRL+C-ga kinni pannakse. Viisakam, kuid keerulisem võimalus oleks luua eraldi protokoll serveri juhtimiseks ning selle abil saata serverile teateid sulgemise või muude toimingute kohta. while(true){ Oodatakse esimese kliendi saabumist ning teatatakse talle, et tema märgiks on X. Serveri administraatorile antakse teada, et paarist esimene klient on saabunud. Socket sc1=ss.accept(); new PrintWriter(sc1.getOutputStream(), true).println(".margiksX "+ "Oota paarilist"); System.out.println("Esimene"); Sama lugu teise kliendiga. Socket sc2=ss.accept(); new PrintWriter(sc2.getOutputStream(), true).println(".margiks0"); System.out.println("Teine"); Luuakse Tripsuloim'e nimelisest klassist uus eksemplar ning antakse sellele kaasa juurdepääsuvõimalus mõlema kliendi ühendusele, et loodud isendil oleks võimalus hakata nende omavahelise suhtluse üle hoolt kandma. new Tripsuloim(sc1, sc2); } } Ning serveri peaklassil rohkem tööd polegi. } Tripsuloim'e ülesandeks on siis talle etteantud ühendustega suhtlema hakata ning hoolitseda, et nad omavahel saaksid mängu ilusti peetud. class Tripsuloim extends Thread{ Klassi sisse luuakse koht ühenduste andmete hoidmiseks. Lühiduse mõttes on tehtud Socket tüüpi massiiv. Sellisel juhul pole vaja kahte eraldi muutujat ning ühised operatsioonid (näiteks sulgemine lõpus) on võimalik tsükli abil läbi viia nii, et pole tarvilik kummagi pistiku jaoks eraldi käske välja kirjutada. Eeldatakse, et esimese mängija ühendus paigutatakse elemendiks sc[0] ning teise oma sc[1]. Socket[] sc=new Socket[2]; Konstruktor saab kahe pistiku andmed, talletab need kohalikku pistikühenduste massiivi ning start- meetodi käivitamise abil palub virtuaalmasinal käivitada run-nimeline meetod eraldi lõimena, mis peaks hoolt kahe saabunud ühenduse vahelise suhtlemise eest. Kuni siiani ootab veel klassis Tripsuserver olev peaprogramm iga täidetava käsu taga enne kui oma järjega edasi läheb. Kui aga konstruktor on läbitud, siis võib peaprogramm käsu new Tripsuloim(sc1, sc2); edukalt lõppenuks lugeda ning järgmise juurde asuda. Järgmiseks tuleb aga peaprogrammis hüpe tsükli algusse ning siis asutakse juba uut klienti ootama. Loodud Tripsuloim'e isend aga toimetab tasapisi run-meetodis edasi. public Tripsuloim(Socket usc1, Socket usc2){ sc[0]=usc1; sc[1]=usc2; start(); } Omaette lõimes töötav mängijapaarivahelist suhtlust korraldav meetod. public void run(){ Veapüünis, kuna üle kaetud run-meetodist pole võimalik erindeid throws käsuga välja lasta, samas aga mitmed võrguga seotud käsud nõuavad erindite töötlemist. Samuti on viisakas tekkivatele probleemidele reageerida. try{ Nii sisend- kui väljundvoogude tarbeks luuakse massiivid, et pärastpoole oleks nende voogude poole mugavam pöörduda. BufferedReader sisse[]=new BufferedReader[2]; PrintWriter valja[]=new PrintWriter[2]; for(int i=0; i<2; i++){ sisse[i]=new BufferedReader( new InputStreamReader(sc[i].getInputStream()) ); valja[i]=new PrintWriter(sc[i].getOutputStream(), true); } Esimesele kliendile teatatakse, et too võib oma käiguga alustada. valja[0].println(".kaikVaba"); Muutuja veel näitab, kas vastav lõim peab oma tööd jätkama. Kui klient saadab lõpetamisteate, siis muutuja väärtus läheb vääraks ning enam uut ringi teateid kuulama ei minda. boolean veel=true; while(veel){ Esimeselt kliendilt saabunud käik või lõpetamisteade saadetakse mõlemale edasi. Klient on koostatud nii, et korrektseks toimimiseks peab ta ka ise serverilt oma saadetud teated kätte saama. See on justkui kontroll, et ühendus serveriga töötab. String vastus=sisse[0].readLine(); kirjuta(valja, vastus); Kui esimeselt kliendilt saadi lõpetamisteade, siis teise kliendi teadet enam ei oodata. if(!vastus.equals(Tripsuserver.lopp)){ Muul juhul kirjutatakse ka teiselt saabunud teade mõlemale. vastus=sisse[1].readLine(); kirjuta(valja, vastus); } if(vastus.equals(Tripsuserver.lopp))veel=false; } Jõudnud tsüklist välja, suletakse ühendused mõlema kliendiga. for(int i=0; i<2; i++){ sc[i].close(); } }catch(Exception e){ System.out.println("Probleem:"+e); } } Abimeetod teate välja saatmiseks. Teade jõuab nii mõlemale kliendile kui serveri konsoolile. Piiritleja private meetodi sees teatab, et seda võib kasutada ainult sama klassi (Tripsuloim) isendi seest. private void kirjuta(PrintWriter[] pw, String teade){ for(int i=0; i0)serverinimi=argumendid[0]; Frame f=new Frame("Tripsuraam"); f.add(new Tripsupaneel(), BorderLayout.CENTER); f.setSize(300, 300); f.setVisible(true); } } Kujundus on Tripsupaneeli nimelisse klassi kokku pandud. Klass nimega Tripsuklient on vaid käivitamiseks. class Tripsupaneel extends Panel{ Kõik lehel nähtavad nupud on paigutatud ühisesse massiivi. Sellisena on neid kergem tsükli abil külmutada, sulatada või puhastada. Button nupp[]=new Button[10]; String omanimi; //X või 0, mille alt klient mängib TextArea suhtlus=new TextArea("",3, 60,TextArea.SCROLLBARS_VERTICAL_ONLY); public Tripsupaneel(){ setLayout(new BorderLayout()); add(suhtlus, BorderLayout.NORTH); Nupud omaette paneeli, kus 3*3 elementi. Panel nupupaneel=new Panel(); nupupaneel.setLayout(new GridLayout(3, 3)); Tsükli abil luuakse üheksa mängunuppu, igaüks neist lisatakse nii paneeli näitamiseks kui massiivi pärastiseks kergemaks ligipääsuks. for(int i=0; i<9; i++){ nupp[i]=new Button(" "); nupupaneel.add(nupp[i]); } add(nupupaneel, BorderLayout.CENTER); Massiivi viimane nupp mängu peatamiseks. Kui nupupaneel paigutati Tripsupaneeli keskele (kogu vabale alale), siis seiskamis/käivitusnupp pannakse Tripsupaneeli allserva, nupupaneeli alla. nupp[9]=new Button("Stopp"); add(nupp[9], BorderLayout.SOUTH); Kõik nupud külmutatakse. seaNupud(false); Käivitatakse serveri teadete kuulamiseks ning nende töötlemiseks omaette lõim. Lõimele antakse ette osuti loodud Tripsupaneelile, mille kaudu on omakorda võimalik ligi pääseda seal paiknevatele nuppudele ning tekstialale. TripsukliendiLoim vahendaja=new TripsukliendiLoim(this); } Alamprogramm kliendi nuppude külmutamiseks ning lahti sulatamiseks. Külmutamise korral külmutatakse kõik nupud. See toimub juhul, kui käigujärg läheb vastasele ning sel ajal pole server võimeline ühtki teadet vastu võtma. Lahti sulatamisel aga tehakse toimivaks vaid nupud, millel pole mingit kirja ehk mängunuppude puhul need, mis on veel vabad. void seaNupud(boolean kasutatav){ if(!kasutatav)for(int i=0; i0)serverinimi=argumendid[0]; Frame f=new Frame("Tripsuraam"); f.add(new Tripsupaneel(), BorderLayout.CENTER); f.setSize(300, 300); f.setVisible(true); } } class Tripsupaneel extends Panel{ Button nupp[]=new Button[10]; String omanimi; TextArea suhtlus=new TextArea("",3, 60,TextArea.SCROLLBARS_VERTICAL_ONLY); public Tripsupaneel(){ setLayout(new BorderLayout()); add(suhtlus, BorderLayout.NORTH); Panel nupupaneel=new Panel(); nupupaneel.setLayout(new GridLayout(3, 3)); for(int i=0; i<9; i++){ nupp[i]=new Button(" "); nupupaneel.add(nupp[i]); } add(nupupaneel, BorderLayout.CENTER); nupp[9]=new Button("Stopp"); add(nupp[9], BorderLayout.SOUTH); seaNupud(false); TripsukliendiLoim vahendaja=new TripsukliendiLoim(this); } void seaNupud(boolean kasutatav){ if(!kasutatav)for(int i=0; ijava ParooligaJututuba .pr 94 62 33 21 .pr 113 48 8 14 .pr 112 70 12 13 .pr 99 70 7 7 Joonistasin sulle maja pildi Väljund serveriaknas Kolmas arendusring. Järgnevalt on tahvli koodi täiendatud javadoc-ile sobivate kommentaaridega. Nii õnnestub ülevaatlikkuse tarbeks genereerida koodi kohta mõningane dokumentatsioon ilma selle jaoks eraldiseisvat juttu kirjutamata. Joonistussündmustega ümber käimiseks on lihtne JooniseKuular-liidese tüüpi muutuja asemele paigutatud nimistu jooniseKuularid, mis võib eneses hoida kuulajaid nõnda palju kui parajasti tarvilik on. LinkedList jooniseKuularid=new LinkedList(); Et kannataks tahvlile väljapoolt kuulajaid külge panna, selleks alljärgnev meetod. Töötab teine samal põhimõttel, kui näiteks nupu või tekstivälja puhul addActionListener. Ehk sündmuse allikaks olev objekt jätab enese juurde meelde, kuhu tuleb sündmuse toimumise puhul teated välja saata. public void lisaJooniseKuular(JooniseKuular j){ jooniseKuularid.add(j); } Saatmise puhul tuleb hoolitseda, et teade kõikide sooviavaldanuteni jõuaks. Kui ennist oli valida kas nulli või ühe kuulaja vahel, siis nüüd alampiiriks endiselt null, ülempiiri pole aga seatud. Tõsi küll, mõni klass võib liiga suure kuulajaks registreerunute arvu puhul anda TooManyListenersException'i. Nagu ikka, on korduvate tegevuste puhul abiks tsükkel. Nimistust soovitud järjekorranumbriga elemendi aitab kätte saada get. Et oleme nimistusse paigutanud vaid JooniseKuulareid, siis võime ka kindlad olla, et sama tüüpi elemente sealt kätte saame. void saada(){ if(jooniseKuularid.size()==0){return;} int laius=hyx-hax; int korgus=hyy-hay; for(int i=0; iparem)dx=-Math.abs(dx); if(y>all)dy=-Math.abs(dy); if(xmustripikkus)nihe=nihe-mustripikkus; g.drawImage(taust, 0, nihe-mustripikkus, this); } public void update(Graphics g){ paint(g); } public void start(){ veel=true; new Thread(this).start(); } public void run(){ while(veel){ repaint(); try{Thread.sleep(paus); }catch(Exception e){} } } public void stop(){ veel=false; } Image laePilt(String failinimi){ try{ return getImage(getCodeBase(), failinimi); }catch(Exception e){} return Toolkit.getDefaultToolkit(). getImage(failinimi); } public static void main(String argumendid[]){ Frame f=new Frame("Pildiraam"); Pildike1 ap=new Pildike1(); f.add(ap); f.setSize(300, 300); f.setVisible(true); ap.start(); } } Taust koos lilledega Nagu iga keerukama ülesande kallale tuleb asuda üksikute osade kaupa, nii ka siin on järgmiseks ette võetud lillede paigutamine taustale. Ning hoolitsetud, et lilled suhteliselt ühtlase tihedusega, kuid samas juhuslikult pinnale jaotuksid ning pinna suhtes paigal püsides inimese eest läbi liiguksid. Juurde on tulnud muutujad lilledega seotud andmete hoidmiseks. Lille kõrguse järgi saab arvestada millal ta ekraanile paistma hakkab, millal kaob ning hiljem ka leida, kas lill mõne muu alaga kattuma juhtub. Reaalarvuline lillelisamistõenäosus näitab, mitu lille luuakse keskmiselt iga sammu korral. Et väärtuseks on praegu 0.03*samm, tähendab, et iga kõrguse ekraanipunkti kohta tuleb keskmiselt 0.03 lille, ehk siis ligikaudu üks lill iga 33 punkti kohta. Lilli endeid hoitakse Vector'is; kollektsioonis, mille elemente võib vabalt lisada ja eemaldada ilma, et peaks ise muret tundma mälu eraldamise või vabastamise pärast. Aega arvutatakse ekraanipunktides. Et liikumine on ühtlase kiirusega, siis leidub ka kulunud ajaga võrdeline seos. Samahästi võiks muutujat nimetada kogunihkeks, ehk maapinna liikumise kogu punktide arvuks. Lillede haldamise tarbeks on loodud mitu alamprogrammi: lisamise, eemaldamise ja joonistamise tarbeks. Lillelisamistõenäosus näitas, mitu lille tuleb ühe joonistustsükli eel lisada. Kui see väärtus on näiteks 2,7, siis kaks lille lisatakse kindlasti, kolmas aga tõenäosusega 0,7 ehk 70%. Muutuja lt liigub tõenäosuse algsest väärtusest kuni nullini. Kuni lt väärtus on suurem kui 1, lisatakse lill kindlasti, sest Math.random()-i väljastatu on vahemikus 0<=x<1. Jääb aga lt väärtus alla ühe, siis sõltub Math.random'i tegevusest, kas tuleb veel lill või mitte. Lill luuakse uue objektina, millele jäetakse meelde tema x- koordinaat (muutumatu) ning aeg ehk maapinna liikumise teepikkus ekraanipunktides lille lisamise ajaks. Selle järgi on võimalik pärast arvutada, kus lill ekraanil peaks asuma, sest kõik lilled luuakse vaikimisi ülaserva. X-koordinaat leitakse juhuslikult pea kogu nähtava laiuse ulatuses. Kümme punkti võetakse laiusest x-i leidmisel maha, et ei tekiks lille, mis ekraanil sugugi näha poleks. Meeldejäetud x tähistab lille vasakut serva. void lisaUusiLilli(){ for(double lt=lillelisamistoenaosus; lt>0; lt=lt-1){ if(Math.random()korgus+lillekorgus){ lillekesed.removeElementAt(i); i--; } } } Joonistamine läks pikemaks. Ehkki paint näeb välja ilus lühike, on tema ülesannet täitma tulnud hulk abilisi. public void paint(Graphics g){ koostaPilt(); g.drawImage(pilt, 0, 0, this); } Lisaks taustale tuleb sisse lugeda ka lille pilt. Samuti ei joonistata tausta enam otse ekraanile, vaid pannakse mälus enne taust ja lilled uue pildi peale kokku ning alles seejärel näidatakse tulemus ekraanile. Mälupildi koostamiseks on createImage, mis jällegi rakendi puhul pole enne võimeline käivituma kui esimese paint-i ajal. Loodud pildi käest küsitud piltg jääb globaalsena üle kogu isendi kättesaadavaks, et ei pea muudele joonistavatele alamprogrammidele seda eraldi kätte jagama. Nagu näha, pärast iga sammu eemaldatakse vanad lilled, lisatakse uusi, joonistatakse taust mälupildi põhjaks ning lilled ükshaaval sellele. void koostaPilt(){ if(taust==null)taust=laePilt("rohetaust320x480.gif"); if(lill==null)lill=laePilt("lill1.gif"); if(pilt==null){ pilt=createImage(laius, korgus); piltg=pilt.getGraphics(); } nihe=nihe+samm; aeg=aeg+samm; if(nihe>mustripikkus)nihe=nihe-mustripikkus; eemaldaVanadLilled(); lisaUusiLilli(); piltg.drawImage(taust, 0, nihe-mustripikkus, this); joonistaLilled(); } Lillede joonistamisel käiakse lihtsalt läbi kõik vektoris olevad lilled ning paigutatakse nende andmete järgi lillepilt taustapildile. Arvutus aeg-lille algaeg näitab lille asukoha ekraanil. Lisaks võetakse maha veel lillekõrgus, et lill asuks nähtavasse alasse sisenema oma alumise poolega ja mitte ei tekiks järsku tervikuna inimese vaatevälja. void joonistaLilled(){ for(int i=0; imustripikkus)nihe=nihe-mustripikkus; eemaldaVanadLilled(); lisaUusiLilli(); piltg.drawImage(taust, 0, nihe-mustripikkus, this); joonistaLilled(); } void lisaUusiLilli(){ for(double lt=lillelisamistoenaosus; lt>0; lt=lt-1){ if(Math.random()korgus+lillekorgus){ lillekesed.removeElementAt(i); i--; } } } void joonistaLilled(){ for(int i=0; iputukasamm))putukax-=putukasamm; if((kood==KeyEvent.VK_RIGHT) && (putukaxputukasamm)putukay-=putukasamm; if(kood==KeyEvent.VK_DOWN && putukaymustripikkus)nihe=nihe-mustripikkus; piltg.drawImage(taust, 0, nihe-mustripikkus, this); piltg.drawImage(putukas, putukax, putukay, this); } public void start(){ veel=true; new Thread(this).start(); } public void run(){ while(veel){ repaint(); try{Thread.sleep(paus); }catch(Exception e){} } } public void stop(){ veel=false; } public void keyPressed(KeyEvent e){ int kood=e.getKeyCode(); if((kood==KeyEvent.VK_LEFT) && (putukax>putukasamm))putukax-=putukasamm; if((kood==KeyEvent.VK_RIGHT) && (putukaxputukasamm)putukay-=putukasamm; if(kood==KeyEvent.VK_DOWN && putukaymustripikkus)nihe=nihe-mustripikkus; eemaldaVanadLilled(); lisaUusiLilli(); kontrolliImemisi(); piltg.drawImage(taust, 0, nihe-mustripikkus, this); joonistaLilled(); piltg.drawImage(putukas, putukax, putukay, this); } void lisaUusiLilli(){ for(double lt=lillelisamistoenaosus; lt>0; lt=lt-1){ if(Math.random()korgus+lillekorgus){ lillekesed.removeElementAt(i); i--; } } } void kontrolliImemisi(){ int kauguseruut=imemiskaugus*imemiskaugus; for(int i=0; iputukasamm))putukax-=putukasamm; if((kood==KeyEvent.VK_RIGHT) && (putukaxputukasamm)putukay-=putukasamm; if(kood==KeyEvent.VK_DOWN && putukay=0 && x=0 && ymustripikkus)nihe=nihe-mustripikkus; if(sees(putukax+putukaxkiirus, putukay+putukaykiirus, laius-putukalaius, korgus-putukakorgus)){ putukax+=putukaxkiirus; putukay+=putukaykiirus; } else { putukaxkiirus=putukaykiirus=0; } } Reageering klahvivajutusele on mõnevõrra muutunud. Kui ennist muutus vajutusel putuka asukoht, siis nüüd püütakse muuta ta liikumise kiirust. Näitena näha vasak nooleklahv. Kui putukas liikus ennist vasakule (st. putukaxkiirus oli nullist väiksem) sel juhul klahvile vajutades vasakule liikumise kiirus kasvab kiirusesammu võrra, ehk kiirusest lahutatakse kiirusesammu väärtus. Kui aga putukas juhtus enne paigal seisma või paremale liikuma, siis määratakse uueks kiiruseks -kiirusesamm, ehk sama palju, kui muidu iga vajutamisega kiirust juurde tuleb. Nõnda ei pea kiiruse suuna muutmisel liialt aega kulutama pidurdamisele, vaid võib küllalt järsku teises suunas liikuma hakata, nagu putuka lennu puhul ikka näha võib. public void keyPressed(KeyEvent e){ int kood=e.getKeyCode(); if(kood==KeyEvent.VK_LEFT){ if(putukaxkiirus<0)putukaxkiirus-=kiirusesamm; else putukaxkiirus=-kiirusesamm; } //... } Juurde tuli mõningane statistika, et kasutajal oleks näha, kaugele ta mänguga jõudnud on. Kulunud aja arvutamiseks küsitakse käivitamise ajal eraldi muutujasse alghetke väärtus. Tegemist küllalt suure ja väheütleva arvuga: millisekundeid alates aastast 1970. long alghetk=new Date().getTime(); Kui nüüd aga millalgi uuesti süsteemi käest aeg küsida, siis nende kahe väärtuse vahe ütleb, palju mängu algusest aega kulunud on. void joonistaStatistika(){ String st="Lilli: "+lilli+" Imetud: "+imetud+ " Kadunud: "+kadunud+" Aeg: "+(new Date().getTime()-alghetk)/1000; piltg.setColor(Color.white); piltg.drawString(st, 30, korgus-10); } Rakendusele pandi kaasa heli. Pidevalt korduv linnutaust, et veidigi tekiks mulje niidul lendavast putukast ning märku andev sahin, kui putukas lillest nektari kätte sai. Taustaga on lihtsam. Kui esimest korda joonistama asutakse, siis palutakse muutujasse laadida linnutaust ning käsu loop abil pannakse too korduvalt end ketrama. if(linnutaust==null){ linnutaust=laeKlipp("linnutaust.au"); linnutaust.loop(); } Lille tabamise sahina saaks ka lihtsamal juhul laadida ning käsuga play mängima panna. Kui aga lilli palju ning sahinaheli vähegi pikem, siis kipuvad järjestikustel tabamustel helid kattuma ning üksikud tabamused ei eristu kõrvale ilusti ja loendatavalt. Sahinahelide eristamiseks loodi omaette klass. Lähem kirjeldus näha juba kommentaarides. /** * Lõimeklass, mille abil saab meloodiajuppe mängida. Sünkroniseerimise abil hoolitsetakse, * et mängualguste vahel oleks vähemalt poolesekundiline paus. Luku * (loa)ga sünkroniseeritud plokki pääseb korraga vaid üks lõim. Ülejäänud * peavad selle ees ootama, kuni eelmine on plokist väljunud. */ class Piiksuja extends Thread{ static Object lukk=new Object(); static java.applet.AudioClip sahin; public void run(){ synchronized(lukk){ sahin.play(); try{Thread.sleep(500);}catch(Exception e){} } } } Nagu näha, on Piiksuja nii lukk kui sahin staatilised muutujad. See tähendab, et neile on võimalik ligi pääseda sõltumata klassi eksemplaride arvust. Lukuobjekt luuakse programmi käivitamisel ning sama isend on kättesaadav kõigile klassi isenditele - nõndamoodi saab selle järgi sünkroniseerimisel otsustada, et korraga ei juhtuks mitu sahinat mängima. Piiksuja sahin laetakse koos muude klippidega ja paigutatakse sinna staatilisse muutujasse. if(Piiksuja.sahin==null) Piiksuja.sahin=laeKlipp("sahin.au"); Kui nüüd käivitamise juures jõutakse niikaugele, et on paras aeg sahistada, siis luuakse lõimeklassist Piiksuja uus eksemplar ning palutakse start abil eraldi lõimes ta run käivitada. if(!l.kasTyhi() && (xkaugus*xkaugus+ykaugus*ykaugus)mustripikkus)nihe=nihe-mustripikkus; if(sees(putukax+putukaxkiirus, putukay+putukaykiirus, laius-putukalaius, korgus-putukakorgus)){ putukax+=putukaxkiirus; putukay+=putukaykiirus; } else { putukaxkiirus=putukaykiirus=0; } } void koostaPilt(){ if(taust==null)laeKlipid(); arvutaAsukohad(); eemaldaVanadLilled(); lisaUusiLilli(); kontrolliImemisi(); piltg.drawImage(taust, 0, (int)nihe-mustripikkus, this); joonistaLilled(); joonistaStatistika(); piltg.drawImage(putukas, (int)putukax, (int)putukay, this); } void lisaUusiLilli(){ for(double lt=lillelisamistoenaosus; lt>0; lt=lt-1){ if(Math.random()korgus+lillekorgus){ if(!l.tyhi)kadunud++; lillekesed.removeElementAt(i); i--; } } } void kontrolliImemisi(){ int kauguseruut=imemiskaugus*imemiskaugus; for(int i=0; i=0 && x=0 && y0)putukaxkiirus+=kiirusesamm; else putukaxkiirus=kiirusesamm; } if(kood==KeyEvent.VK_UP){ if(putukaykiirus<0)putukaykiirus-=kiirusesamm; else putukaykiirus=-kiirusesamm; } if(kood==KeyEvent.VK_DOWN){ if(putukaykiirus>0)putukaykiirus+=kiirusesamm; else putukaykiirus=kiirusesamm; } } public void keyReleased(KeyEvent e){} public void keyTyped(KeyEvent e){} Image laePilt(String failinimi){ try{ return getImage(getCodeBase(), failinimi); }catch(Exception e){} return Toolkit.getDefaultToolkit().getImage(failinimi); } AudioClip laeKlipp(String failinimi){ return getAudioClip(getDocumentBase(), failinimi); } } Esimesed lilled Kaks imetud, kaks silmist mööda läinud Tihedaks määratud lilleväli Edasiarendusvõimalused Loodud rakendus küll töötab ning on mõningase näpuharjutusena mängitav, kuid pole ta veel ei terviklik mäng ega saa seda kuigivõrd asjalikult ka mõneks muuks otstarbeks kasutada. Olemasolevat koodi vähemalt osalt mõistes on võimalik küllalt vähese vaeva abil koodi toimimist ja rakenduse väljanägemist täiesti märkimisväärselt muuta. Lihtsaimaks asenduseks ehk pildid ja heliklipid. Kui joonistada või leida muru ja lillede abil miskit muud, siis annab kergesti kokku panna koristaja maanteel või elevandi portselanikaupluses. Üks üliõpilane aga kujundas piltide muutmise abil sellest samast näitest aga näiteks küllaltki verise sõjamängu. Kui näidet mänguks kujundada, siis tõenäoliselt tuleb üsna pea lisada punktidearvutus ja mängu lõpp, ehk ka tasemed. Punkte kannatab kuvada statistikafunktsiooni veidi muutes. Iseseisva rakenduse puhul annab vahetulemusi faili salvestada. Rakendi puhul on turvapiirangute tõttu see veidi keerulisem, kuid kel PHP või muu veebiserveripoolse programmeerimisvahendiga kokkupuuteid ja kasutusvõimalusi, siis on ka see täiesti tehtav. Ilus võistlusmoment tekib, kui ühe putuka asemel lendleb kaks, kummagi juhtimiseks omaette klahvid. Ning samuti võib saabuvaid pilte olla mitut tüüpi. Nii magusaid mesikaid, õiteta okaspuid kui muude vahele ära eksinud kärbsepabereid. Näide ei pea aga mitte ainult mängu aluseks olema. Mõningase muutmise teel saab selle põhjal koostada katseseadme pidurdusteekonna pikkuse või molekulide kaootilise liikumise näitamiseks. Kõige rohkem läheb vaja pealehakkamist. Ülesandeid Sirelasemäng Alustuseks on kasutada mängu põhi · Tutvu mänguga · Lae lähtekood, pildid ja helilõigud oma arvutisse, kompileeri ja käivita. · Otsi Internetist pilte ning kujunda nende abil mäng oma silma järgi. · Lisa punktidearvutus. · Loo mängule lõpp. · Luba samaaegselt liikuda kahel mängijal Klahvidega liigutamine. · Nooleklahvidega saab liigutada ekraanil olevat ringi. · Ekraanil juhuslikus kohas paikneb rist. Jõudes ringiga selleni, hüppab rist uude juhuslikku kohta. · Liigutatavaid ringe on kaks, kummalgi oma klahvid. · Risti asemel on pisike pilt. All servas on kirjas, mitu korda kumbki mängija on ristini jõudnud. Tennis · Võrguga tenniseväljakul liigub pall (ring) vasakult paremale. · Samaaegselt ringi liikumisega saab ekraanil liigutada reketit. · Kummalgi serval on liigutatav reket, mille tabamisel pall tagasi põrkab. Loetakse punkte. Ussimäng · 10*10 ruudustikul asub vasakul ülaservas neljalüliline uss, keda saab nooleklahviga paremale liigutada. · Uss liigub pidevalt, nooleklahvidega saab tema liikumise suunda määrata. Seinani jõudmisel uss seiskub. · Ussiga püütakse juhuslikus kohas asuvat kuldmuna. Muna kättesaamisel uss pikeneb kahe lüli võrra ning muna tekib uude kohta. Seina või enese hammustamisel uss lüheneb nelja lüli võrra. Tilgapüüdja · All servas saab liigutada kaussi · Ekraanil kukub tilk. Selle kaussi püüdmisel saab punkti. · Tilku võib korraga kukkuda mitu. Mäng on meeldivalt kujundatud ning töötab märgatavate vigadeta. Nooltega ralli. · Noolte abil saab muuta ekraanil liikuva ringi kiirust. · Külgmiste nooltega saab muuta liikumise suunda, teiste nooltega kiirust. · Korraga võib üle võrgu sõita kaks kasutajat. Aetakse taga juhuslikus kohas paiknevat aaret, mis selleni jõudmisel hüppab uude kohta. Graphics2D Graafiline kontekst, joone ja tausta omadused, kujundid. Põhilised joonistamisfunktsioonid on klassis java.awt.Graphics. Alates versioonist 1.2 pakub laiendatud joonistamisvõimalusi eelmise alamklass Graphics2D. Kui esimesel juhul tuli alati arvutada ekraanikoordinaatides ning joone laiuseks oli üks punkt, siis siin võib valida omale sobiva taustsüsteemi ning ka joonistamisel saab enam parameetreid määrata. Kuna Graphics2D on klassi Graphics alamklass, siis ta oskab kõike mida eellanegi ning vajadusel saab temaga joonistada samade meetoditega, mis klassist Graphics omale sisse harjunud. Ühilduvuse huvides on komponendi meetodi paint parameetriks endiselt Graphics, kuid tegelikult antakse sellesse meetodisse joonistamiseks isend, kes suudab ka Graphics2D klassis kirjeldatud operatsioone täita. Juhul, kui spetsiifilisi omadusi vajatakse, tuleb muutuja tüüp enne teisendada. Joone omadused Joone tõmbamisel saab Graphics2D juures klassi BasicStroke abil määrata joone laiust, otsa kuju ning kahe joone ühendust (võimalused leiad API dokumentatsioonist). import java.awt.*; public class Joon1 extends Canvas{ public void paint(Graphics alggr){ Graphics2D g=(Graphics2D)alggr; float laius=15; int jooneots=BasicStroke.CAP_ROUND; int uhendus=BasicStroke.JOIN_BEVEL; g.setStroke(new BasicStroke( laius, jooneots, uhendus)); g.drawRect(10, 10, 70, 100); g.drawLine(100, 10, 140, 200); } } Punktiirjoon Punktiiri puhul tuleb määrata, millise pikkusega on punktiiri kriipsud, lisaks sellele hulk joontega seotud andmeid. Kuid kui korra on sulepea (Stroke) valmis tehtud, võib sellega rahumeeli jooni tõmmata nii palju kui vaid soovid – nii nagu eelmises näites. Kui vaadata sulepea koostamise käsklust BasicStroke bs1=new BasicStroke( 15, ots, yhendus, yhendusemaxpikkus, punktiir, punktiirinihe); siis 15 tähendab tõmmatava joone laiust. Ots näitab, millised (ümarad, kandilised) tuleb joone otsad teha. Muutuja yhendusemaxpikkus on enamasti tarbetu, vaja võib teda minna vaid siis, kui miskis kujundis ühinevad jooned väga väikese nurga all ning tekib oht pika väljaulatuva nurga moodustumiseks. Siis selle muutuja järgi vaadatakse, mitmest punktist vastav nurk pikemaks ei tohi minna. Eelnevalt loodud massiiv punktiir näitab, kuidas punktiirjoones vahelduvad kriipsud ja vahed. Nagu siin näites eespoolt võib piiluda, on esimese kriipsu pikkuseks määratud 5 punkti, sellele peaks järgnema viieteistpunktiline vahe. Edasi kümnepunktiline kriips ning selle järele kahekümnepunktine vahe. Siis taas algusest peale, et kahekümnepunktise vahe järele jälle viiene kriips, siis viieteistkümnene vahe ning nõnda edasi. Muutujast punktiirinihe vaadatakse, kus kohalt mustriga peale hakata. Kui nihe on 0, siis algab esimene kriips joone otsast. Kui mõni suurem arv, siis on esimese kriipsu algus vastavalt nihutatud. Teine BasicStroke on loodud ühe käsuga, enamjaolt on väärtused otse konstruktorisse kirjutatud. new float[]{15} loob üheelemendilise massiivi otse kohapeal. import java.awt.*; import java.awt.geom.*; import java.applet.*; public class Punktiir1 extends Applet{ float laius=5; int ots=BasicStroke.CAP_ROUND, yhendus=BasicStroke.JOIN_MITER; float yhendusemaxpikkus=10; float[] punktiir={5, 15, 10, 20}; //kriips, vahe, kriips, vahe, ... float punktiirinihe=0; BasicStroke bs1=new BasicStroke( 15, ots, yhendus, yhendusemaxpikkus, punktiir, punktiirinihe); BasicStroke bs2=new BasicStroke(laius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, yhendusemaxpikkus, new float[]{15}, 2); public void paint(Graphics g){ Graphics2D g2=(Graphics2D)g; Line2D l1=new Line2D.Float(10, 10, 10, 200); Rectangle2D r1=new Rectangle2D.Float(40, 10, 150, 190); g2.setStroke(bs1); g2.draw(l1); g2.setStroke(bs2); g2.draw(r1); } public static void main(String argumendid[]){ Frame f=new Frame("Punktiir"); f.add(new Punktiir1()); f.setSize(250, 250); f.setVisible(true); } } Joonistusala piiramine Joonistamisala on võimalik piirata käsuga clip, andes ette kujundi, mille piires tohib joonistada. Siin näites määratakse joonistamise alaks ellipsi pind ning seejärel joonestatakse seest täidetud ristkülik. Tulemusena tekib ekraanile vaid ellipsi ning ristküliku ühisosa. import java.awt.*; import java.awt.geom.*; public class Kujund1 extends Canvas{ public void paint(Graphics alggr){ Graphics2D g=(Graphics2D)alggr; Shape kujund= new Ellipse2D.Float(10, 10, 300, 100); g.clip(kujund); g.fillRect(20, 20, 400, 60); } } Venitamine, keeramine Joonistuspinda saab liigutada, venitada ja keerata. AffineTransform'i abil saab määrata, kuidas ja kui palju. Esimese näite puhul lükatakse koordinaatide alguspunkti saja ühiku võrra paremale ning alla. import java.awt.*; import java.awt.geom.AffineTransform; public class Transform2 extends Canvas{ public void paint(Graphics alggr){ Graphics2D g=(Graphics2D)alggr; g.setTransform(AffineTransform.getTranslateInstance(100, 100)); g.fillRect(20, 20, 40, 20); } } rida g.setTransform(AffineTransform.getScaleInstance(3, 1.5)); suurendab joonist x-telje suunas 3 ning y-suunas 1,5 korda. g.setTransform(AffineTransform.getRotateInstance(Math.PI/4, 100, 75)); keerab joonist Pi/4 ehk 45 kraadi võrra ümber punkti 100, 75 g.setTransform(AffineTransform.getShearInstance(Math.PI/4, 0)); keerab püsttelge Pi/4 võrra. Kui soovida mitut muutust üheskoos, siis võib luua AffineTransform tüüpi isendi ning talle soovitud muutusi järjekorras rakendada. import java.awt.*; import java.awt.geom.AffineTransform; public class Transform6 extends Canvas{ public void paint(Graphics alggr){ Graphics2D g=(Graphics2D)alggr; AffineTransform tr=new AffineTransform(); tr.rotate(Math.PI/4, 50, 100); tr.translate(150, 0); g.setTransform(tr); g.fillRect(20, 20, 40, 20); } } Värviüleminek Joonistamisel saab lisaks ühele värvile soovi korral pinda katta ka nii värviülemineku kui piltidest koostatud mustriga. Soovitud katmisstiil tuleb määrata Graphics2D meetodiga setPaint. Värviülemineku andmeid kannab GradientPaint. Konstruktoris tuleb määrata kaks punkti ning kummagi punkti juurde kuuluv värv. Nendes punktides vastab joonise värv sinna määratud värvile, punkte ühendaval sirget mööda muutub värv sujuvalt ühest värvist teiseks. Tavajuhul jääb kummagi punkti "selja taha" punktile vastav värv, kuid kui lisada konstruktorisse tõeväärtusmuutuja, siis saab panna värvi tsükliliselt lainetama nii, et laine pikkuseks jääb kahe punkti vahe. import java.awt.*; import java.awt.geom.*; public class Kujund2a extends Canvas{ public void paint(Graphics alggr){ Graphics2D g=(Graphics2D)alggr; g.setPaint(new GradientPaint(0, 100, Color.yellow, getSize().width, 100, new Color(0, 200, 100) )); g.fillRect(0, 0, getSize().width, getSize().height); } } Muster Et taustaks saaks mustrit panna, selleks tuleb kõigepealt mustripilt koostada või välja otsida ning alles seejärel saab määrata, et joonistatava kujundi värviks on pidevalt korduv loodud muster. Järgnevas näites on mustritükiks lihtsalt roheline ring mustal taustal. BufferedImage bi hoiab eneses loodavat pilti, TexturePaint tp aga juba hoolitseb, kuidas pilt korralikult õigesse kohta paigutada. Konstruktoris luuakse pildile graafiline kontekst, mille abil joonistatakse pildile 20*20 punkti laiune roheline ring. Rida tp=new TexturePaint(bi, new Rectangle(0, 0, 20, 30)); tähendab, et olemasolev pilt (algse suurusega 20*20 punkti) asub loodud TexturePaint'is olema 20 punkti lai ning 30 kõrge, seega pikkust pidi välja venitatud. Joonistuskäsu paint sees võetakse pakutud Graphics vastu Graphics2D-na, et õnnestuks vajalikke käske (setPaint) kasutada. Edasi joonistatakse pinnale ristkülik. Kuna aga joonistusmustriks oli määratud pilt, siis loodud ristkülik näebki välja mustrilisena. import java.awt.*; import java.awt.geom.*; import java.awt.image.*; import java.applet.*; public class Mustritaust extends Applet{ BufferedImage bi=new BufferedImage( 20, 20, BufferedImage.TYPE_INT_RGB); TexturePaint tp; public Mustritaust(){ Graphics2D big=bi.createGraphics(); big.setColor(Color.green); big.drawOval(0, 0, 20, 20); tp=new TexturePaint(bi, new Rectangle(0, 0, 20, 30)); } public void paint(Graphics g){ Graphics2D g2=(Graphics2D)g; Rectangle2D r1=new Rectangle2D.Float(30, 10, 150, 190); g2.setPaint(tp); g2.fill(r1); } public static void main(String argumendid[]){ Frame f=new Frame("Muster taustaks"); f.add(new Mustritaust()); f.setSize(250, 250); f.setVisible(true); } } Värviüleminekuga tekst Joonistusala piiramist ning värviüleminekut kombineerides saab päris keeruka ja ilusa kujundusega pildi kokku panna. import java.awt.*; import java.awt.geom.*; import java.awt.font.*; public class Kujund2 extends Canvas{ public void paint(Graphics alggr){ Graphics2D g=(Graphics2D)alggr; TextLayout tl=new TextLayout("Kassikene", new Font("Arial", Font.PLAIN, 100), new FontRenderContext(null, false, false) ); g.setPaint(new GradientPaint(200, 0, Color.yellow, 200, getSize().height, new Color(255, 200, 0) )); g.fillRect(0, 0, getSize().width, getSize().height); g.clip(tl.getOutline(AffineTransform.getTranslateInstance(20, 120))); g.setPaint(new GradientPaint(0, 100, new Color(66, 66, 0), getSize().width, 100, new Color(0, 200, 100) )); g.fillRect(0, 0, getSize().width, getSize().height); } } Soovides jätta all olevat pilti joonistatava kujundi alt läbi paistma, tuleb enne peale joonistamist määrata Composite, mis paluks peale joonistataval alumine vaid osaliselt ära katta ning jätta tulemuseks vanaga segatud värvid. import java.awt.*; import java.applet.*; public class Labipaistmine extends Applet{ public void paint(Graphics g){ Graphics2D g2=(Graphics2D)g; g2.setColor(Color.red); g2.fillRect(20, 50, 100, 100); g2.setComposite(AlphaComposite.getInstance( AlphaComposite.SRC_OVER, 0.7f)); //70% joonistab, 30% paistab alt läbi g2.setColor(Color.blue); g2.fillRect(60, 60, 100, 70); } public static void main(String argumendid[]){ Frame f=new Frame("Läbipaistmine"); f.add(new Labipaistmine()); f.setSize(250, 250); f.setVisible(true); } } Kujundi äärejooned Kujundi (näiteks ellipsi) äärejooned annab BasicStroke meetod createStrokedShape. Nii näiteks on võimalik kujundist vaid äärejooned välja joonistada või siis teise värviga esile tuua. public void paint(Graphics alggr){ Graphics2D g=(Graphics2D)alggr; BasicStroke b1=new BasicStroke(15); Shape kujund=b1.createStrokedShape( new Ellipse2D.Float(100, 100, 50, 70) ); g.fill(kujund); g.setColor(Color.green); g.draw(kujund); } Äärejoonte äärejooned Kuna nii ellips on kujund ning ka jämedajoonelise ellipsi äärejooned on samuti kujundid, siis juhul, kui annan äärejoontele jämeduse, võin ka nendelt omakorda äärejooned küsida. Nii saan tulemuseks juba neli joont: kaks üksteisele lähedal asuvat ovaali sees ning teised kaks ovaali välisringis. public void paint(Graphics alggr){ Graphics2D g=(Graphics2D)alggr; BasicStroke b1=new BasicStroke(15); BasicStroke b2=new BasicStroke(3); Shape kujund=b1.createStrokedShape( new Ellipse2D.Float(100, 100, 50, 70) ); Shape kujund2=b2.createStrokedShape(kujund); g.draw(kujund2); } Kujundi koostamine Kui sooviksin nende joontega midagi eraldi teha, näiteks neid igaüht isevärvi värvida, siis on mul võimalik kujund joonteks jagada klassi PathIterator abil. Selle abil saan kätte iga joone andmed. Sirgjoonel piisab kahest punktist. Kolme punkti abil määratakse kõverjoon, kus kaks punkti on otspunktideks ning kolmas näitab, milline peab kaar tulema. Nelja punktiga määratud joone puhul on samuti kaks otspunktideks, ülejäänud kahe punkti abil aga määratakse joone suunda otspunktist väljumisel. Uusi kujundeid aitab kombineerida klass GeneralPath. Talle tuleb lihtsalt öelda millise koha peale joon või ring või muu olemasolev kujund paigutada. Kriipsujuku saab tema abil küllalt kergesti valmis ning siis võib seda kasutada nagu iga muud kujundit. public void paint(Graphics alggr){ Graphics2D g=(Graphics2D)alggr; GeneralPath gp=new GeneralPath(); gp.append(new Ellipse2D.Float(20, 50, 40, 40), false); gp.append(new Line2D.Float(40, 100, 40, 150), false); g.draw(gp); } Tehted kujunditega Kujundit luues on võimalik olemasolevatega ka keerukamaid tehteid teha. Näiteks kui soovida koostada kuuveerandikku, siis võib kõigepealt teha ühe väiksema ringi ning siis sellest lahutada maha suurem ring, nii et vaid väike osa algsest jääb alles. All olevas näites on võetud kas osaliselt kattuvat ristkülikut ning näidatud, mis tehteid nendega läbi viia saab. Liites esimesele juurde teise saame kujundite ühendi, kus mõlema kujundi pinnad on liidetud. Ühisosa tekitab käsk intersect – alles jääb vaid osa pinnast, mis kuulub mõlema kujundi koosseisu. Lahutamise (subtract) korral jääb esialgsest pinnast alles vaid osa, mis teise alla ei kuulu. Välistuse (exclusiveOr) puhul jääb mõlemast pinnast alles vaid osa, kus üks pind teisega ei kattu. import java.awt.*; import java.awt.geom.*; import java.awt.image.*; import java.awt.event.*; import java.applet.*; public class Pind extends Applet implements ItemListener{ Area r1=new Area(new Rectangle2D.Float(20, 50, 100, 100)); Area r2=new Area(new Rectangle2D.Float(60, 60, 100, 70)); Choice valik=new Choice(); public Pind(){ valik.add("Ühend"); valik.add("Ühisosa"); valik.add("Vahe"); valik.add("Välistus"); add(valik); valik.addItemListener(this); } public void paint(Graphics g){ Area pind=new Area(); pind.add(r1); String s=valik.getSelectedItem(); if(s.equals("Ühend"))pind.add(r2); if(s.equals("Ühisosa"))pind.intersect(r2); if(s.equals("Vahe"))pind.subtract(r2); if(s.equals("Välistus"))pind.exclusiveOr(r2); Graphics2D g2=(Graphics2D)g; g2.setColor(Color.blue); g2.fill(pind); g2.setColor(Color.red); g2.draw(pind); } public void itemStateChanged(ItemEvent e){ repaint(); } public static void main(String argumendid[]){ Frame f=new Frame("Muster taustaks"); f.add(new Pind()); f.setSize(250, 250); f.setVisible(true); } } Ülesandeid · Joonista ring joone laiusega 25 punkti. · Määra joonistusvärviks üleminek punaselt kollasele · Määra taustamustriks väikesed kriipsujukud. · Muuda joon punktiiriks. · Määra joonistusala nii, et sinna jääb vaid osa ringjoont · Jäta taust ringi alt veidi läbi paistma. Swing Operatsioonisüsteemist sõltumatud komponendid, kujundus Lisaks kümnekonnale paketis java.awt asuvale komponendile saab kasutajaga suhtlemiseks tarvitada ka paketi javax.swing graafilisi komponente. Nagu kirjeldatud, palutakse awt-komponendid joonistada operatsioonisüsteemil, swing-komponente joonistatakse Java vahenditega. Sellest tulenevalt näevad esimesed välja nii nagu vastavas operatsioonsiüsteemis tavaks, swingi nupp või silt aga on igal pool tavajuhul peaaegu ühesugune. UIManageri abil aga on võimalik panna ka swing-komponente vastavalt operatsioonisüsteemile välja nägema. Swingi graafikakomponendid algavad tähega J. Tõenäoliselt seetõttu, et neid oleks kerge eristada analoogilistest awt komponentidest. Järgnevas näiteks on raamiks JFrame, selle sees on silt JLabel ning nupp JButton. Nii nupule kui sildile (ja ka mitmetele muudele komponentidele) saab tema ilmestamiseks määrata ikooni. ImageIcon loob ikooni kasutades aluseks pildifaili, kuid vajadusel saab ikooni ka käskude abil joonistada. Kõikidele swingi komponentidele saab määrata ToolTipText'i. Seda näidatakse ekraanile juhul, kui kasutaja on hiirega vastava komponendi peale liikunud. Enamasti vastav tekst seletab komponendi otstarvet või annab kasutajale tegutsemissoovitusi. Korraldus setMnemonic lubab klahvikombinatsiooni (Alt + täht) võrdsustada hiirega nupule vajutamisele. Komponentide raami lisamisel tuleb swingi puhul määrata, millisesse kihti ta paigutada. Harilikult kasutatava alumise kihi saab kätte getContentPane() abil. Pealmist kihti nimetatakse GlassPane ning vahepealsetesse kihtidesse paigutamiseks saab kasutada LayeredPane vahendeid. Kihtidega mängides saab komponente mitmesse kihti paigutada. import java.awt.*; import javax.swing.*; public class Pildid{ public static void main(String argumendid[]){ JLabel silt=new JLabel("Maja silt"); Font suurkiri=new Font("Serif", Font.BOLD+Font.ITALIC, 30); Icon majapilt=new ImageIcon("maja.gif"); silt.setFont(suurkiri); silt.setIcon(majapilt); JButton nupp=new JButton("Maja nupp", majapilt); nupp.setToolTipText("Head vajutamist!"); nupp.setMnemonic(java.awt.event.KeyEvent.VK_M); JFrame f=new JFrame("Sildiraam"); Container p=f.getContentPane(); p.setLayout(new GridLayout(2, 1)); p.add(silt); p.add(nupp); f.pack(); f.setVisible(true); } } HTML-kujundus Swingi komponentidel näidatavat teksti saab HTMLi abil kujundada. Täpne väljanägemine võib sõltuda intepretaatorist, kuid selliselt on programmi väljanägemist lihtsam pilkupüüdvaks muuta kui awt vahenditega kujundades. Nagu lihtsat teksti kandva sildi puhul, nii ka siin on võimalik programmi töö käigus sildi sisu muuta. import javax.swing.*; public class HtmlLabel{ public static void main(String argumendid[]){ JFrame f=new JFrame("Kujundatud silt"); JLabel silt=new JLabel( "

Pealkirja

\n"+ "ja punase tekstiga silt" ); f.getContentPane().add(silt); f.pack(); f.setVisible(true); } } Sisemised raamid Mõnes programmis on näha, et pearaami sees on omaette väiksemad raamid. Nii näiteks Wordi puhul võib iga tekst olla lahti omaette raamis, need aga omakorda suure Wordi raami sees. Sellist olukorda Java keskkonnas saab tekitada JInternalFrame abil. Nemad käituvad JDesktopPane sees samuti nagu harilikud raamid suure ekraani sees. Tema sees paiknevasse paneeli saab lisada komponente nagu ikka. Ka sisemisi raame saab muuta ikooniks alla serva, suurendada ja vähendada. Kui suure raami teateid püüab WindowListener, siis siseraamiga toimuva teada saamiseks aitab InternalFrameListener. Vorm on küll erinev, kuid võimalused samad. Näiteks koostab JDesktopPane tüüpi komponendi eraldi staatiline meetod. Rakendis paigutatakse komponent ekraanile init meetodi sees, käsurealt käivitades luuakse raam ning siis paigutatakse komponent raami sisse. import javax.swing.*; public class SiseraamigaRaam extends JApplet{ static JDesktopPane looRaamiPaneel(){ JInternalFrame siseraam1=new JInternalFrame("Esimene"); JInternalFrame siseraam2=new JInternalFrame("Teine", true, true, true, true); siseraam2.getContentPane().add(new JTextArea()); JDesktopPane paneel=new JDesktopPane(); siseraam1.setSize(200, 100); siseraam1.setLocation(10, 80); siseraam1.setVisible(true); paneel.add(siseraam1); try{siseraam1.setSelected(true);}catch (Exception e){} siseraam2.setVisible(true); paneel.add(siseraam2); siseraam2.setSize(150, 150); siseraam2.setLocation(120, 50); return paneel; } public void init(){ getContentPane().add(looRaamiPaneel()); } public static void main(String argumendid[]) throws Exception{ JFrame f=new JFrame("Kest"); f.setContentPane(looRaamiPaneel()); f.setSize(300, 300); f.setVisible(true); } } Värvivalija Kasutajapoolseks värvi valimiseks saab vajadusel kirjutada ise dialoogiakna, kust hiirega omale sobiv värv leida võimalik on. Swingi all aga on programmeerimisvaeva vähendamiseks loodud komponent JColorChooser, mille abil kasutaja võib pakutavate hulgast sobiva värvi välja valida. Värvivalikuks pakub komponent kolme võimalust: esimesel juhul saab hiirega vajutada sobivat värvi ruudule. Teisel puhul saab valida värvi ning teiselt skaalalt värvile vastava tumeduse. Kolmandas valikuaknas lastakse kasutajal määrata punase, rohelise ning sinise vahekord loodavas värvis. Vaikimisi kujul näitab komponent otsitavat värvi mitmesuguste kujundite ning ingliskeelse teksti peal. Selle kujundi saab aga vajadusel programmi ilmele sobivama või hoopis tühja pisikese paneeli vastu välja vahetada (järgnevas näites vastav rida välja kommenteeritud). Värvivaliku registreerimiseks ning temale reageerimiseks saab kasutada kuularit. Värvivaliku klassi juurde on loodud meetodid ka dialoogiakna kaudu värvi valimiseks. import javax.swing.*; import javax.swing.event.*; import java.awt.BorderLayout; public class Varvivalik{ public static void main(String argumendid[]){ final JTextArea tekstiala=new JTextArea("Värvilised tervitused"); final JColorChooser valija=new JColorChooser(); // valija.setPreviewPanel(new JPanel()); valija.getSelectionModel().addChangeListener( new ChangeListener(){ public void stateChanged(ChangeEvent e){ tekstiala.setForeground(valija.getColor()); } } ); JFrame f=new JFrame("Värvivalik"); java.awt.Container p=f.getContentPane(); p.add(tekstiala, BorderLayout.CENTER); p.add(valija, BorderLayout.SOUTH); f.setSize(300, 500); f.setVisible(true); } } Failinime valija Ka failinime valimise dialoogi saab ise suurema vaevata kirjutada, kuid swingi klass JFileChooser on juba vastavaks otstarbeks loodud ilusasti kujundatud komponent. Saab määrata, millisest kataloogist alates faili otsima saab hakata. Meetod showOpenDialog avab dialoogi ning programm jääb kasutajapoolset valikut ootama. Kui fail on valitud või valimine tühistatud, läheb programm edasi ning komponendi meetodite kaudu saab teada kasutaja vastuse. Kui soovitud fail on käes, saab temaga samuti toimida nagu failiga ikka, s.t. tema kohta infot küsida, sealt lugeda või sinna kirjutada. import javax.swing.*; import java.io.*; public class Failivalik{ public static void main(String argumendid[]){ JFileChooser valija=new JFileChooser(new File(".")); valija.showOpenDialog(new JFrame()); System.out.println("Valiti "+valija.getSelectedFile()); } } D:\Kasutajad\jaagup\java\naited\gr\swing>java Failivalik Valiti D:\Kasutajad\jaagup\java\Allikas.java Failivalikut saab veidi programmiga kohandada. Näiteks võib määrata avamisnupu peal olevat kirja. Samuti võib lubada korraga mitut faili valida. Järgnevas näites lisatakse filtri abil võimalus eraldi vaid pildifailide hulgast kasutajal sobivat otsida. Filtri loomisel tuleb üle katta meetodid accept ning getDescription. Esimesele antakse järjekorras ette kõik failid, mida parasjagu oleks võimalik valida. See meetod peab igaühe kohta neist ütlema, kas vastavat faili kasutajale näidata või mitte. Meetod getDescription väljastab filtrile sobivate failide ühisnimetaja, siin näites "Pildifailid". import javax.swing.*; import javax.swing.filechooser.FileFilter; import java.io.*; public class Failivalik2{ public static void main(String argumendid[]){ JFileChooser valija=new JFileChooser(new File(".")); valija.addChoosableFileFilter(new Pildifilter()); valija.showDialog(new JFrame(), "Vali fail"); System.out.println("Valiti "+valija.getSelectedFile()); } } class Pildifilter extends FileFilter{ public boolean accept(File f){ String failinimi=f.getName(); if(failinimi.endsWith(".gif")|failinimi.endsWith(".jpg")) return true; else return false; } public String getDescription(){ return "Pildifailid "; } } Puud Infohulgas orienteerumiseks saab andmeid esitada puuna. Siis on kasutajal võimalik ekraanilt kasutu kõrvaldada ning hierarhia abil enesele sobiv üles leida. Puu abil on näiteks esitatud failid ja kataloogid WindowsExploreris. Puusse saab lisada kõiki objekte. Vaikimisi juhul määrab JTree ise okstele ja lehtedele sobivad ikoonid ning objekti kirjelduseks kasutab sõnet mille väljastab selle toString meetod. Vajadusel aga võib nii ikooni kui kirjeldust muuta. Puu hierarhia saab kokku panna DefaultMutableTreeNode abil. Kui element on reas viimane, on ta leht, keskel oks ning algul juur. Puu ekraanile kuvamiseks tuleb luua JTree, kellele määrata juur, millest alates elemente näidata tuleb. Siin näites on pandud juureks ülikool, tema alla paar teaduskonda ning teaduskonna alla mõni õppetool. Võib jääda mulje, nagu tuleks puu loomiseks hirmus palju kirjutada, kuid suuremate andmehulkade korral saab luua või kasutada olemasolevaid alamprogramme ning puu tegemine polegi kuigi keeruline. import javax.swing.*; import javax.swing.tree.*; public class Puu{ public static void main(String argumendid[]){ DefaultMutableTreeNode juur=new DefaultMutableTreeNode("Ülikool"); DefaultMutableTreeNode teaduskond=new DefaultMutableTreeNode( "Matemaatika-Loodusteaduskond"); teaduskond.add(new DefaultMutableTreeNode("Informaatika õppetool")); teaduskond.add(new DefaultMutableTreeNode("Bioloogia õppetool")); juur.add(teaduskond); juur.add(new DefaultMutableTreeNode("Kultuuriteaduskond")); JTree puu=new JTree(juur); JFrame f=new JFrame("Puu"); f.getContentPane().add(puu); f.setSize(200, 200); f.setVisible(true); } } Kasutaja tegevuse registreerimiseks saab tarvitada mitmesuguseid kuulareid. Saab teada, millal ta puu nähtavat osa suurendas või vähendas, mis osa puust nähtav on, millal mõni element valiti. Ka pärast puu loomist saab temasse elemente lisada ja sealt eemaldada. Käsklus puu.putClientProperty("JTree.lineStyle", "Angled"); palub puu osad omavahel joontega ühendada. Siin näites trükitakse igal valikul välja valitud komponent. TreeSelectionEvent'i meetod getPath() annab tulemuseks kogu rea alates juurest kuni märgitud leheni. Selle kaudu oleks võimalik ükskõik millise tee peale jääva komponendi poole pöörduda. Meetod getLastPathComponent() annab tulemuseks tee viimase elemendi, ehk selle, millele kasutaja vajutas. import javax.swing.*; import javax.swing.tree.*; import javax.swing.event.*; import java.awt.BorderLayout; public class Puu2a extends JApplet{ static JTextField tekstikast=new JTextField(); static JPanel looPuu(){ DefaultMutableTreeNode juur=new DefaultMutableTreeNode("Ülikool"); DefaultMutableTreeNode teaduskond=new DefaultMutableTreeNode( "Matemaatika-Loodusteaduskond"); teaduskond.add(new DefaultMutableTreeNode("Informaatika õppetool")); teaduskond.add(new DefaultMutableTreeNode("Bioloogia õppetool")); juur.add(teaduskond); juur.add(new DefaultMutableTreeNode("Kultuuriteaduskond")); JTree puu=new JTree(juur); puu.addTreeSelectionListener( new PuuKuular() ); puu.putClientProperty("JTree.lineStyle", "Angled"); JPanel paneel=new JPanel(new BorderLayout()); paneel.add(puu); paneel.add(tekstikast, java.awt.BorderLayout.SOUTH); return paneel; } public void init(){ getContentPane().add(looPuu()); } public static void main(String argumendid[]){ JFrame f=new JFrame("Puu"); f.getContentPane().add(looPuu()); f.setSize(200, 200); f.setVisible(true); } } class PuuKuular implements TreeSelectionListener{ public void valueChanged(TreeSelectionEvent e){ Puu2a.tekstikast.setText(e.getPath().getLastPathComponent()+""); } } Paigutus Jaotuspaneel JSplitPane võimaldab temale eraldatud pinna jaotada kahe komponendi vahel. Tuleb vaid määrata, kas pind jaotatakse kaheks horisontaalselt või vertikaalselt ning komponendid, mis kummasegi ossa panna. Lisameetoditega saab määrata ja muuta jagamise kohta, samuti kasutajapoolset vahepiiri nihutamise võimalusi. Jagatud paneelidena näevad välja mitut lehte sisaldavad brauseri aknad, kus vasakul näiteks sisukord ja paremal sisu. Kui koodi vaadata, siis üles on pandud JButton, alla JBoggleButton. Viimase omapäraks on, et esimest korda vajutades jääb nupp sisse (tumedaks), alles teisel korral tuleb välja tagasi. import javax.swing.*; public class Jaotuspaneel{ public static void main(String argumendid[]){ JSplitPane paneel=new JSplitPane( JSplitPane.VERTICAL_SPLIT, new JButton("Ülemine"), new JToggleButton("Alumine") ); JFrame f=new JFrame("Jagatud raam"); f.getContentPane().add(paneel); f.setSize(200, 200); f.setVisible(true); } } Valikupaneel Kui määranguaknas on võimalusi rohkem kui kasutajale korraga mõistlik näidata on, siis JTabbedPane abil saab muud paneelid ülekuti paigutada. Sarnaselt on loodud näiteks Exceli lahtrimäärangute dialoogiaken, kus ühel paneelil saab määrata andmete tüüpi, teisel kujundust jne. Soovi korral saab paneelivaliku nuppudele lisada ikoonid, eemaldada, vahetada ja lisada paneele, mõne valiku tegemist keelata, automaatselt soovitud paneel esile tuua ning mitmel moel kasutaja tegevuse kohta teateid saada. import javax.swing.*; public class Valikupaneel{ public static void main(String argumendid[]){ JPanel p1=new JPanel(new java.awt.GridLayout(2,1)); p1.add(new JButton("Ülemine")); p1.add(new JButton("Alumine")); JTabbedPane paneel=new JTabbedPane(); paneel.add("Esimene", p1); paneel.add("Teine", new JLabel("Suur silt")); JFrame f=new JFrame("Valikupaneeli näide"); f.getContentPane().add(paneel); f.setSize(200, 200); f.setVisible(true); } } Tööriistariba Ka sellenimeline abivahend on Swingi all täiesti olemas. Kui on soovi nuppe ja muid tööriistu oma silma järgi ümber paigutada, siis tööriistariba peaks selleks parim valik olema. Kokku saab selle panna nagu tavalise paneeli, kuid edaspidi on loodud paneeli võimalik pea vabalt paigutada. Riba saab liigutada nii omaette raamaknana kui paigutada vabalt iga BorderLayout'i vaba serva peale. Piisab vaid riba lohistamisest õige koha lähedusse, kui see juba haakub. Paigalt eemale saab ka täiesti rahumeeli lohistada. Et JAppleti vaikimisi paigutushalduriks ongi BorderLayout, siis neli külge on tööriistaribade jaoks kohe kasutatavad. Riba ennast annab koostada ning valmis komponente sinna peale panna paari käsuga. Pea pool näiteprogrammist on kulunud ovaali kujutava pildiga ikoone loova klassi valmistamiseks, mille abil on hõlbus äratuntava pildiga nuppe toota. Ikooni loomiseks tuleb teha liidest Icon realiseeriv klass. Siin on see paigutatud Tooriistariba sisemiseks klassiks, et oleks viimasele alati kättesaadav ning kopeerides kaduma ei läheks. Iseenesest aga võib loodav ikoone tootev klass olla rahumeeli eraldi klassina, sel juhul on võimalik ovaalseid ikoone ka teiste programmide kasuks luua. Meetodis paintIcon tuleb kirja panna, milline ikoon välja näeb, getIconWidth() ning getIconHeight() annavad ikooni soovitud suuruse. import java.awt.*; import javax.swing.*; import java.awt.event.*; public class Tooriistariba extends JApplet { public Tooriistariba() { JToolBar riba = new JToolBar(); JButton nupp = new JButton("Nupp"); riba.add(nupp); riba.addSeparator(); riba.add (new Checkbox ("Märkeruut")); getContentPane().add (riba, BorderLayout.NORTH); riba = new JToolBar(); Icon icon = new OvaalneIkoon(Color.red); nupp = new JButton(icon); riba.add(nupp); icon = new OvaalneIkoon(Color.blue); nupp = new JButton(icon); riba.add(nupp); icon = new OvaalneIkoon(Color.green); nupp = new JButton(icon); riba.add(nupp); riba.addSeparator(); icon = new OvaalneIkoon(Color.magenta); nupp = new JButton(icon); riba.add(nupp); getContentPane().add (riba, BorderLayout.SOUTH); } class OvaalneIkoon implements Icon { Color varv; public OvaalneIkoon (Color c) { varv = c; } public void paintIcon (Component c, Graphics g, int x, int y) { g.setColor(varv); g.fillOval ( x, y, getIconWidth(), getIconHeight()); } public int getIconWidth() { return 20; } public int getIconHeight() { return 10; } } public static void main(String[] argumendid){ JFrame f=new JFrame("Tööriistaribad"); f.getContentPane().add(new Tooriistariba()); f.setSize(200, 200); f.setVisible(true); } } Ennistamine Kui üheksakümnendate aastate algul Word 2-te käsu tagasi võtmise võimalus sisse pandi, siis tundus olevat tegemist uue tähelepanuväärse ja omapärase lahendusega. Nüüd on kasutajad programmide juures pea alatise tagasivõtmise võimalusega nii harjunud, et panevad imeks, kui mõne käsu tagajärgi pole võimalik olematuks teha ning peab enne näpuliigutust hoolikalt läbi mõtlema, mis tehtu tulemuseks võib olla. Isegi terveid kataloogitäisi andmed saab Windows Exploreris paigast nihutada ning tagasi panna ilma, et selle peale suuremaid raskusi tekiks. Samuti kannatab mitmeski kohas mitte ainult üht või paari käsku tagasi võtta, vaid annab pea sammhaaval kogu töö algusesse minna. Sellised võimalused ei teki töösse iseenesest, selle loomiseks tuleb programmi kirjutamisel kõvasti hoolt kanda. Samuti peab arvestama, et igat asja pole siiski võimalik tagasi võtta. Kui kord on soovimatu sisuga kirjad teistele inimestele laiali saadetud, siis on nad teel ja kohal ning meie loodud programmil pole nende kaotamiseks võimalik enam midagi ette võtta. Parimal juhul annab tagasivõtmise käsu juures sihtkohta uus kiri saata, et ärgu vastuvõtja eelmise saabunud kirja sisu liialt südamesse võtku. Tagasivõtmist annab koodi sisse mitut moodi ehitada, kuid Swingi vahendite juures on eraldi selle tarbeks loodud UndoManager, mis peaks aitama suuremate tööde puhul toimingule süsteemsemalt läheneda. Et tööd saaks pärast ilusti tagasi võtta, tuleb iga tehtud samm lisada ennistushaldurisse ning iga sammu juures peab olema kirjas, kuidas samm teha ning kuidas tagasi võtta. Niimoodi tekib sammude ahel, mida mööda on pärastpoole mugav edasi ja tagasi käia. Allpool olevas näites on tagasivõetava programmi koostamine läbi mängitud lihtsa pildiredaktori peal, millele saab joonistada vaid ringe. Lisatud on nupud käskude tagasi võtmiseks ning sama teed pidi edasi liikumiseks. Iga hiirevajutusega lisatakse näidatavate ringide nimistusse (Vector) (muutuva pikkusega massiiv) punkti andmed, mis tähistavad hiirevajutuse asukohta. Nii on mälus kirjas, kuhu joonistusvajaduse korral ringid tekitada ning vähemasti iga operatsiooni järel palutakse ekraanipilt uuendada. Ennistuse huvides ei lisata punkti andmeid nimistusse otseselt, vaid tehakse veidi pikem ring. Ringi lisamise kirjeldamiseks on loodud eraldi klass LisatavRing, mis laiendab klassi AbstractUndoableEdit. Klassis on käsud undo ja redo, kuhu tuleb kirja panna tegevused, mis tuleb sooritada vastavalt tagasi või edasi liikumiseks. Klassis on isendimuutujaks Point (paketist java.awt) parasjagu lisatava punkti andmete hoidmiseks. Töö lihtsustamiseks on kohe klassi LisatavRing isendi loomisel palutud tal kohe teha läbi edasi liikumisega seotud töö ehk lisada punkti andmed. Sel juhul piisab peaprogrammis sammu tegemisel vaid soovitud koordinaatidega LisatavaRingi loomisest. Edaspidise tagasivõtu sujumiseks tuleb vastav isend UndoableEditEvent'i koosseisus ennistushaldurisse lisada. Nuppude abil tööjärjes edasi-tagasi liikumiseks tuleb vaid ennistushaldurile anda käske undo või redo vastavalt soovitud suunale. Samuti on viisakas enne kontrollida, kas vastavas suunas üldse võimalik liikuda on. import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.undo.*; import javax.swing.event.*; import java.util.Vector; public class Ennistus extends JApplet implements ActionListener{ static Vector ringid=new Vector(); UndoManager ennistushaldur=new UndoManager(); Button edasi=new Button(">"); Button tagasi=new Button("<"); public Ennistus(){ getContentPane().add(tagasi, BorderLayout.WEST); getContentPane().add(edasi, BorderLayout.EAST); tagasi.addActionListener(this); edasi.addActionListener(this); addMouseListener( new MouseAdapter(){ public void mousePressed(MouseEvent e){ ennistushaldur.undoableEditHappened( new UndoableEditEvent( Ennistus.this, new LisatavRing(e.getX(), e.getY()) ) ); repaint(); } } ); } public void paint(Graphics g){ g.setColor(Color.white); g.fillRect(0, 0, getWidth(), getHeight()); g.setColor(Color.black); for(int i=0; i tg1(nihe) -> keere1 -> kuup. Selliselt järgnevate muunete omadused liituvad. Transform3D t3d1=new Transform3D(); t3d1.setTranslation(new Vector3f(0.5f, 0, 0)); TransformGroup tg1=new TransformGroup(t3d1); Transform3D keerd1=new Transform3D(); keerd1.rotX(Math.PI/4); TransformGroup keere1=new TransformGroup(keerd1); juur.addChild(tg1); tg1.addChild(keere1); keere1.addChild(new ColorCube(0.5)); juur.compile(); Sama tulemuse võib ka lühemalt saavutada, arvutades nii keeramise kui nihke summa välja ühe Transform3D objekti sees ning tulemus määrata ühe TransformGroup'i omaduseks. Transform3D t3d1=new Transform3D(); t3d1.setTranslation(new Vector3f(0.5f, 0, 0)); Transform3D keerd1=new Transform3D(); keerd1.rotX(Math.PI/4); t3d1.mul(keerd1); TransformGroup tg1=new TransformGroup(t3d1); juur.addChild(tg1); tg1.addChild(new ColorCube(0.5)); juur.compile(); Käsk mul (multiple) korrutab kahe maatriksi väärtused (ühendab nendes paiknevad omadused) ning paneb tulemuse esimesse algse väärtuse asemel. Seda Transform3D'd saab nüüd kasutada kui algsete omaduste summat. Liigutamine Järgneva näite tulemusena pannakse kuup ekraanil keerlema ning lähemale-kaugemale nihkuma. import java.awt.*; import javax.swing.*; import com.sun.j3d.utils.geometry.ColorCube; import com.sun.j3d.utils.universe.SimpleUniverse; import javax.media.j3d.*; import javax.vecmath.*; public class Kuup2a extends JApplet implements Runnable{ TransformGroup keere1; double nurk; public Kuup2a() { Canvas3D c = new Canvas3D(SimpleUniverse.getPreferredConfiguration()); getContentPane().add(c, BorderLayout.CENTER); BranchGroup juur = new BranchGroup(); Transform3D keerd1=new Transform3D(); keere1=new TransformGroup(keerd1); keere1.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); juur.addChild(keere1); keere1.addChild(new ColorCube(0.5)); juur.compile(); SimpleUniverse u = new SimpleUniverse(c); u.getViewingPlatform().setNominalViewingTransform(); u.addBranchGraph(juur); new Thread(this).start(); } public void run(){ while(true){ try{Thread.sleep(50);}catch(Exception e){} nurk+=0.1; double kaugus=-1+Math.sin(nurk/3); Transform3D t=new Transform3D(); t.rotX(nurk); t.setTranslation(new Vector3d(0, 0, kaugus)); keere1.setTransform(t); } } public static void main(String[] args) { Frame f=new Frame("Keerlev kuup"); f.add(new Kuup2a()); f.setSize(300, 300); f.setVisible(true); } } Võrreldes eelmise näitega on objektipuu loomisel juures üks käsk keere1.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); mille tulemusena lubatakse muundusgrupi väärtusi ka programmi töö käigus muuta. Muutmine ise on toodud eraldi lõime, meetodisse run, kus igal sammul suurendatakse nurka 0.1 radiaani võrra ning vastavalt sellele määratakse kuubi keere ning nihe. Kui soovida, et kuup kaugust vaatajast ei muuda, siis võib kaugusega seotud käsud välja jätta. Samuti nagu siin run-käsus tsüklina kuupi keerates võib ka mujal mitmesugustele andmetele vastavalt. Nii võib rahumeeli paluda kujundeid paigutada vastavalt kasutaja sisestatud või võrgust saabunud andmetele. Interpolaatorid Liikumisi tuleb kolmemõõtmelises graafikas küllalt palju ette. Nende sujuvamaks loomiseks on kasutaja vabastatud pidevast lõimede kirjutamisest ning ta võib koostada interpolaatoreid, mis hoolitsevad muundusgrupi kasutajale sobiva oleku eest. Interpolaatori "südameks" on Alpha, mis vastavalt kasutaja poolt etteantud parameetritele muudab enese sees arvu väärtusega nulli ja ühe vahel. Interpolaator vaatab iga natukese aja tagant seda väärtust ning vastavalt sellele seab muundusgrupi parameetrid. RotationInterpolatori puhul vastab Alpha väärtusele 0-null ning 1-täisring, muud suurused vahepeal. Esimene parameeter tähendab korduste arvu (-1 vastab igavesele pöörlemisele), teine aega millisekundites, mille jooksul suureneb väärtus nullist üheni. Siinses näites siis pööreldakse igavesti, kulutades täisringile kümme sekundit. Kuna algseisu ning täisringi puhul on kuup samas asendis, siis paistabki liikumine ühtlane, kuigi vahepeal toimub muundegrupi väärtuses täisringine nihe, kui Alpha hüppab väärtuselt 1 järsku 0 peale tagasi. Interpolaator on siin näites pandud objektihierarhiasse ühes kuubiga lehena muundegrupi järglaseks. Viimane omakorda kuulub juure alla. BranchGroup juur = new BranchGroup(); TransformGroup keere1=new TransformGroup(); keere1.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); RotationInterpolator keeraja1= new RotationInterpolator(new Alpha(-1, 10000), keere1); keeraja1.setSchedulingBounds(new BoundingSphere()); juur.addChild(keere1); keere1.addChild(keeraja1); keere1.addChild(new ColorCube(0.5)); juur.compile(); Interpolaatoreid on mitmesuguseid. PositionInterpolator suudab kujundeid (või nende komplekti) liigutada kahe punkti vahel. Millisel ajahetkel ese kus paigas asub, seda määrab jällegi isend tüübist Alpha. Temagi muutumisi saab täpsemalt määrata. Parameetriteta konstruktori puhul kasvab väärtus sekundi jooksul nullist üheni ning siis asub jälle otsast peale. Põhjalikuma määramise puhul – nagu järgnevast näitest näha – on Alpha parameetrite järjekord selline: 1. korduste arv (-1=lõpmatus) 2. lubatud suunad(kasvamine/kahanemine) 3. ooteaeg enne käivitumist (kõik ajad millisekundites) 4. ooteaeg iga ringi algul, kasvamiskiiruse suurenemise aeg 5. üheni jõudmise aeg 6. väärtusel 1 püsimise aeg 7. vähenemiskiiruse suurenemise aeg 8. nullini jõudmise aeg 9. nullil püsimise aeg. PositionInterpolatori konstroktoris määratakse ära Alpha, muundusgrupp muutmiseks, liikumissuund ning pikkused palju algasendist edasi ning tagasi liikuda. Liikumissuuna võib Transform3D abil keerata täpselt selliseks nagu vaja. BranchGroup juur = new BranchGroup(); TransformGroup tg1=new TransformGroup(); tg1.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); Transform3D suund=new Transform3D(); suund.rotZ(Math.PI/2); //keerab ümber z-telje Alpha a=new Alpha(-1, Alpha.INCREASING_ENABLE | Alpha.DECREASING_ENABLE , 0, 2000, 2000, 2000, 0, 2000, 2000, 0); PositionInterpolator liigutaja1= new PositionInterpolator(a, tg1, suund, -0.6f, 0.5f); liigutaja1.setSchedulingBounds(new BoundingSphere()); juur.addChild(tg1); tg1.addChild(liigutaja1); tg1.addChild(new ColorCube(0.2)); juur.compile(); Sarnaselt töötab ScaleInterpolator, mille abil kujundi suurust muuta võib. BranchGroup juur = new BranchGroup(); TransformGroup tg1=new TransformGroup(); tg1.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); ScaleInterpolator ip1= new ScaleInterpolator(new Alpha(), tg1); ip1.setSchedulingBounds(new BoundingSphere()); juur.addChild(tg1); tg1.addChild(ip1); tg1.addChild(new ColorCube(0.2)); juur.compile(); Liikumistrajektoori võib punktide kaupa ette anda. Selleks tuleb kirjeldada punktide asukohad ning ajad, millal mingis punktis peab olema. Aegade väärtused tuleb määrata järjest, nullist üheni (ka Alpha annab sama vahemiku) ning aegu peab olema sama palju kui punkte. Siin näites algab liikumine koordinaatide alguspunktist. Kui Alpha on jõudnud 0,7-ni selleks ajaks asub kuup punktis (1, 0, 0) ehk on liikunud ühe ühiku võrra paremale. Järgneva 0,3 Alpha-ühiku jooksul nihutatakse kuup diagonaalis vasakule üles, nii, et ajal kui Alpha=1, on kuup punktis (0, 1, 0). Siis jälle tuldud teed tagasi. Liikumistee võib ette arvutada hulga pikema, nii et saab koostatud kujundi päris keerukat rada pidi käima panna. Kui keeramisrealt (suund.rotZ(Math.PI/2)) kommentaarimärgid eest ära võtta, siis liigub kuup kõigepealt üles ning sealt vasakule alla, sest siis on kogu liikumistee ümber Z-telje täisnurga jagu vasakule pööratud. TransformGroup tg1=new TransformGroup(); tg1.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); Transform3D suund=new Transform3D(); // suund.rotZ(Math.PI/2); //keerab liikumistrajektoori ümber z-telje Alpha a=new Alpha(-1, Alpha.INCREASING_ENABLE | Alpha.DECREASING_ENABLE , 0, 2000, 2000, 2000, 0, 2000, 2000, 0); float[] ajad={0, 0.7f, 1}; Point3f[] punktid={ new Point3f(0, 0, 0), new Point3f(1, 0, 0), new Point3f(0, 1, 0) }; PositionPathInterpolator liigutaja1= new PositionPathInterpolator(a, tg1, suund, ajad, punktid); liigutaja1.setSchedulingBounds(new BoundingSphere()); juur.addChild(tg1); tg1.addChild(liigutaja1); tg1.addChild(new ColorCube(0.2)); Kaks kuupi Esimesed näited olid koostatud ühe kuubiga vaid seepärast, et tulemus lihtsamini kätte tuleks ning kood lühem välja näeks. Täiesti samamoodi saab kujundeid ka juurde lisada. Kui need aga otse juure külge panna, siis satuvad nad otse üksteise peale/sisse ning mõni võib sootuks teise taha peitu jääda et teda sugugi näha ei ole. Kui aga vähemalt üks objekt eemale nihutada, siis on kummalgi oma koht ning neid võib rahumeeli ekraani peal vaadelda. BranchGroup juur = new BranchGroup(); Transform3D tr1=new Transform3D(); tr1.rotX(Math.PI*2/3); tr1.setTranslation(new Vector3d(0.5, 0, 0)); TransformGroup tg1=new TransformGroup(tr1); juur.addChild(tg1); tg1.addChild(new ColorCube(0.2)); juur.addChild(new ColorCube(0.1)); juur.compile(); Muud kujundid Lisaks värvilisele kuubile, millega algul hea katseid teha, leiab geomeetriapaketist veel silindri (Cylinder), koonuse (Cone), risttahuka (Box) ning kera (Sphere). Neile pole veel värvi antud, seetõttu tuleb see töö ise ära teha. Cylinder s1=new Cylinder(0.2f, 1.2f); //raadius, pikkus s1.setAppearance(new Appearance()); juur.addChild(s1); Nagu aimata võib, saab silindrile ette anda raadiuse ning kõrguse, koonusele samuti. Risttahukale kolme külje pikkused ning kerale raadiuse. Taust Soovides tagapinnaks oleva musta tausta millegi muu värvi vastu vahetada, tuleb lihtsalt kirjutada millist värvi selle asemele soovitakse. Ning nagu muud elemendid, tuleb seegi juure külge haakida. Background taust=new Background(new Color3f(Color.yellow)); //kollane taust taust.setApplicationBounds(new BoundingSphere(new Point3d(), 1000)); juur.addChild(taust); Vaatekoha muutmine Et ruumis ringi liikuda või lihtsalt pilti teise nurga alt vaadata, selleks võib oma vaateplatsi asukohta muuta. Järgmise paari reaga luuakse uus Transform3D, määratakse sellele parameetrid (0,6 ühikut üles, 3 ettepoole) ning palutakse vaataja silmad ehk kaamera selle koha peale paigutada. Kui ise liikuda ülespoole, siis paistab pilt selle jagu altpoolt. Transform3D t=new Transform3D(); t.setTranslation(new Vector3f(0, 0.6f, 3)); u.getViewingPlatform().getViewPlatformTransform().setTransform(t); Järgnevalt veidi pikem näide, kus lennutatake taevasse hulk kuupe juhuslikesse asukohtadesse tähtedeks ning seejärel võib oma asukohta muutes kui kosmoselaevaga mööda taevalaotust ringi liikuda. Iga kuubi tarvis luuakse oma muundegrupp ning selle abil nihutatakse kuup keskpunkti suhtes juhuslikult kuni +/- 50 ühiku võrra igas suunas. KeyNavigatorBehavior võimaldab vaateplatvormilt küsitud TransformGroupi muuta ning selle abil meile ruumis liikumis ette kujutada. import java.applet.Applet; import java.awt.*; import com.sun.j3d.utils.geometry.ColorCube; import com.sun.j3d.utils.universe.SimpleUniverse; import com.sun.j3d.utils.behaviors.keyboard.*; import javax.media.j3d.*; import javax.vecmath.*; public class Asukoht2 extends Applet { public Asukoht2() { setLayout(new BorderLayout()); Canvas3D c = new Canvas3D(SimpleUniverse.getPreferredConfiguration()); add(c, BorderLayout.CENTER); BranchGroup juur = new BranchGroup(); for(int i=1; i<1000; i++){ //luuakse hulk juhuslikke kuupe taevasse Transform3D t3=new Transform3D(); t3.setTranslation(new Vector3f((float)(100*Math.random()-50), (float)(100*Math.random()-50), (float)(100*Math.random()-50))); TransformGroup tg=new TransformGroup(t3); tg.addChild(new ColorCube(0.4)); juur.addChild(tg); } SimpleUniverse u = new SimpleUniverse(c); TransformGroup tg=u.getViewingPlatform().getViewPlatformTransform(); KeyNavigatorBehavior kb=new KeyNavigatorBehavior(tg); kb.setSchedulingBounds(new BoundingSphere(new Point3d(), 1000)); juur.addChild(kb); Transform3D t=new Transform3D(); //Määratakse vaatekoht t.setTranslation(new Vector3f(0, 0, -5)); u.getViewingPlatform().getViewPlatformTransform().setTransform(t); Background taust=new Background(new Color3f(Color.blue)); //sinine taust taust.setApplicationBounds(new BoundingSphere(new Point3d(), 1000)); juur.addChild(taust); juur.compile(); u.addBranchGraph(juur); } public static void main(String[] args) { Frame f=new Frame("Kuubid taevas"); f.add(new Asukoht2()); f.setSize(300, 300); f.setVisible(true); } } Kiri Lihtne kahemõõtmeline tekst õnnestub ekraanile manada küllalt kergesti. juur.addChild(new Text2D("Tervitus", new Color3f(Color.green), "Times", 55, Font.ITALIC)); Selle sama teksti pöörlema panek on aga natuke suurem ettevõtmine. Samas aga on tulemus küllaltki ilus ning mõnelgi juhul võib end ära tasuda. Endiselt tuleb lubada muundegrupi väärtust programmi töö ajal kirjutada, et saaksime teksti nurka muuta. Tagapoolsed ettevõtmised pinnaga hoolitsevad, et saaksime teksti mõlemat külge näha. Vaikimisi hoiab arvuti enese energiat kokku ning ei soostu kirja tagapoolt näitama. TransformGroup keere1=new TransformGroup(); keere1.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); RotationInterpolator keeraja1= new RotationInterpolator(new Alpha(-1, 10000), keere1); keeraja1.setSchedulingBounds(new BoundingSphere()); juur.addChild(keere1); keere1.addChild(keeraja1); Text2D tekst=new Text2D("Tervitus", new Color3f(Color.green), "Times", 55, Font.ITALIC); PolygonAttributes pind=new PolygonAttributes(); pind.setCullFace(PolygonAttributes.CULL_NONE); //et ka teine pool näha oleks tekst.getAppearance().setPolygonAttributes(pind); keere1.addChild(tekst); juur.compile(); Ka kolmemõõtmelise kirja puhul tuleb hoolitseda, et seda ekraanil näitama soostutaks. Text3D iseenesest kirjeldab vaid kuju ehk geomeetria. Nägemiseks peab looma kujundi (Shape3D) ning seadma talle geomeetria ning materjali. Et materjal ruumis paistaks, selleks lubame seda valgustada (setLightingEnable) ning paigutame ruumi tausta/hajuvalguse allika. Nii ilmub õrn kolmemõõtmeline tekst ekraanile. Text3D t=new Text3D( new Font3D(new Font("Helvetica", Font.PLAIN, 1), new FontExtrusion()), "Tere" , new Point3f(-1, 0.6f, -1.5f) ); Material m=new Material(); m.setLightingEnable(true); Appearance a=new Appearance(); a.setMaterial(m); Shape3D s=new Shape3D(); s.setGeometry(t); s.setAppearance(a); juur.addChild(s); Color3f valgusvarv=new Color3f(Color.white); AmbientLight al=new AmbientLight(valgusvarv); al.setInfluencingBounds(new BoundingSphere()); juur.addChild(al); juur.compile(); Tasapind Omale sobivatest tasapindadest saab kokku ehitada kõik meile sobivad kujundid. Ka kera pole siinse programmi jaoks muud kui piisavalt tihedasti üksteise kõrvale pandud kolmnurkade kogum. QuadArray abil saab määrata loodava pinna nurgad ning siis paluda nende vahele pind koostada. Edasine toiming sama kui kolmemõõtmelise teksti puhul: geomeetria ja materjal ühendada kujundiks ning selle nägemiseks panna ta lavagraafi(puusse) ja lasta valgus peale. QuadArray nurgad=new QuadArray(4, GeometryArray.COORDINATES | GeometryArray.COLOR_3 | GeometryArray.NORMALS); nurgad.setCoordinate(0, new Point3f(-0.5f, 0, 0)); nurgad.setCoordinate(1, new Point3f( 0.5f, 0, 0)); nurgad.setCoordinate(2, new Point3f( 0, 0.2f, 0)); nurgad.setCoordinate(3, new Point3f( 0, 0.4f, 0)); Shape3D kujund=new Shape3D(); kujund.setGeometry(nurgad); Appearance a=new Appearance(); a.setMaterial(new Material()); kujund.setAppearance(a); juur.addChild(kujund); AmbientLight taustavalgus=new AmbientLight(); taustavalgus.setInfluencingBounds(new BoundingSphere()); juur.addChild(taustavalgus); juur.compile(); Joonemassiiv Nii pindadest kui joontest võib oma tarbeks kujundi luua. Järgnevas näites luuakse kahest joonest L-täht mida võib edaspidi kasutada nii nagu tavalist kujundit, nagu näiteks kuupi või koonust. import javax.media.j3d.*; import javax.vecmath.*; public class SuurL extends Shape3D{ Point3d p1=new Point3d(0, 0, 0), p2=new Point3d(0, 1, 0), p3=new Point3d(0.5, 0, 0); Point3d[] jooned={ p1, p2, p1, p3 }; public SuurL(){ LineArray joonemassiiv=new LineArray(4, LineArray.COORDINATES); joonemassiiv.setCoordinates(0, jooned); setGeometry(joonemassiiv); setAppearance(new Appearance()); } } Selline klass tuleb salvestada eraldi faili ning edaspidi kui tarvist seda kasutada, tuleb hoolitseda, et see SuurL asuks samas kataloogis või sisseloetavas paketis ning loodud tähe ekraanile manamiseks piisab vaid käsust juur.addChild(new SuurL()); Valgus Vaid osa kujundeid suudame näha siis, kui neile valgus peale ei paista. Ka ülejäänuid on võimalik pealelangeva valgusega ilmestada. Java3D on loodud liikuvate kujundite tarbeks ning värvipeensuste arvutamiseks kuigi palju aega ei kulutata, sellegipoolest võib hea tahtmise korral täiesti äratuntava pildi ekraanile manada. Fotokvaliteediga ühe pildi algandmetest välja arvutamiseks kulub võimsatel arvutitel tunde ning siiski tuleb osa tegelikkuses aset leidvaid peegeldumisi ja muid seoseid tähelepanuta jätta. Java3D abil saab esitada haju-, suund-, punkt- ning kohtvalgust. Esimene on nagu päevavalgus, see on hajunud ühtlaselt üle ruumi ning selle abil on võimalik ka laua all ning kapi taga esemeid näha. Suundvalguse kiired on paralleelsed, samuti nagu need mis päikeselt meieni jõuavad. Punktvalgus levib ühest punktist alates igas suunas laiali. Kohtvalguse puhul saab lisaks määrata ruuminurga, kui laia koonusena see valgus laiali läheb. Valgustatavatel kehadel peavad olema välja arvutatud pinnanormaalid. Isegi pealtnäha kumerad pinnad on arvuti tarvis pisikeste kolmnurgakujuliste tasapindade ühendid, kus iga tasapinna jaoks vastavalt valgustatusele tema värv välja näidatakse. Nagu muud 3D objektid, tuleb ka valgus lisada elementide puusse ning määrata tema mõjupiirkond (InfluencingBounds). BranchGroup juur = new BranchGroup(); Appearance a=new Appearance(); a.setMaterial(new Material()); juur.addChild(new Sphere(0.5f, Sphere.GENERATE_NORMALS, a)); AmbientLight taustavalgus=new AmbientLight(); taustavalgus.setInfluencingBounds(new BoundingSphere()); juur.addChild(taustavalgus); Suundvalguse lisamine on peaaegu analoogiline BranchGroup juur = new BranchGroup(); Appearance a=new Appearance(); a.setMaterial(new Material()); juur.addChild(new Sphere(0.5f, Sphere.GENERATE_NORMALS, a)); DirectionalLight suundvalgus=new DirectionalLight(); suundvalgus.setInfluencingBounds(new BoundingSphere()); juur.addChild(suundvalgus); juur.compile(); Punktvalguse puhul esimene punkt näitab valgusallika asukohta, teine valguse nõrgenemist. Valgustatus mingis punktis arvutatakse valemi järgi 1/(k1+kaugus*k2+kaugus*kaugus*k3), kus k1-k3 on teise Point3f-i parameetriteks antavad väärtused. Nii, et esimene vähendab heledust igal poole, teine võrdeliselt kaugusega ning kolmas võrdeliselt kauguse ruuduga. PointLight punktvalgus=new PointLight( new Color3f(Color.green), new Point3f(-0.6f, -0.7f, 0), new Point3f(0, 0, 1) ); punktvalgus.setInfluencingBounds(new BoundingSphere()); juur.addChild(punktvalgus); Kohtvalguse puhul antakse lisaks punktvalgusele ka suund (vektori abil) ja nurk, kui laialt valgus paistab. Viimane parameeter näitab, kui palju on sõõri keskus enam valgustatud kui ääred. Kui see väärtus on 0, siis paistab valgus ühtlaselt igale poole. SpotLight kohtvalgus=new SpotLight( new Color3f(Color.green), new Point3f(-0.5f, -0.7f, 0), new Point3f(0, 0, 1), new Vector3f(1, 1, 0), (float)Math.PI/8, 0.5f ); kohtvalgus.setInfluencingBounds(new BoundingSphere()); juur.addChild(kohtvalgus); Materjal Kujundite pindade omadusi määratakse materjali abil. Kui materjal või muul moel väljanägemine puudub, siis pole ka midagi näha. Eralduva valguse abil määratakse värv, mis paistab ka siis, kus keha peale muud valgust ei tule. Selliselt on määratud näiteks värvilise kuubi küljed, millega eespool mitmel pool tutvusime. Koonus ja silinder vaikimisi ei helenda, nende nägemiseks tuleb sinna kas valgus peale suunata või neile endile värvus määrata. Allpool määratakse koonuse helenduv valgus roheliseks ning seetõttu näemegi seda ekraanil rohelisena. BranchGroup juur = new BranchGroup(); Cone k1=new Cone(); Appearance valimus=new Appearance(); Material materjal=new Material( new Color3f(Color.black), //hajuvalgus new Color3f(Color.green), //eralduv new Color3f(Color.green), //peegelduv valgus new Color3f(Color.black), 1 //läige ); valimus.setMaterial(materjal); k1.setAppearance(valimus); juur.addChild(k1); juur.compile(); Muster Lisaks ühtlasele värvile või mitme valguse üheaegsest pealepaistmisest tingitud värviüleminekule võib keha pinnaks määrata mustri. Arvutuskiiruse suurendamiseks peavad mustriks määratud pildi laius ja kõrgus punktides olema arvu 2 astmed, ehk 2, 4, 8, 16 jne. Mõne kujundi (näiteks tasapind) puhul on lisaks vaikemäärangutele võimalik mustripilti ka mitmeti kujundi peale seada: keerata väänata, võtta sellest vaid osa jne. BranchGroup juur = new BranchGroup(); Appearance a=new Appearance(); TextureLoader mustrilugeja=new TextureLoader("taust1.gif", this); ImageComponent2D pilt=mustrilugeja.getImage(); Texture2D muster = new Texture2D(Texture.BASE_LEVEL, Texture.RGBA, pilt.getWidth(), pilt.getHeight()); muster.setImage(0, pilt); a.setTexture(muster); juur.addChild(new Sphere(0.5f, Primitive.GENERATE_TEXTURE_COORDS, a)); juur.compile(); Töö käigus kujundite lisamine Pärast programmi algset käivitamist saab kujundeid lisada vaid koos uue oksa ehk BrahchGroupiga. Sedagi vaid juhul, kui algsele juurele on lisatud omadus ALLOW_CHILDREN_WRITE, ehk maakeeli öelduna tohib siis sinna järglasi juurde panna (või ära võtta). Nii ka siin all olevas näites nupule vajutamise puhul luuakse kõigepealt uus BranchGroup, selle külge lisatakse kujund (kuup). Siis oksa näitamine optimeeritakse (compile) ning alles seejärel lisatake oks olemasoleva juure külge. Kui kõik ilusti töötab, siis on nupu vajutamise peale näha, kuidas kuubi esikülg nupuvajutuse peale ekraanile juurde tekib. Lisada võib ka suuremat kujundite gruppi. Siis tuleb see lihtsalt enne ekraanile paigaldamist valmis teha ning alles siis üheskoos sinna paigutada. Kui soovida lisada mujale kui keskkohta (mis on ju täiesti loomulik soov), siis peab oksa ja esemete vahele paigutama (vähemalt ühe) TransformGroup'i, mille abil siis kujund sobivasse asupaika nihutada ning keerata. import java.awt.*; import java.awt.event.*; import javax.swing.*; import com.sun.j3d.utils.geometry.*; import com.sun.j3d.utils.universe.SimpleUniverse; import javax.media.j3d.*; import javax.vecmath.*; public class Kuubilisamine1a extends JApplet implements ActionListener{ TransformGroup keere1; Button nupp=new Button("Lisa kuup"); BranchGroup juur = new BranchGroup(); public Kuubilisamine1a() { Canvas3D c = new Canvas3D(SimpleUniverse.getPreferredConfiguration()); getContentPane().add(c, BorderLayout.CENTER); getContentPane().add(nupp, BorderLayout.NORTH); juur.setCapability(TransformGroup.ALLOW_CHILDREN_WRITE); juur.compile(); SimpleUniverse u = new SimpleUniverse(c); u.getViewingPlatform().setNominalViewingTransform(); u.addBranchGraph(juur); nupp.addActionListener(this); } public void actionPerformed(ActionEvent e){ BranchGroup oks1=new BranchGroup(); oks1.insertChild(new ColorCube(0.3), 0); oks1.compile(); juur.insertChild(oks1, 0); } } Ruumimängu põhi Järgneval paaril leheküljel oleva näite järgi saab ehitada mitmeid kolmemõõtmelisi mänge – ja miks mitte ka asjalikke tutvustavaid programme, muutes väljanägemist, lisades kaunimaid ja keerukamaid kujundeid, määrates lubatud ja lubamatu liikumise piirkondi ning vajaduse korral andmeid võrgu kaudu teise kasutajani saates ja sealt teispoole teateid arvesse võttes. Samu sündmusi on näha kahel pinnal. Alumises saab kasutaja klahvide abil muuta oma asukohta ruumis, nähes, kuidas tähtedeks olevad kuubikesed tema silmade läbi paistavad. Üleval on näha tema asukohta määravad parameetrid, samuti tasandil kujutadud pinnal kasutaja asukoht ning vaatesuund universumis ning viimase keskkoht. See peaks aitama jälgida enese asukohta ning vältima eksimisi, mis kosmoses ilma vaatlusandmeteta ringi hõljudes on küllalt kerge tulema. Peaklassi sisse on loodud kaks sisemist klassi, üks klahvivajutustele reageerimiseks ja liikumise eest hoolitsemiseks, teine ülemisel tasapinnalisel lõuendil kasutaja andmete näitamiseks. Kõikjal vajaminevad andmed kasutaja asukoha ja suuna kohta kirjeldati peaklassi alguses, nii on need kergesti kõikjale kätte saadavad ning vajaduse korral on ka näiteks võrgu kaudu andmete vahetamist lihtne korraldada. Nooleklahvid "Üles" ja "Alla" liigutavad kasutajat edasi-tagasi vastavalt tema vaatesuunale. Teiste nooleklahvidega saab vaatesuunda vastavalt kas paremale või vasakule pöörata. Enese püstloodis kõrgemale või madalamale nihutamiseks aitavad leheküljeklahvid "Page up" ning "Page down", viltusuunas nihutamiseks lihtuse mõttes vahendeid loodud ei ole. Kuna aga asukohta saab kergesti muutujate väärtuste abil määrata, siis on sellise võimaluse lisamine täiesti võimalik. Praeguse lähenemise juures on kergemini võimalik piirduda kahemõõtmelises ruumis kehtivate matemaatiliste valemitega, muul juhul tuleb kas programmi pikemaks või valemeid keerukamaks ajada. import java.applet.Applet; import java.awt.*; import java.awt.event.*; import com.sun.j3d.utils.geometry.ColorCube; import com.sun.j3d.utils.universe.SimpleUniverse; import com.sun.j3d.utils.behaviors.keyboard.*; import javax.media.j3d.*; import javax.vecmath.*; public class Asukoht3a extends Applet { float x, y, z, uusx, uusy, uusz, nurk=0f, sammx, sammz, samm=-0.5f, nurgavahe=0.05f; //samm on negatiivne, kuna silmad on z-teljel miinuse poole. Canvas3D c = new Canvas3D(SimpleUniverse.getPreferredConfiguration()); SimpleUniverse u = new SimpleUniverse(c); TransformGroup tg=u.getViewingPlatform().getViewPlatformTransform(); Canvas tasapind=new Plaan(); Klahvikuular k=new Klahvikuular(); public Asukoht3a() { setLayout(new BorderLayout()); setLayout(new GridLayout(2, 1)); add(tasapind); add(c); BranchGroup juur = new BranchGroup(); for(int i=1; i<1000; i++){ Transform3D t3=new Transform3D(); t3.setTranslation(new Vector3f((float)(60*Math.random()-30), (float)(60*Math.random()-30), (float)(60*Math.random()-30))); TransformGroup tg=new TransformGroup(t3); tg.addChild(new ColorCube(0.3)); juur.addChild(tg); } juur.addChild(new ColorCube(2)); //suur kuup keskele c.addKeyListener(k); Background taust=new Background(new Color3f(new Color(200, 200, 255))); taust.setApplicationBounds(new BoundingSphere(new Point3d(), 1000)); juur.addChild(taust); u.addBranchGraph(juur); tasapind.addFocusListener(new FocusListener(){ public void focusGained(FocusEvent e){c.requestFocus();} public void focusLost(FocusEvent e){} }); //hoolitksetakse, et klahvidele reageeriv ruumilõuend saaks fookusesse. } double umarda(double arv, double koht){ return Math.round(arv*Math.pow(10, koht))/Math.pow(10, koht); } public static void main(String[] args) { Frame f=new Frame("Kuubid taevas"); f.add(new Asukoht3a()); f.setSize(400, 600); f.setVisible(true); } class Klahvikuular implements KeyListener{ public Klahvikuular(){ arvutaSamm(); } void arvutaSamm(){ sammz=(float)(Math.cos(nurk)*samm); sammx=(float)(Math.sin(nurk)*samm); } public void keyPressed(KeyEvent e){ int klahv=e.getKeyCode(); if(klahv==KeyEvent.VK_RIGHT){ //keerab paremale nurk-=nurgavahe; arvutaSamm(); } if(klahv==KeyEvent.VK_LEFT){ nurk+=nurgavahe; arvutaSamm(); } if(klahv==KeyEvent.VK_UP){ //edasi uusx+=sammx; uusz+=sammz; } if(klahv==KeyEvent.VK_DOWN){ uusx-=sammx; uusz-=sammz; } if(klahv==KeyEvent.VK_PAGE_UP)uusy=y-samm; //üles if(klahv==KeyEvent.VK_PAGE_DOWN)uusy=y+samm; prooviAstuda(); } void prooviAstuda(){ if(kasAsukohtSobib()){ omista(); liiguta(); tasapind.repaint(); } else { piiksu(); } } /** * Meetodi abil määratakse, kas uude arvutatud asukohta tohib kasutaja liikuda. */ boolean kasAsukohtSobib(){ if(uusy<10) return true; else return false; } void omista(){ x=uusx; y=uusy; z=uusz; } void liiguta(){ Transform3D t1=new Transform3D(); Transform3D t2=new Transform3D(); t1.set(new Vector3d(x, y, z)); t2.rotY(nurk); t1.mul(t2); tg.setTransform(t1); } void piiksu(){ Toolkit.getDefaultToolkit().beep(); } public void keyReleased(KeyEvent e){} public void keyTyped(KeyEvent e){} } class Plaan extends Canvas{ public void paint(Graphics g){ g.setColor(new Color(230, 230, 230)); g.fillRect(0, 0, getWidth(), getHeight()); int keskx=getWidth()/2; int kesky=getHeight()/2; int varv=100+(int)(10*y); //vastavalt kõrgusele arvutatakse värv if(varv<10)varv=10; if(varv>240)varv=240; g.setColor(new Color(varv, 0, 0)); g.fillOval(keskx+(int)(2*x)-5, kesky+(int)(2*z)-5, 10, 10); g.setColor(Color.black); g.drawString("x="+(int)x+" y="+(int)y+" z="+(int)z+" : nurk="+umarda(nurk, 2)+ " sammx="+umarda(sammx, 2)+" sammz="+umarda(sammz, 2) , 10, 30); //koordinaate kirjeldav tekst g.drawLine( keskx+(int)(2*x), kesky+(int)(2*z), keskx+(int)(2*x+50*sammx), kesky+(int)(2*z+50*sammz) ); g.drawOval(keskx-5, kesky-5, 10, 10); } public void update(Graphics g){paint(g);} } } Tehniline seletus Import-käsklused on tuttavad eelsmistest näidetest ning täiesti tavalised. java.awt.event.* on siin tarvilik klahvivajutuste registreerimiseks. Canvas3D võimaldab neid kinni püüda ja vastu võtta nagu iga teinegi tavaline graafikakomponent. float x, y, z, uusx, uusy, uusz, nurk=0f, sammx, sammz, samm=-0.5f, nurgavahe=0.05f; Muutujad x, y ja z tähistavad kasutaja parasjagu kehtivat asukohta, nurk vaatenurga muutust võrreldes lähteasendiga (pilk sihitud kaugusse mööda miinusesse eemalduvat z-telge); uusx, uusy ja uusz tähistavad kasutaja uut väljaarvutatud asukohta, enne kui viimane sinna liikunud on. Niimoodi on kergesti võimalik kontrollida, kas uude kohta ta üldse liikuda tohib, samuti, kas enne uude kohta jõudmist tuleb mõni eelnev operatsioon teostada (nt. uks avada või meloodialõik mängida). samm tähistab iga üksiku sammu pikkust ruumis. Negatiivne on ta seetõttu, et algasendis on silmad z-telje miinuspoolele suunatud ning edasi astudes paratamatult koordinaadi väärtus väheneb. Nurgavahe näitab, kui palju tuleb igal keeramisnupule vajutamisel vaatesuunda radiaanides muuta. Sammx ja sammz arvutatakse iga keeramise korral välja, et oleks teada, palju edasi või tagasi liikumise puhul vastavaid koordinaate muuta tuleb. Suurused on float tüüpi ehk ühekordse (mitte topelt) täpsusega reaalarvud, kuna Java3D on loodud enam liikumise tarvis ning siin arvestatakse enam sujuvust kui täpsust. float võtab arvutamisel vähem ressursse ning arvutamisest tekkivad erinevused pole siinse mõõtkava juures nagunii märgatavad. TransformGroup tg=u.getViewingPlatform().getViewPlatformTransform(); Canvas tasapind=new Plaan(); Klahvikuular k=new Klahvikuular(); Kirjeldatud muutujate otstarve on järgmine: tg tähistab kasutaja asukohta määravat TransformGroup'i. Selle omadusi muutes saab kasutajat nihutada ja keerata. Tasapinnal on näha andmed kasutaja asukohta ning klahvikuular hoolitseb selle määramise eest vastavalt vajutatud klahvidele. for(int i=1; i<1000; i++){ Transform3D t3=new Transform3D(); t3.setTranslation(new Vector3f((float)(60*Math.random()-30), (float)(60*Math.random()-30), (float)(60*Math.random()-30))); TransformGroup tg=new TransformGroup(t3); tg.addChild(new ColorCube(0.3)); juur.addChild(tg); } juur.addChild(new ColorCube(2)); //suur kuup keskele Tähtedeks olevatele kuupidele arvutatakse igal korral uus koht. Nii x-, y- kui z-telje peal leitakse igale koordinaadi väärtusele vastama juhuslik arv –30 ning 30 vahel. Seda teeb (60*Math.random()-30) , mis loob juhuarvu vahemikust 0..1, korrutab selle 60-ga (vahemik 0..60) ning lahutab tulemusest 30. Sulgudes (float) võrrandi ees muudab tüübi ühekordse täpsusega reaalarvuks, et tulemus muude siin kasutatavate andmetega kokku käiks. Allpool lisatud suur kuup aitab suures tähemeres orienteeruda ning näitab ekslemisel kätte nullpunkti asukoha. tasapind.addFocusListener(new FocusListener(){ public void focusGained(FocusEvent e){c.requestFocus();} public void focusLost(FocusEvent e){} }); //hoolitksetakse, et klahvidele reageeriv ruumilõuend saaks fookusesse. Asukoha klahvidega liigutamiseks on vajalik, et vajutusi registreeriv komponent oleks fookuses. Siin näites võib aktiivseks osutuda nii ülemine kui alumine graafikakomponent. Esimesel juhul aga teated kuularini ei jõua ning tulemusena võib kasutaja nuppe muudkui vajutada ja vajutada, aga soovitud liikumistulemust ei ole. Lisatud kuulari puhul saadab tasapind koheselt omale fookuse vastuvõtmise puhul selle edasi alumisele kolmemõõtmelisele lõuendile, niimoodi ei lähe kasutaja teated kaotsi. Sarnase tulemuse võiks saada, kui paneksime ka ülemise tasapinna samale klahvikuularile teateid saatma. Viimasel on ükskõik kust teated tulevad, tema ülesandes on neile vastavalt reageerida. Samuti võib ülemisel lõuendil fookuse vastuvõtmise lihtsalt ära keelata, tulemus oleks ikka sama. Niimoodi sisemise klassina loodud fookusekuular suudab alumise lõuendi poole pöörduda kuna viimane on loodud isendimuutujana. Alamprogrammis loodud muutujate puhul on selline lähenemine raskem. double umarda(double arv, double koht){ return Math.round(arv*Math.pow(10, koht))/Math.pow(10, koht); } on lihtsalt abifunktsioon, kus etteantud arv ümardatakse etteantud kohtadeni pärast koma. Ta pole sisuliselt kuidagi siinse programmiga seotud, kuid aitab andmete välja kirjutamisel koodi lühendada. Lahtiseletatult: Math.pow(10, koht) leiab arvu 10 sellise astme, mida koht näitab. Kui koht on 2, siis selle avaldise tulemuseks on 100. Edasi korrutatakse arv leitud väärtusega läbi ning ümardatakse. Kui arv oli enne 0,657, siis nüüd on tulemuseks 66. Et taas algsesse vahemikku tagasi jõuda, tuleb uuesti tulemus leitud kümne astmega jagada. public Klahvikuular(){ arvutaSamm(); } void arvutaSamm(){ sammz=(float)(Math.cos(nurk)*samm); sammx=(float)(Math.sin(nurk)*samm); } Nii klahvikuulari loomisel kui igakordsel vaataja suuna muutmisel palutakse tema nii x- kui z- koordinaadi suunas tehtava sammu pikkus uuesti välja arvutada. Siis saab edasi või tagasi liikumisel rahulikult neid väärtusi koordinaatidele lisada või eemadada, ilma, et peaks energiamahukat siinuse või koosiinuse arvutamist ette võtma. Nagu koolimatemaatikast teada, on koosinus sirge projektsioon ühe ning siinus teise koordinaattelje peal ehk täisnurkse kolmnurga peal vaadatuna lähis- ning vastaskülg. public void keyPressed(KeyEvent e){ int klahv=e.getKeyCode(); if(klahv==KeyEvent.VK_RIGHT){ //keerab paremale nurk-=nurgavahe; arvutaSamm(); } if(klahv==KeyEvent.VK_UP){ //edasi uusx+=sammx; uusz+=sammz; } Olles kasutajalt kinni püüdnud klahvivajutuse (meetod keyPressed käivitus), küsitakse sealt parameetrist edasise mugavama kasutuse huvides välja allavajutatud klahvi kood. Asutakse võrdlema, millist klahvi vajutati ning vastavalt sellele tegutsetakse edasi. Kui klahviks oli "Nool paremale", siis selle tulemusena vähendatakse nurga väärtust nurgavahe võrra ning arvutatakse välja uus samm liikumiseks nii x- kui z suunas. Paremale pööramisel tuleb lahutada seetõttu, et koordinaatteljestikul nurga suurenedes keeratakse vastupäeva, paremale poole saamiseks aga tuleb pöörata päripäeva. if(klahv==KeyEvent.VK_PAGE_UP)uusy=y-samm; //üles Üles liikumisel jäävad x- ning z-koordinaat muutmata, vaid y muutub ning seda siis terve sammupikkuse ulatuses. prooviAstuda(); } void prooviAstuda(){ if(kasAsukohtSobib()){ omista(); liiguta(); tasapind.repaint(); } else { piiksu(); } } boolean kasAsukohtSobib(){ if(uusy<10) return true; else return false; } Klahvivajutuskontrolli lõpuks kutsutakse välja meetod prooviAstuda(), kus otsustatakse, mida uute arvutatud koordinaatidega edasi tegema hakata. Tingimuses kasAsukohtSobib(), kontrollitakse, kas uutele arvutatud koordinaatidele tohib liikuda. Siin näites on tingimus lihtne, kasutajal ei lubata vaid tõusta kümne ja enama ühiku kõrgusele. Keerukamatel juhtudes saab aga siin hoolitseda selle eest, et kasutaja kõnniks ilusti mööda koridore, läbiks etteantud kontrollpunkti või ei jõuaks teisele liiklejale liiga lähedale. Kui leiti, et uus asukoht sobib, siis omistatakse väljaarvutatud koordinaadid keha õigeteks koordinaatideks, liigutatakse ekraanil kasutaja sinna kohta ning samuti palutakse tasapind üle joonistada, et seal kasutaja kohta õiged andmed näha oleksid. Kui püüti liikuda keelatud kohale, siis selles näites tehakse sel puhul piiksu. void liiguta(){ Transform3D t1=new Transform3D(); Transform3D t2=new Transform3D(); t1.set(new Vector3d(x, y, z)); t2.rotY(nurk); t1.mul(t2); tg.setTransform(t1); } Liigutamise tarvis arvutatakse nii nihke kui pööramise tarvis Transform3D. Nende omadused ühendatakse maatriksite korrutamise abil ning tulemus määratakse kasutaja asendiks. class Plaan extends Canvas{ public void paint(Graphics g){ g.setColor(new Color(230, 230, 230)); g.fillRect(0, 0, getWidth(), getHeight()); Üleval paiknev andmelõuend kaetakse joonistuskäsu väljakutsel kõigepealt etteantud hallika värviga kogu oma suuruses. All olev update-meetodist otse paint'i väljakutse tähendab, et ekraani vahepealset lisapuhastust ei toimu. Sama tulemuse võiks isegi veidi vähemate koodiridadega saavutada, kui määrata lõuendi taustavärviks kohe hallikas. Sel juhul update hoolitseb enne joonistuse algust vana pildi mustriga katmise eest. int keskx=getWidth()/2; int kesky=getHeight()/2; Edaspidise joonistamise tarbeks küsitakse lõuendilt keskkoht. Sellisel juhul jääb ruumi keskpunkt lõuendi keskele ka juhul, kui kasutaja viimase suurust peaks muutma. int varv=100+(int)(10*y); //vastavalt kõrgusele arvutatakse värv if(varv<10)varv=10; if(varv>240)varv=240; g.setColor(new Color(varv, 0, 0)); Kasutajat tähistava ringi värvus sõltub kasutaja asukoha kõrgusest. Mida kõrgemal see on, seda punasemalt joonistatakse kasutaja. Samas hoolitsetakse, et värvi määravate muutujate väärtused ei satuks äärmustesse, hoitakse, et punast värvi tähistav komponent oleks 10 ja 240 vahel. Kui 0 ja 255 vahelt väljuda, siis satutaks väärtusteni, mida pole võimalik ekraanil näidata ning väljastataks veateade. g.fillOval(keskx+(int)(2*x)-5, kesky+(int)(2*z)-5, 10, 10); Joonistatakse, arvestades koordinaate lõuendi keskpunktist. Kuna ovaali joonistamisel tuleb määrata vasak serv, siin aga soovime ringi keskpunkti panna kasutaja asukohta tähistama, tuleb 10 punkti laiuse ovaali sobivasse paika loomiseks selle vasak serv viie punkti võrra vasemale nihutada. g.setColor(Color.black); g.drawString("x="+(int)x+" y="+(int)y+" z="+(int)z+" : nurk="+umarda(nurk, 2)+ " sammx="+umarda(sammx, 2)+" sammz="+umarda(sammz, 2) , 10, 30); //koordinaate kirjeldav tekst Kasutaja vaatesuunda näitav joon algab ta asukohast lõuendil ning lõpeb sinna suunas, kuhu kasutaja vaadata ja liikuda saab. Algots lõuendil sõltub niisiis lõuendi keskpunktist ja kasutaja asukohast, lõppotsa puhul aga tuleb juurde veel vaatenurgast sõltuvad väärtused. Meie õnneks on x- ning z-suunalised sammud juba välja arvutatud, piisab vaid need piisavalt suure teguriga korrutada, et need ekraanil mõistlikena välja paistaksid. g.drawLine( keskx+(int)(2*x), kesky+(int)(2*z), keskx+(int)(2*x+50*sammx), kesky+(int)(2*z+50*sammz) ); g.drawOval(keskx-5, kesky-5, 10, 10); } public void update(Graphics g){paint(g);} } Kokkuvõte Java3D abil õnnestub luua ruumiline keskkond, kontrollida ja muuta seal nii esemete kui kasutaja asukohta ja asendit. Piltide ja sissetoodud objektide abil õnnestub sealne keskkond piisavalt tõeliseks, et võiks võiks võrreldavalt muude 3D vahenditega tunda end virtuaalkeskkonna osana. Kõik kasutatavad vahendid peavad olema ühtses andmepuus. Leiduvad vahendid nihutamiseks ja keeramiseks (TransformGroup), automaatseks asukoha ning omaduste muutmiseks (Interpolaatorid) ning sündmuste peale käitumiseks (Behavior). Ülesandeid 3D kujundid · Loo ekraanile mitmesse kohta mitmesuguseid kuupe · Koosta õu laternapostide, istepinkide, põrkava palli ning liigutatava palliga. Luba kasutajal oma asukohta muuta. Katseta värve, mustreid ja interpolaatoreid. · Anna palli koordinaadid ette tekstiväljast. 3D võrgumäng · Mängijate asukohad määratakse serverist tulevate andmete järgi. Samaaegselt on seis näha nii kahe- kui kolmemõõtmelisel kujul. · Platsil on sein. Serveris olevate arvutuste järgi hoolitsetakse, et seda ei saaks läbida. · Kujunda eelnenu põhjal võimalikult tõelisena näiv võrgumäng. Rekursiivne joonistamine Kordused, rekursiooni baas, fraktal Plaan plaani peal Ü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. Lõputu koridor 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); } } Teineteises peituvad hulknurgad 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; i10){ 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)); } } Kasutaja soovitud puu 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. Murdjoon Ü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+1pikimaJoonePikkus){ 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; ipikimaJoonePikkus && (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 && ex0 && ey255)punane=255; if(punane<0)punane=0; int sinine = heledusesb.getValue(); punktid[nr++] = (255<<24)|(punane << 16) | sinine; } } return createImage(new MemoryImageSource(laius, korgus, punktid, 0, laius)); } public Image[] looPildiseeria(int laius, int korgus, int kx, int ky, double lainepikkus, int kaadritearv){ Image[] pildikaadrid=new Image[kaadritearv]; for(int i=0; i=pildid.length)pildinr=0; } }.start(); } void kontrolliSuurust(){ if(pildilaius!=louend.getSize().width || pildikorgus!=louend.getSize().height){ hx=louend.getSize().width/2; hy=louend.getSize().height/2; arvutaPildid(); } } public void adjustmentValueChanged(AdjustmentEvent e){ arvutaPildid(); } public void run(){ veel=true; while(veel){ if(pildid!=null && ++pildinr>=pildid.length)pildinr=0; louend.paint(louend.getGraphics()); try{Thread.sleep(ootesb.getValue());}catch(Exception e){} } } static double kaugus(double x1, double y1, double x2, double y2){ double vahex=x2-x1; double vahey=y2-y1; return Math.sqrt(vahex*vahex+vahey*vahey); } public void joonista(Graphics g){ if(g==null)return; if(pildid!=null && pildinr0; korgus-=500){ Thread.sleep(200); kanal.setPitchBend(korgus); } kanal.allNotesOff(); Pillide loetelu Kanalil mängivat instrumenti saab muuta käsuga programChange, andes parameetritena ette uue pilli helipanga ning panga sees sellele pillile vastava programmijupi järjenumbri. Süntesaatorile kättesaadavad pillid saab küsida getDefaultSoundbank().getInstruments() abil. All näites paiknevad trükitakse tsüklis järgemööda välja pillide nimed ning mängitakse igal pillil noot. Instrument[] pillid=synthesizer.getDefaultSoundbank(). getInstruments(); MidiChannel kanal=synthesizer.getChannels()[0]; for(int i=0; i" + name + "", seq); sequenceNode = new DefaultMutableTreeNode(noh); Track[] rajad = seq.getTracks(); System.out.println(rajad.length+" rada"); for (int nr = 0; nr < rajad.length; nr++){ trackNode = new DefaultMutableTreeNode(rajad[nr]); sequenceNode.add(trackNode); } treeModel.insertNodeInto(sequenceNode, rootNode, rootNode.getChildCount()); } public void actionPerformed(ActionEvent e) { if (e.getSource() == btnPlay) { play(); } else if (e.getSource() == btnStop) { stop(); } else if (e.getSource() == btnCopy) { copyTrack(); } else if (e.getSource() == btnNew) { newSequence(); } else if (e.getSource() == btnCut) { cutTrack(); } else if (e.getSource() == btnPaste) { pasteTrack(); } else if (e.getSource() == btnDelete) { deleteTrack(); } else if (e.getSource() == mnLoad) { loadFile(); } else if (e.getSource() == mnSave) { saveFile(); } } JFileChooser fc = new JFileChooser(); public void loadFile() { int returnVal = fc.showOpenDialog(this); if (returnVal == JFileChooser.APPROVE_OPTION) { file = fc.getSelectedFile(); try { System.out.println("Opening: " + file.getCanonicalPath()); sekvents = MidiSystem.getSequence(file); addSeqenceToTree(sekvents, file.getName()); } catch (Exception ex) { ex.printStackTrace(); } } else { System.out.println("Open command cancelled by user."); } } public void saveFile() { sekvents = getCurrentSequence(); //millist faili salvestada if (sekvents == null) return; int returnVal = fc.showSaveDialog(this); if (returnVal == JFileChooser.APPROVE_OPTION) { file = fc.getSelectedFile(); try { MidiSystem.write(sekvents, 1, new FileOutputStream(file.getCanonicalPath())); System.out.println("Saved: " + file.getCanonicalPath()); } catch (IOException ex) { ex.printStackTrace(); } } else { System.out.println("Save command cancelled by user."); } } JMenuItem mnUus, mnLoad, mnSave; public JMenuBar createMenuBar() { JMenuBar menuBar = new JMenuBar(); JMenu menu = null; JMenuItem menuItem = null; menu = new JMenu("Failid"); menuItem = new JMenuItem("Lae.."); menuItem.addActionListener(this); menu.add(menuItem); mnLoad = menuItem; menuItem = new JMenuItem("Salvesta.."); menuItem.addActionListener(this); menu.add(menuItem); mnSave = menuItem; menuBar.add(menu); return menuBar; } public static void main(String[] args) { // JFrame.setDefaultLookAndFeelDecorated(true); // Käsk kasutatav alates JDK versioonist 1.4 JFrame frame = new JFrame("KMidi"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); KMidi kodContentPane = new KMidi(); kodContentPane.setOpaque(true); frame.setJMenuBar(kodContentPane.createMenuBar()); frame.setContentPane(kodContentPane); frame.pack(); frame.setSize(600, 400); frame.setVisible(true); } } /** * Vahend, mille abil hoida objekti koos tema juurde kuuluva sildiga * Swingi puu tarvis esitataval kujul. Kasutatakse näiteks sekventsi * hoidmiseks. */ class NodeObjectHolder { public String label = "no name"; public Object userObject = null; public NodeObjectHolder() { } public NodeObjectHolder(String label, Object userObject) { this.label = label; this.userObject = userObject; } public String toString() { return label; } } Ülesandeid Mandoliin · Joonista ekraanile mandoliin. · Kasutaja saab määrata joonistatavate krihvide arvu ning kaela laiust. · Vajutades keele ja krihvi ristumiskohale, kõlab sellele vastav heli. Kitarri mudel · Joonista 6-keelse kitarri kaela mudel · Mängi kitarriakord (E, H, G, D, A, E) (64, 59, 55, 50, 45, 40) · Peenemal keelel saab määrata, milline krihv on alla vajutatud. Hääl kõlab vastavalt vajutatule · Krihve saab valida ka teiste keelte puhul ning kuulata tulemust · Lisaks võib valida täisakorde. Akordion · Joonista klaviatuuri ja 36 bassiga akordion. · Basside ridade ja veergude arvu saab kasutaja määrata. Samuti hiire abil lõõtsa lahti ja kokku vedada. · Lisaks eelmisele kõlab bassinuppudele vajutades neile vastav heli. Vilepill · Joonista vilepill. Pillil on avatud tekstiväljas määratud hulk auke. · Lisaks eelmisele teeb pillile vajutamisel viimane häält. · Augule vajutamisel on augud lõpust kuni sinnani avatud. Kõlab sellisele sõrmede paigutusele vastav hääl. MIDI fail · Mängi MIDI fail · Väljasta radade arv · Salvesta fail tooni jagu kõrgemalt. · Kopeeri faili muusika iseendale sappa · Lisa rada, kus viisi mängitakse nihkega (kaanon) Digitaalheli MIDI abil võib kord kokku pandud pillide helisid uuesti esitada, mängida saab vaid helikõrgusega ning kestusega. Olematute pillide hääli aga sünteesida ei õnnestu, samuti tuleb loobuda muudest heliefektidest. Kui palju kõlari membraan mingil hetkel välja venitatud on, seda otsustab meie eest helikaart või vastav süsteemiprogramm ning programmeerijal selle koha pealt kuigi palju kaasa rääkimist ei ole. "Hariliku" muusika loomise puhul ongi nii hea, sest saame rahumeeli noodikõrgustele ja -kestustele mõelda ning ei pruugi tehniliste andmete peale üleliia oma energiat kulutada. Kui aga tahta olemasolevat häält moonutada, kaja tekitada või hoopis uusi kõlasid luua, siis tuleb hakata mõtlema helilainete kuju peale. Edasi tuleb koostatud kuju kvantida, et saaks kusagil arvumassiivis hoida igale ajahetkele vastavat heligraafiku väärtust sarnaselt nagu matemaatikaski võime esitada funktsiooni x-idele vastavate y-ite massiivina, kus x-teljel oleks aeg ning y- il helirõhu kõrvalekalle tasakaaluasendist. Kvantimissagedusest (kandesagedusest) sõltub helikõvera edasiandmise kvaliteet. Mängitav kõver luuakse punkte ühendavatest sirgete järgi ning mida tihedamalt on punkte, seda lähedasemalt õnnestub säilitada esialgse lindistatud või välja arvutatud kõvera kuju. Lihtne piiks Järgnevalt on püütud kokku panna võimalikult lihtne heli, mida ka kõrvaga kuulda oleks. Kvantimissageduseks on määratud 1000 mõõtmist sekundis. Helirõhud on kordamööda määratud maksimumile ja miinimumile. Sedasi on väljastatava heli sageduseks pool kvantimissagedusest ehk 500 hertsi. Mõõtmistäpsuseks on üks bait ning heli väljastatakse ühe kanalina (mono, mitte stereo). Nõnda saab mõõtmistulemused panna ilusti baidimassiivi, kus igale kvandile vastab üks bait. AudioFormat'i abil määratakse, millist tüüpi andmed tulemas on. AudioSystem'i abil küsitakse operatsioonisüsteemilt SourceDataLine, kuhu saadetud heliandmed jõuavad lõpuks kasutaja kõrvadeni. Pärast voo avamist võime sinna andmeid saata kirjutades andmeid loodud voogu. import javax.sound.sampled.*; public class Piiks1{ public static void main(String[] args) throws Exception { int kandesagedus =1000; byte[] andmed=new byte[5*kandesagedus]; //5 sekundit for(int i=0; i> 8 & 0xFF); //eelviimane bait Ning näide ise: import javax.sound.sampled.*; import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Piiks3Rakend extends Applet implements ActionListener, Runnable{ Button nupp=new Button("Piiksu"); Checkbox korda=new Checkbox("Korda"); public Piiks3Rakend(){ setLayout(new BorderLayout()); add(nupp); add(korda, BorderLayout.SOUTH); korda.setState(true); nupp.addActionListener(this); } public void actionPerformed(ActionEvent e){ new Thread(this).start(); } public void run(){ try{ int kandesagedus =44100; int sagedus=400; int mitmeBitineHeli=16; int kanaliteArv=1; int valjus=7000; //max 32767 int nr=0; byte[] andmed=new byte[5*kandesagedus*mitmeBitineHeli/8]; //5 sekundit while(nr> 8 & 0xFF); //eelviimane bait } AudioFormat formaat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, kandesagedus, mitmeBitineHeli, kanaliteArv, kanaliteArv*mitmeBitineHeli/8, kandesagedus, false); SourceDataLine line = (SourceDataLine) AudioSystem.getLine( new DataLine.Info(SourceDataLine.class, formaat, AudioSystem.NOT_SPECIFIED) ); line.open(formaat); line.start(); do{ line.write(andmed, 0, andmed.length); }while(korda.getState()); line.close(); }catch(Exception ex){ex.printStackTrace();} } public static void main(String[] argumendid){ Frame f=new Frame("Piiks"); Piiks3Rakend p=new Piiks3Rakend(); f.add(p); f.setSize(200, 100); f.setVisible(true); p.actionPerformed(null); } } Digitaalheli redaktor. Mõnerealiste koodinäidetega õnnestus läbi proovida märgatav osa kvanditud heliga ümberkäimise tehnilistest käskudest. Ehkki lõpuks taandub suurem osa tegemisi otseste kvantide väärtuste väljaarvutamise ning tulemuste analüüsi peale, õnnestub kõrvale ehitatud kestprogrammi abil matemaatiliselt kuivana tunduvat arvutamist kasutajale märgatavalt mugavamaks muuta. Heliredaktorit on märkimisväärne osa meist kasutanud, paljudel pole aga aimu, kui kerge või keeruline võiks olla selline rakendus kokku panna. Siin on püütud alustada võimalikult lihtsast näitest ning korduste teel jõutud rohkemate võimalustega rakenduseni. Siin ettetulevaga sarnaseid küsimusi peab lahendama enamiku kasutajalt sisestust ootava helidega seotud rakenduste puhul. Lihtsaim heliredaktor õnnestub kirjutada mõneteistkümne reaga ning tema ainuke oskus on koodi otse sisse kirjutatud. Olgu selleks siis faili salvestamine vaiksemaks, tagurpidi või kahe helifaili sisu kokkuliitmine. Üldiseks malliks ikka, et tuleb algsed failid sisse lugeda, leida juurdepääs üksikute kvantide andmete juurde. Seal soovitud muutused sisse viia või olemasolevate andmete põhjal uus massiiv kokku kirjutada ning lõpuks tulemus kasutajale ette mängida või faili salvestada. Rakendused asuvad keerukamaks minema selle osa juurest, kus kasutajal antakse voli määrata, milliseid muutusi andmetega ette võtta. Märgistusala Esimeseks näiteks on redaktor, kus kasutaja saab faili programmi mällu lugeda, seal andmeid kustutada ja kopeerida ning siis tulemuse uude faili talletada. Heli mängimise ega helikõvera vaatamise võimalust ei pakuta. Sobiva lõigu märkimiseks on loodud eraldi klass Margistusala1. Klassi nähtavaks osaks on lõuend, kus kasutaja saab omale soovitava lõigu märkida. Jäetakse lihtsalt meelde protsendid, kust maalt kuhu maale sedakorda kasutaja oma lõigu märkis. Alale eraldatud laiust tähistatakse 100% ehk 1,0-ga. Kui kasutaja juhtus märkima näiteks loo keskmise kolmandiku, siis see talletatakse muutujates väärtused 0,33 ja 0,66 abil vastavalt algus- ja lõppprotsendi tarbeks. Muutuja kaudu kannatab isendile ette anda mälus hoitava andmeploki kogupikkuse ning sellele vastavalt teatavad meetodid algKaader ja loppKaader, mitmendast kaadrist mitmenda kaadrini kasutaja märgistatud ala paikneb. Et testiks on klassil küljes ka main-meetod, saab komponenti ka eraldi katsetada ning veenduda, et kasutajal soovitud lõike ka märgistada õnnestub. import java.awt.*; import java.awt.event.*; public class Margistusala1 extends Panel implements MouseListener{ int kogupikkus=50000; double algusprotsent=0.1, loppprotsent=0.4; Color margistusvarv=Color.black; public Margistusala1(){ addMouseListener(this); } public void paint(Graphics g){ g.setColor(margistusvarv); g.fillRect( (int)(algusprotsent*getWidth()), 0, (int)((loppprotsent-algusprotsent)*getWidth()), getHeight() ); } public void mousePressed(MouseEvent e){ algusprotsent=e.getX()/(double)getWidth(); } public void mouseReleased(MouseEvent e){ loppprotsent=e.getX()/(double)getWidth(); if(loppprotsentjava Digiheliredaktor3 PCM_SIGNED, 22050.0 Hz, 16 bit, stereo, little-endian, audio data Heli väljatrükitud formaat. Kommenteeritud rakendus pole veel kaugeltki täiuslik, kuid ega eesmärgiks polegi ühe järjekordse helitöötlusprogrammi loomine, milliseid juba nii vaba kui kommertstarkvara hulgas mitmeid kätte saada on. Pigem tuleb ise kirjutada selliseid lõike, mis olemasolevat lindistust soovitud suunas analüüsivad või siis kasutaja toimetuste saateks sobilikke helisid kokku panevad. Nagu ehk katsetuste juures kuulda võisite, kippusid lõikamise ja kleepimise juures krõpsud sisse tulema. Põhjuseks, et nõnda lõikamisel ei tea baite kopeeriv koodilõik helikõvera kujust midagi ning sujuva joone sisse tekivad järsud murdekohad. Krõpsuga koht Üheks krõpsude vähendamise võimaluseks on kokkuliidetavad pooled veidi üksteise peale paigutada ning eelnev heli mõne võnke jooksul summutada ning järgnev heli sama aja sees vaikusest paari võnkega üles võimendada. Kui kokku liidetavad helid on ligikaudugi sama amplituudi ja võnkesagedusega, siis võib õnnestuda võnkumised kokku ühendada samas faasis ning nii silmade kui kõrvade jaoks tundub, et helid oleksid nagu algusest peale kokku kuulunud. Kokkuvõte Java muusikavõimalused on tasapisi laienenud ning alates versioonist 1.3 õnnestub nii MIDI vahenditega orkestrilugusid kokku panna kui ise digitaalhelisid luua ja töödelda. Nagu mujal, nii ka siin pole programmeerimiskeel imevahend, selle kaudu lihtsalt õnnestub arvuti vastavatele seadmetele ligi pääseda ning soovitud helisid salvestada või kuuldavale tuua. Muusikalised ja matemaatilised tagamaad tuleb ikka enesel läbi mõelda ning pärast esmavahenditega tutvumist tulebki rakenduse loomisel enam sellele pühenduda. Lugudele saate koostamisel tuleb mõelda harmoonia ning taustalõikude peale neid sobivasse helistikku paigutades. Olenevalt muusikastiilist saab sageli kolme põhiduuriga enamikuga soovitust toime ning alles ilustuste ja kaunistuste juures tuleb muud oskused ja vahendid meelde tuletada ning arvutile selgeks teha. Olemasolevat noodifaili muutes tuleb arvestada, millistel radadel asuvate häältega me mida ette võtta soovime. Digitaalhelis tuleb helilaine kuju ise välja arvutada. Nõnda kulub küll juba lihtsate häälte tekitamiseks paras kogus matemaatikat ning vähegi ilusamate helide loomiseks tuleb hulga arvutada ning tarkadest raamatutestki tarkust juurde ammutada, kuid põhimõtteliselt on võimalik luua või olemasolevatest kokku panna pea iga heli, mis vähegi ettekujutatav võiks olla. Ülesandeid Heli kõrgus · Mängi signaali sagedusega 440 Hz. · Pane signaali kõrgus sõltuma hiire asukohast ekraanil. · Hoolitse, et kõrguse muutumine oleks sujuv. · Korraga kõlab kaks heli. Ühe kõrgus sõltub hiire x- ning teine y-koordinaadist. · Salvesta loodud heli faili. Helifaili muundamine · Salvesta helifail kaks korda üksteise järele. · Salvesta helifail algsest poole vaiksemalt. · Salvesta helifail iseenesega kajanihkega. Lõppsõna Eelnevatel lehekülgedel pole kirjas kaugeltki kõik Java ja graafika ja muusikaga seonduv, kuid märgatavale osale tekkivatest ideedest peaks siinsest kirjutisest omale teostusaluse leidma. MIDI poolest tasub kokku võtta kogu eneses leiduv muusikaalane fantaasia ning lihtsa taustaviiuli asemel terve orkester viisile saatjaks välja mõelda. Rääkimata lihtsatest ja ilusatest kõllidest, mida õnnestub oma rakendusele kasutaja meeleolu ilmestamiseks juurde lisada. Digitaalhelinäidetest peaks piisama, et akustikaõpiku abil lihtsast kajast või valjenemisest tunduvalt keerulisemaidki efekte kokku panna. Graafika vallas pean tähtsaks tegelikest nähtustest koostatud arvutimudeleid, mis võimaldavad nii näidata kui katsetada olukordi, mida käepäraste vahenditega tülikas või suisa võimatu on luua. Olgu siis tegemist õpetajaga koolitunnis, autojuhiga liikluses või piloodiga taeva all. Kel piltide ilusama kujundusega või sealt info eraldamisega vaja rinda pista, sel tuleb millalgi lähemalt tutvuda Java Advanced Imaging lisapaketiga. Küllalt palju aga õnnestub ka ise „põlve otsas" pildist eraldatud piksleid uurides ja muutes kokku panna. Video töötlejad ja ülekandjad vajavad Java Media Frameworki, kus suur osa vajalikku tööd juba ära tehtud, piisab vaid öelda, mida ja kuidas näidata vaja. Eraldi tähelepanu väärib graafika mobiilirakenduste juures. Viimase paari aastaga on Java loojate unistused selles valdkonnas hakanud täituma ning võib loota, et mõne aasta jooksul jõutakse üksikutest töötavatest vidinatest ka lahendusteni, mis paljude kasutajatele tarvilikke lisandusi miniseadmetesse toovad. Kes on kõik eelnevad näited ja seletused läbi lugenud, mõelnud, proovinud ja enese jaoks edasi arendanud, sel kuluks veidi puhata, tagasi vaadata ning kogunenud killukestest enesele edasiseks tervik mõelda, millele edaspidi kindlalt toetuda, nii et julgeb pea iga siinse valdkonnaga seotud toimetuse ette võtta. Kui loetud vähem, aga kasvõi mõni seletuse või koodi lõik on aidanud oma tegemisi edasi viia ehk teadmisi korrastada, ka siis on õpitust kasu olnud. Tänan lugemast! =========== FIELD SUMMARY =========== ======== CONSTRUCTOR SUMMARY ======== ========== METHOD SUMMARY =========== 2 2