Multimedia Prosjekt Rapport - Våren 1999
Interaktiv læring i Grafisk Databehandling
Frode Kristensen og Jostein Trondal.
Indeks
Eksempel på hvordan man legger til en ny Applet
Aroma.java kildekode med kommentarer
dda.java kildekode med kommentarer
Kommentarer til scanline og midp
Formålet med dette prosjektet var å utvikle et system som interaktivt demonstrerer noen deler av pensumet i faget Grafisk Databehandling ved Høgskolen i Agder. Målet er at studenter som følger faget skal kunne bruke systemet til å lettere lære seg hvordan algoritmer og prinsipper i faget fungerer. Systemet er utviklet i Java, og skal kunne kjøres fra en vanlig web-browser.
Vi har tatt for oss tre metoder i Grafisk Databehandling;
- Digital Differential Analyser for scankonvertering av linjer
- Bresenhams midpoint algoritme for scankonvertering av linjer
- Scanlinje algoritmen for fylling av polygoner
Disse metodene demonstreres vha. Applets. Alle metodene har en info-tekst om hva algoritmen går ut på. Vi valgte derfor, å samle fremgangsmåten på å bruke Appletene i en Applet-viewer. For å følge Java-klisjeen om at de fleste programmer utviklet for å håndtere Java .class filer har et navn som har noe å gjøre med kaffe, har vi valgt å kalle Applet-vieweren vår for Aroma. Dette er en videre-utvikling av resultatet av et prosjekt i faget Objektorientert Analyse og Design Implementasjon.
Filer i Aroma-systemet:
fig. 1Aroma.class
Dette er hoved-Appleten som startes med HTML-parameterne som vist nedenfor.
Aroma.html
APPLET code = Aroma.class width=640 height=480> <PARAM NAME=NumberOfModules VALUE="3"> <PARAM NAME=NameOfModule1 VALUE="dda"> <PARAM NAME=NameOfModule2 VALUE="midp"> <PARAM NAME=NameOfModule3 VALUE="scanline"> </APPLET>Dette er kode som gjør det mulig for Aroma å finne Appletene som skal viewes.
AromaInterface.class
Alle Appleter som skal brukes i Aroma, må implementere dette interfacet;
interface AromaInterface { public String getInfo(); public String getTitle(); public String getListname(); public java.applet.Applet getApplet(); }
fig. 2: Aroma i aksjon. Teksten under henviser til bokstavene i bildet.getInfo() skal returnere info-teksten om Appleten (C i fig. 2)
getTitle() skal returnere tittelen til Appleten (D i fig. 2)
getListname() skal returnere navnet Appleten skal ha i menyvalget (A i fig. 2)
getApplet() skal returnere peker til en Applet (som regel this) (B i fig. 2)Knappen (E i fig. 2) brukes til å alternere på størrelsen på feltet B.
dda.class
Appleten som demonstrerer Digital Differential Analyser for scankonvertering av linjer.
midp.class
Appleten som demonstrerer Bresenhams midpoint algoritme for scankonvertering av linjer.
scanline.class
Appleten som demonstrerer Scanlinje algoritmen for fylling av polygoner.
splash.gif
Bildet som vises i feltet B i fig. 2 når Aroma startes.
Eksempel på hvordan man legger til en ny Applet
Steg man må følge for å legge til en ny Applet:
- Lag en ny Applet / Ta utgangspunkt i en gammel Applet
- Appleten må implementere AromaInterface med tilhørende metoder
- Aroma.html (eller tilsvarende fil) sin APPLET tag må endres i parameter-feltene
Vi skal nå legge til en ny Applet, og vise hvert steg;
1. Først lager vi en simpel Applet som viser en hvit ellipse på sort bakgrunn;
class nyapplet extends java.applet.Applet { public void paint(java.awt.Graphics g) { g.setColor(java.awt.Color.black); g.fillRect(0, 0, getSize().width, getSize().height); g.setColor(java.awt.Color.white); g.fillArc(0, 0, getSize().width, getSize().height, 0, 360); } }
Applet med hvit ellipse på sort bakgrunn.
2. For at denne Appleten skal kunne brukes i Aroma, må den implementere AromaInterface;
class nyapplet extends java.applet.Applet implements AromaInterface { public void paint(java.awt.Graphics g) { g.setColor(java.awt.Color.black); g.fillRect(0, 0, getSize().width, getSize().height); g.setColor(java.awt.Color.white); g.fillArc(0, 0, getSize().width, getSize().height, 0, 360); } public String getInfo() { return "infotekst til nyapplet"; } public String getTitle() { return "tittel til nyapplet "; } public String getListname() { return "nyapplet"; } public java.applet.Applet getApplet() { return this; } }
3. Aroma.html (eller tilsvarende fil) sin APPLET tag må også endres:
<APPLET code = Aroma.class width=640 height=480> <PARAM NAME=NumberOfModules VALUE="4"> <PARAM NAME=NameOfModule1 VALUE="dda"> <PARAM NAME=NameOfModule2 VALUE="midp"> <PARAM NAME=NameOfModule3 VALUE="scanline"> <PARAM NAME=NameOfModule4 VALUE="nyapplet"> </APPLET>
Resultat (gif-bilde):
Aroma.java kildekode med kommentarer
Koden under gjør følgende (hovedtrekk):
- Leser HTML-parameterne og sjekker om Appletene som er referert der eksisterer, og om de virker skikkelig. Feilsjekking sørger for å gi beskjed i info-vinduet om noe er galt, og eventuelt hva som kan gjøres for å rette opp feilen. OK Applets legges inn i listeboksen.
- Lytter til valg i listeboksen og laster inn tilsvarende Applet, initierer og starter den. Hvis andre Applets kjører fra før, stoppes og destroyes de først.
- Lytter til trykk på resize-knappen som tar bort info-feltet og listeboksen, stopper og destroyer kjørende Applet, initierer og starter Appleten igjen med større felt (eller motsatt hvis man kjører i stort felt modus).
// Aroma v1999.mar.16 import java.applet.*; // classes: Applet import java.awt.*; // classes: BorderLayout, Button, Color, Graphics, Image, Insets, Label, List, Panel, TextArea import java.awt.event.* ; // interfaces: ActionListener, ItemListener import java.util.Vector; // classes: Vector public class Aroma extends Applet implements ItemListener, ActionListener { final String version = "Aroma v1999.mar.16"; // version string int width, height; // max width and height of this applet (updated in paint()) Image splashimage; // image loaded in beginning boolean showsplash = true; // show image until module choice boolean firstTime = true; // some things in paint is done only once boolean isSmall = true; // true if module-area is small boolean modulesLoaded = false; // true if modules are accessed first time int numberOfModulesParam; // number of modules (from parameterlist) int numberOfModulesLoaded = -1; // (how many modules actually found) - 1 Vector moduleNameList = new Vector(); // storage of names of successfully found modules Panel northpanel = new Panel(new BorderLayout(0,0)); // contains title of moduleapplet and resize button Button bResize = new Button("big"); // resize button Label title = new Label(version); // title of module on top of screen List list = new List(); // listbox with choice of modules TextArea info; // info about module on bottom of screen AromaInterface module = null; // module "handle" / "pointer" public void init() { if (module!=null) module.getApplet().init(); } public void start() { if (module!=null) module.getApplet().start(); } public void stop() { if (module!=null) module.getApplet().stop(); } public void destroy() { if (module!=null) module.getApplet().destroy(); } public Insets getInsets() { return new Insets(3,3,3,3); } public String unknownErrorMessage() { return "This is a strange bug. If you know how to reproduce it, please notify the authors.\n\n"; } public boolean moduleLoader(String modName) { try { module = (AromaInterface)Class.forName(modName).newInstance(); } catch (ClassNotFoundException a) { info.append(modName + "... failure; ClassNotFoundException! --Maybe the module name is misspelled?\n"); return true; } // Thrown by forName catch (IllegalAccessException a) { info.append(modName + "... failure; IllegalAccessException!\n"); info.append(unknownErrorMessage()); return true; } // Thrown by newInstance catch (InstantiationException a) { info.append(modName + "... failure; InstantiationException! --Maybe the module is abstract?\n"); return true; } // Thrown by newInstance catch (ClassCastException a) { info.append(modName + "... failure; ClassCastException! --Maybe the module does not implement AromaInterface?\n"); return true; } // Thrown by Java itself (java.lang ?). The AromaInterface cast will not work if the module does not implement AromaInterface return false; } public void paint(Graphics g) { if (firstTime) { splashimage = getImage(getCodeBase(), "splash.gif"); // get splashimage numberOfModulesParam = java.lang.Integer.parseInt(getParameter("NumberOfModules")); width = getSize().width; // get width of applet height = getSize().height; // get height of applet int c=10; if (height<420) c=(int)(height/48); // if height is unreasonable small, compensate with info-field info = new TextArea("", c, 80, TextArea.SCROLLBARS_VERTICAL_ONLY); info.setBackground(Color.white); info.setEditable(false); list.setBackground(Color.white); list.addItemListener(this); title.setBackground(Color.lightGray); setLayout(new BorderLayout(2,2)); // 2,2 = Gaps between components setBackground(Color.lightGray); // Color of applet background bResize.setEnabled(false); bResize.addActionListener(this); northpanel.add("Center",title); northpanel.add("East",bResize); add("East", list); add("South", info); add("North", northpanel); firstTime = false; validate(); } // draw edge around main applet g.setColor(Color.white); g.drawLine(0,0,width-2,0); g.drawLine(0,0,0,height-2); g.setColor(Color.black); g.drawLine(1,height-1,width-1,height-1); g.drawLine(width-1,1,width-1,height-1); if (showsplash==true) g.drawImage(splashimage,(int)((width-list.getSize().width-splashimage.getWidth(null)-3*3)/2)+3,(int)((height-info.getSize().height-northpanel.getSize().height-splashimage.getHeight(null)-4*3)/2)+6+northpanel.getSize().height,null); if (!modulesLoaded) { // Tries to access all modules to get info about name and accessability String moduleName; info.append("HTML claims there are " + numberOfModulesParam + " modules available. Attempting to load:\n"); for(int n=1; n<=numberOfModulesParam; n++) { moduleName = getParameter("NameOfModule"+n); if ((moduleName + "").compareTo("null") == 0) { info.append(moduleName + "... failure; No Class Found! --Maybe the NumberOfModules parameter has the wrong value?,\n or the NameOfModule parameter has the wrong index?\n"); continue; } if (moduleLoader(moduleName)) continue; // Try to load the class, skip if error numberOfModulesLoaded++; // if no exception happened, we assume all is okay moduleNameList.addElement(moduleName); // add modulename to modulenamelist list.addItem(module.getListname()); // add item to list info.append(moduleName + "... success\n"); // add success remark to info } info.append("ready.\n"); modulesLoaded = true; } // if (!modulesLoaded) } // Interface ActionListener implementation... (for resize button) public void actionPerformed(java.awt.event.ActionEvent e) { int choice = list.getSelectedIndex(); if (choice > -1) { if (module != null) { remove(module.getApplet()); // remove active module applet module.getApplet().stop(); // stop active module applet module.getApplet().destroy(); // destroy active module applet } if (isSmall == true) { remove(info); // remove info remove(list); // remove list isSmall = false; // the module applet view is now big bResize.setLabel("small"); // update button label to "small" } else { add("East", list); // add list again add("South", info); // add info again isSmall = true; // the module applet view is now small bResize.setLabel("big"); // update button label to "big" } add("Center", module.getApplet()); // add module applet module.getApplet().init(); // init module applet module.getApplet().start(); // start module applet validate(); // get this.applet to re-layout its members } } // actionPerformed() // Interface ItemListener implementation... (for listbox) public void itemStateChanged(java.awt.event.ItemEvent e) { int choice = list.getSelectedIndex(); boolean error; if (choice > -1) { showsplash=false; title.setText(""); info.setText(""); if (module != null) { remove(module.getApplet()); // remove active module applet module.getApplet().stop(); // stop active module applet module.getApplet().destroy(); // destroy active module applet } // Try to load the module (this should almost always work, since we have already established which modules work, and which dont) error = moduleLoader((String)moduleNameList.elementAt(choice)); if (error==false) { title.setText(module.getTitle()); // update title info.setText(module.getInfo()); // update info add("Center", module.getApplet()); // add new module applet module.getApplet().init(); // init new module applet module.getApplet().start(); // start new module applet bResize.setEnabled(true); validate(); // get this.applet to re-layout its members } } } // itemStateChanged() } // Aroma()
dda.java kildekode med kommentarer
// dda v1999.mar.9 import java.applet.*; import java.awt.*; // classes: Panel, Point, Rectangle, Image, Color, Graphics import java.awt.event.*; // interfaces: AdjustmentListener (til scrollbars) class dda extends Applet implements AromaInterface, AdjustmentListener, MouseListener { Image offscreen; // doublebuffer Scrollbar vertScroll, horiScroll; // vertikal og horisontal scrollbar boolean firstTime; // holder rede på om paint er kjørt for første gang float dx, dy; // grid elementbredde og grid elementhøyde float k; // faktor av skjermhøyde brukt til grid (mellom 0 og 1) int sizew, sizeh; // bredde og høyde på hele appleten int x, y; // tellere i for-løkker int i, i2; // forskjellige "insets" int scrollw; // scrollbar bredde int tabw; // lengde fra kant av scrollbar til midten av scrolltab når tab står i den ene siden int gh, gw; // høyde og bredde på hele griden int nvert, nhoris; // antall gridelementer vertikalt og horisontalt int ax, ay, bx, by; // koordinater til øverste venstre gridhjørne og nederste høyre gridhjørne int scrVx, scrVy, scrVw, scrVh; // vertikal scrollbar koordinater og størrelse int scrHx, scrHy, scrHw, scrHh; // horisontal scrollbar koordinater og størrelse int fonth, fontasc; // fonthøyde og font-ascent int wheregx, wheregy, o_wheregx, o_wheregy; // hvor scrollbaren er horisontalt og vertikalt int ix0, iy0, ix1, iy1; // koordinater til rektangel hvor info om variabler står int x0, x1, y0, y1, DX, DY, X, Y; // synlige variabler relatert til dda algoritmen float m; // stigning til linje // Constructor public dda() { setBackground(Color.lightGray); vertScroll = new Scrollbar(Scrollbar.VERTICAL, 0, 160, 0, 300); vertScroll.addAdjustmentListener(this); vertScroll.addMouseListener(this); horiScroll = new Scrollbar(Scrollbar.HORIZONTAL, 0, 160, 0, 300); horiScroll.addAdjustmentListener(this); horiScroll.addMouseListener(this); } public void init() { setLayout(null); // bruker ikke noen layoutmanager; definerer alle koordinater selv add(vertScroll); add(horiScroll); firstTime = true; // noen ting i paint skal kun skje en gang } // AromaInterface implementasjon public String getInfo() { return "\n" +"DDA (Digital Differential Analyzer)\n" +"\n" +"Den mest innlysende måten å tegne opp en linje på er å ta utgangspunkt i den matematiske formelen for en linje (y = m*x + b) hvor m er helningen og b er hvor langt opp på y-aksen linjen krysser. Hvis vi tar for oss en linje med -1 < m < 1, kan vi la x-verdien øke med 1 for hver vurdering av hvilken piksel som skal tennes. Poenget med dette er at vi kan utnytte det ved å lage en mer effektiv algoritme;\n" +"\n" +" y{i+1} = m*x{i+1} + b = m*(x{i} + 1) + b = y{i} + m\n" +"\n" +"Altså: For å finne neste verdi for y, legger vi bare til m for hver x.\n" +"\n" +"Vi må i de fleste tilfeller runde av eksakt linjekoordinat til nærmeste heltallsverdi for å bestemme hvilken piksel som skal tennes. En avrundings-funksjon er som regel på formen;\n" +"\n" +" ( x{i} , floor(y{i} + 0.5) )\n" +"\n" +"Hvor floor betyr største heltall mindre eller lik y{i} (det samme som å typecaste til en int i Java og c++)\n" +"\n" +"Ulempen med denne metoden er at avrundingsfeilen som oppstår her blir akkumulert for hver iterasjon, noe som fører til at den tilpassede linjen kan få feil verdi i kritiske punkter. De kritiske punktene er der avstanden fra eksakt tilpassing til ideell linje er mindre enn den akkumulerte feilen.\n" +"\n" ; } public String getTitle() { return "Linjetegningsalgoritme / DDA"; } public String getListname() { return "Linje / DDA"; } public Applet getApplet() { return this; } // AdjustmentListener implementasjon public void adjustmentValueChanged(AdjustmentEvent e) { wheregx=(int)(((float)(horiScroll.getValue())/dx) + 0.5); // få ny xverdi fra scrollbar wheregy=(int)((nvert-(float)(vertScroll.getValue())/dy) + 0.5); // få ny yverdi fra scrollbar if ((o_wheregx != wheregx) || (o_wheregy != wheregy)) repaint(); // hvis x/y verdi har forandret verdi, tegn skjerm på nytt o_wheregx = wheregx; o_wheregy = wheregy; } public void mouseClicked(MouseEvent e) {} // Invoked when the mouse has been clicked on a component. public void mouseEntered(MouseEvent e) {} // Invoked when the mouse enters a component. public void mouseExited(MouseEvent e) {} // Invoked when the mouse exits a component. public void mousePressed(MouseEvent e) {} // Invoked when a mouse button has been pressed on a component. public void mouseReleased(MouseEvent e) { // Invoked when a mouse button has been released on a component horiScroll.setValue((int)(wheregx*dx)); // tving horisontal scrollbar til heltallig grid-koordinat vertScroll.setValue((int)((nvert-wheregy)*dy)); // tving vertikal scrollbar til heltallig grid-koordinat } // en del av doublebuffer koden public void invalidate() { super.invalidate(); offscreen = null; } // en del av doublebuffer koden public void update(Graphics g) { paint(g); } // konverterer grid-koordinater til verdenskoordinater i x-retning int grid(float gridcoord, float d, float a) { // 1 <= gridcoord <= lvert return (int)((float)(gridcoord)*d + (float)a + 0.5); } // konverterer grid-koordinater til verdenskoordinater i y-retning int grid2(float gridcoord, float d, float a) { return (int)((float)(nvert-gridcoord)*d + (float)a + 0.5); } // tegner "ideell" linje void gridLine(Graphics g, float x, float y) { g.drawLine(grid(0,dx,ax), grid2(0,dy,ay), grid(x,dx,ax), grid2(y,dy,ay)); } // tegner en "knott" med ønsket størrelse i grid void tegnknott(Graphics g, int x, int y, int w) { int d=w/40, r=w/80; g.fillArc(x-r, y-r, d, d, 0, 360); } // regner ut n'te y-verdi av dda-algoritmen int ddaline(float n, float x, float y) { return (int)((y/x)*n+0.5); } public void paint(Graphics g) { String stmp = new String(); // brukes til tekst ut på skjerm Integer itmp; // brukes til tekst ut på skjerm Float ftmp; // brukes til tekst ut på skjerm // initialisering av disse variablene skal kun gjøres en gang // informasjon om hva disse variablene brukes til, står i toppen av dda-klassen if (firstTime==true) { sizew = getSize().width; sizeh = getSize().height; fonth = g.getFontMetrics().getHeight(); fontasc = g.getFontMetrics().getAscent(); k=(float)0.4; i=5; i2=8; scrollw=13; tabw=20; ax=i+tabw; ay=i+tabw; gh=(int)(k*(float)sizeh); gw=sizew-(3*i+2*tabw+scrollw); bx=ax+gw; by=ay+gh; scrVx=sizew-(i+scrollw); scrVy=i; scrVw=scrollw; scrVh=gh+2*tabw; scrHx=i; scrHy=by+tabw+i; scrHw=gw+2*tabw; scrHh=scrollw; nhoris=12; dx=(float)gw/(float)(nhoris); nvert=(int)(((float)gh/dx)+0.5); dy=(float)gh/(float)(nvert); vertScroll.setBounds(scrVx,scrVy,scrVw,scrVh); vertScroll.setVisibleAmount(1); vertScroll.setMinimum(0); vertScroll.setMaximum(by-ay); vertScroll.setBlockIncrement((int)dy); vertScroll.setUnitIncrement((int)dy); horiScroll.setBounds(scrHx,scrHy,scrHw,scrHh); horiScroll.setVisibleAmount(1); horiScroll.setMinimum(0); horiScroll.setMaximum(bx-ax); horiScroll.setBlockIncrement((int)dx); horiScroll.setUnitIncrement((int)dx); ix0 = ax; iy0 = scrHy+scrollw; ix1 = scrHx+scrHw; iy1 = sizeh-i; vertScroll.setValue(0); horiScroll.setValue((bx-ax)/2); wheregx=nhoris/2; wheregy=nvert; o_wheregx = wheregx; o_wheregy = wheregy; firstTime=false; } // initialiser doublebuffer if(offscreen == null) offscreen = createImage(getSize().width, getSize().height); Graphics og = offscreen.getGraphics(); og.setClip(0,0,sizew,sizeh); super.paint(og); og.clearRect(0,0,sizew,sizeh); // tegn grid og linjetall og.setColor(Color.darkGray); for(x=0; x<=nhoris; x++) { og.drawLine(grid(x,dx,ax),ay-i2,grid(x,dx,ax),by+i2); itmp = new Integer(x); stmp = itmp.toString(); og.drawString(stmp,grid(x,dx,ax)-og.getFontMetrics().stringWidth(stmp)/2,by+fontasc+i2+2); } for(y=0; y<=nvert; y++) { og.drawLine(ax-i2,grid(y,dy,ay),bx+i2,grid(y,dy,ay)); itmp = new Integer(y); stmp = itmp.toString(); og.drawString(stmp,ax-og.getFontMetrics().stringWidth(stmp)-i2-4, grid2(y,dy,ay)+fonth/2-2); } // tegn "ideell" linje og.setColor(Color.blue); gridLine(og, nhoris, wheregy); // tegn fyllte pixels og.setColor(Color.black); for(x=0; x<wheregx; x++) { tegnknott( og, grid(x,dx,ax), grid2(ddaline(x,nhoris,wheregy),dy,ay), sizew); } og.setColor(Color.blue); tegnknott( og, grid(x,dx,ax), grid2(ddaline(x,nhoris,wheregy),dy,ay), sizew); // skriv ut info om variabler og.setColor(Color.black); y1 = wheregy; y0 = 0; x1 = nhoris; x0 = 0; DY = y1-y0; DX = x1-x0; m = (float)(DY)/(float)(DX); og.drawString("dy = y1 - y0 = " + new Integer(y1-y0).toString(), ax-i2, iy0 + ((iy1-iy0)/4)*1 + fonth/2); og.drawString("dy = x1 - x0 = " + new Integer(x1-x0).toString(), ax-i2, iy0 + ((iy1-iy0)/4)*2 + fonth/2); og.drawString("m = dy / dx = " + new Float(m).toString(), ax-i2, iy0 + ((iy1-iy0)/4)*3 + fonth/2); og.drawString("x = " + new Integer(wheregx).toString(), ix1/2, iy0 + ((iy1-iy0)/3)*1 + fonth/2); og.setColor(Color.blue); og.drawString("y = floor(x * m + 0.5) = " + new Integer((int)(wheregx*m+0.5)).toString(), ix1/2, iy0 + ((iy1-iy0)/3)*2 + fonth/2); // Tegn offscreen bufferet til skjermen g.drawImage(offscreen, 0, 0, null); og.dispose(); } }
Kommentarer til scanline og midp
scanline er oppbygd på akkurat samme måte som dda. Det eneste som er forskjellig er koden for selve algoritmen som demonstreres.
midp's hovedforskjell fra dda og scanline er animeringen. Det foretas ikke noe særlig magisk animering i midp; alt tegnes opp på nytt for hvert frame. Denne rutinen kunne vi ha implementert på en annen, mer effektiv måte.
Tips til hvordan man tilpasser Applets til Aroma
Skjermstørrelse
Når man skal vise ting på skjermen er det viktig å ta hensyn til antall pixels vertikalt og horisontalt som man har tilgjengelig. Bruker man en LayoutManager trenger man ikke tenke noe over det, men skal man gjøre noe som krever litt presisjon på skjermen, duger ikke dette, og man må regne ut koordinater selv.
For å regne ut sine egne koordinater er man som sagt nødt til å vite bredden og høyden på Appleten. Dette kan gjøres med f.eks. getSize().width og getSize().height. For å ikke kalle disse funksjonene hver gang man trenger informasjonen, kan man sette variabler til disse verdiene. Som regel gjøres slik initialisering i init() funksjonen til en Applet.
Ulempen med dette er at man ikke kan være sikker på å få riktig informasjon fra systemet på det tidspunktet i koden. Erfaringer vi har gjort oss viser at man må vente helt til paint() funksjonen for å være sikker på at denne informasjonen er tilgjengelig. Løsningen er da å ha en boolean variabel som sørger for at en del av koden i paint() bare skjer en gang.
HTML-parametere
En av ulempene med å kjøre Applets i Aroma er at man ikke kan bruke HTML-parametere. Det finnes ikke noe direkte løsning for dette i Java (enda).
Kildekoden er skrevet i Microsoft Notepad 4.0 og/eller TextPad 3.2.5.
All kompilering er foretatt med Jikes 0.39 (ca. 6 ganger raskere enn javac).
Uttesting av Applets foretatt med Microsoft Internet Explorer 5.00.0910.1309.
Bearbeiding av bilder foretatt med Adobe Photoshop 5.0, Kinetix 3D Studio MAX 2.5, Microsoft Paint 4.0, Animagic 1.06a.
HTML-kode utarbeidet med Microsoft FrontPage Express 2.0.2.1118.
"Introduction to Computer Graphics", 1995, Foley, van Dam, Fiener, Hughes.
"Core Java Fundamentals", Horstmann, Cornell
"teach yourself Java in 21 days", Laura Lemay, Charles L. Perkins
Hele prosjektet pakket i en zip-fil
Jostein Trondal
Frode K. Kristensen
Høgskolen i Agder, våren 1999