Libraries
How does it all work? In this chapter we take a look behind the scenes. If you look at the tryItFrame.html file, you will notice the following imports in the head section:
<head> ... <!-- order is importent! --> <script src="./libraries/p5.min.js"></script> <!-- <script src="./libraries/p5.sound.min.js"></script> --> <script src="./libraries/gui.js"></script> <script src="./libraries/graphics.js"></script> <script src="./libraries/console.js"></script> <script src="./libraries/utils.js"></script> </head>
The first library is the standard p5.js library from Processing. The other four are what we will explain on the pages below.
.
Console Programs
A console program is an HTML text area widget that is used to display text. Console programs are very simple, and you can use the following commands
- print()
- println()
- readLine()
- readInt()
- readDouble()
- clear()
Using a text area widget for a console seems straight forward, but there are a few things that makes this tricky:
- the functions print() and clear() are already defined in p5.js
- there is no read() function, the only thing that allows user input is the prompt() function
Example
A simple example of our console program looks like this:
async function setup() { createConsole(); println('This program adds two numbers.'); let n1 = await readInt('Enter number one: '); let n2 = await readInt('Enter number two: '); let sum = n1 + n2; println( 'The sum is: ' + sum ); }
You may notice the somewhat unusual async and await keywords. More on this in a moment.
print() and clear()
Since both print() and clear() already exist, we need to redefine them. This is done in the createConsole() function:
function createConsole(_ROWS, _COLS) { // redefine print(): print = function (msg) { if (msg === undefined) { msg = ''; } // textarea.append(msg); textarea.value += msg; } // redefine clear(): clear = function () { textarea.value = ''; } }
readLine()
The hard part is reading from console. JavaScript only has the prompt to do that. And JavaScript does not have a pause(). One elegant way to get the desired functionality is via promises.
function readLine(msg) { if (msg === undefined) { msg = ''; } textarea.value += msg; enterPressed = false; textEntered = ''; return new Promise((resolveOuter) => { resolveOuter( new Promise((resolveInner) => { var check = function () { if (enterPressed) { // print('Enter'); textarea.value += '\n'; resolveInner(textEntered); } else { // print('No Enter'); setTimeout(check, 100); // check again in a second } } check(); }) ); }); }
This is the reason why we have to use async and await when using any of the read function.
.
Graphics Programs
A graphics program uses the HTML canvas tag for drawing and animation. However, we do not use it directly, but instead use p5.js for most of the drawing. Our graphics library is basically a wrapper around p5.js. In our graphics programs you can use the following commands:
- add(obj, x, y)
- getElementAt(x, y)
- getElementsAt(x, y)
- removeAll()
- removeObj(obj)
- setBackground(color)
- update()
p5.js
p5.js comes from the Processing Foundation, that also created Processing for Java. The idea is very similar, make JavaScript as easy to use as possible. We use p5.js, because it is well developed and maintained, and it runs on most browsers, even the mobile ones. Also, a key feature is its support and abstraction for various hardware, such as webcam and microphone, but also playing videos and 3D drawing are made easy with p5.js.
Basically all p5.js programs have the same structure:
function setup() { createCanvas(400, 400); frameRate(25); //noLoop(); } function draw() { background(220); ellipse(0, 0, 50, 50); }
The setup() method is called once at the start of the program. In addition there is a draw() method that is called repeatedly, depending on what frame rate was chosen. It is also possible to not call the draw() method at all via the noLoop() command. The ellipse() function is one of p5.js many drawing functions.
If you save the above code in a file named sketch.js, then the following HTML is all that is needed to run it:
<html> <head> <script src="https://cdn.jsdelivr.net/npm/p5@1.6.0/lib/p5.js"></script> <script src="sketch.js"></script> </head> <body> </body> </html>
Naturally, all the magic lies in the p5.js file.
Graphics Objects
Writing code in p5.js is not all that object oriented, or rather, there is a strong tendency to not write object oriented code when working with p5.js. For small projects that is perfectly allright. But as soon as things get a little more complicated and objects start interacting, like collisions, or a user clicking on them, things get rather tedious.
This is why we wrote wrapper graphics objects around the p5.js drawing functions, strongly inspired by the ACM graphics library for Java:
- GArc
- GCompound
- GImage
- GLabel
- GLine
- GObject
- GOval
- GPixel
- GPolygon
- GRect
- GVideo
- GVideoPreview
- GWebCam
- GWebCamPreview
Most graphics objects are quite simple, GImage and GCompound maybe a little more involved.
Example
Just to get an idea how the combination of p5.js and graphics objects work, consider the following example:
function setup() { createCanvas(300, 150); frameRate(5); let fritz = new GRect(150, 100, 50, 50); fritz.setColor(color('blue')); fritz.setFilled(true); fritz.setFillColor(color('yellow')); add(fritz); } function draw() { update(); }
In the setup we create the canvas, set the frame rate, and then create a GRect object. In the draw() function we simply call the update() function. The update() function is very simple:
function update() { background(backgroundColor); for (let i = 0; i < gobjects.length; i++) { gobjects[i].draw(); } }
It iterates through all the gobjects and calls their respective draw() functions. The add() method above, adds graphics objects to the gobjects array.
function add(obj, x, y) { if ((x !== undefined) && (y !== undefined)) { obj.setLocation(x, y); } gobjects.push(obj); }
GObject
The class GObject is the parent class of all other graphics objects:
class GObject { constructor(x, y, width, height) { this.x = x; this.y = y; this.width = width; this.height = height; this.color = 'black'; this.filled = false; this.fillColor = 'white'; ... } contains(x, y) { ... } ... }
Meaning every graphics object has a location and a size, a color, a fillColor and is either filled or not. GObject should be considered an abstract class, but JavaScript does not have that concept.
As a concrete implementation of GObject we will only consider GRect, all other graphics classes are very similar:
class GRect extends GObject { constructor(x, y, width, height) { if ((width === undefined) && (height === undefined)) { // new GRect(50,50) has width and height = 50, but x and y = 0! super(0, 0, x, y); } else { super(x, y, width, height); } } draw() { noFill(); stroke(this.color); rect(this.x, this.y, this.width, this.height); } }
Here the functions noFill(), stroke(), and rect() are standard p5.js functions.
GImage
Of the graphics classes the GImage class is a little more complicated, because loading of images in p5.js happens asynchronously. p5.js has the loadImage(path, [successCallback], [failureCallback]) function, which allows for a callback in case of success or failure. Per se, this is not a problem. However, if we want to access the pixels of the image via the getPixelArray() method, there could be a problem: assume it takes a little longer for the image to load, then the call to the getPixelArray() method might happen before the image is loaded.
To solve this, we need two steps: First, we introduce a property imageLoaded, which initially is set to false, and only after the image has been successfully loaded it will be true. But here comes another subtle problem: the this of the callback is different from the this of our GImage. The standard solution is to get a value on the GImage's this and call it that:
... this.pixelArray1 = []; this.imageLoaded = false; let that = this; this.image = loadImage(imageName, img => { image(that.image, that.x, that.y); that.image.loadPixels(); // remember pixels for modification for (let i = 0; i < that.image.pixels.length; i++) { that.pixelArray1[i] = that.image.pixels[i]; } that.image.updatePixels(); that.imageLoaded = true; }); ...
Second, before allowing usage of the getPixelArray() method, we need to make sure, that the image has loaded, i.e. we have to wait until imageLoaded is true. We can do that using a promise:
getPixelArray() {
return new Promise((resolveOuter) => {
let that = this;
resolveOuter(
new Promise((resolveInner) => {
var check = function () {
if (that.imageLoaded) {
resolveInner(that.pixelArray1);
} else {
setTimeout(check, 100); // check again in a second
}
}
check();
})
);
});
}
Subtle, also here we have to remember the this using that. As I said, it is a little tricky.
.
UI Programs
For writing UI programs, we use standard HTML widgets with thin JavaScript wrappers around them. A design challenge was: could you make the resulting code look almost like a Java Swing program? Turns out that very little has to be changed, for the standard HTML widgets to behave like their corresponding Swing counterparts.
In UI programs you can use the following commands:
- createGUI(width, height)
- addWidget(obj, where)
- removeWidget(obj, where)
- setLayout(_layout, _cols)
- addActionListener(obj)
- addChangeListener(obj)
Example
To see how a simple UI program is implemented with our UI library, consider the following example:
let tf; function setup() { createGUI(300, 150); setLayout('border'); let lbl = new JSLabel("Name: "); addWidget(lbl, 'SOUTH'); tf = new JSTextField(10); addWidget(tf, 'SOUTH'); let btn = new JSButton("Login"); addWidget(btn, 'SOUTH'); } function actionPerformed(ev) { print("Name: " + tf.getText()); }
If you are familiar with Swing, you will recognize the respective classes easily.
UI Widgets
Just for demonstration purposes we implemented a handful of the UI widgets:
- JSObject
- JSLabel
- JSTextField
- JSTextArea
- JSAbstractButton
- JSButton
- JSCheckBox
- JSRadioButton
- JSComboBox
- JSLink
- JSFileUpload
- JSOptionPane
- JSPanel
- JSCanvas
If desired you can add as many as you like. Most are really easy to implement, but somewhat trickier were the JSPanel and the JSCanvas.
JSObject
The class JSObject is the parent class of all other UI widgets:
class JSObject { constructor() { this.element = document.createElement('span'); } setStyle(css) { this.element.style = css; } addStyle(css) { this.element.style.cssText += css; } getWidth() { return this.element.width; } getHeight() { return this.element.height; } }
Every widget is based on an underlying HTML element, in case of the JSObject it is the <span> tag. Look and feel is changed via cascading style sheets. Being able to use CSS for styling is very convenient. Really, there is not much more to this.
As a second example, let's look at the JLabel:
class JSLabel extends JSObject { constructor(text) { super(); this.element = document.createElement('span'); this.element.style.padding = DEFAULT_PADDING; this.element.style.margin = DEFAULT_MARGIN; this.element.innerHTML = text; } getText() { return this.element.innerHTML; } setText(text) { this.element.innerHTML = text; } }
I think you get the idea. A side note: if you have read Douglas Crockford's book, then you know that using innerHTML is not such a good idea. If you have not read Douglas Crockford's book, you should.
Event Handling
Again, we don't really have to do much. The standard HTML events will do just fine. Whenever we add a widget, we add an EventListener for it:
function addActionListener(obj) { if (obj instanceof JSAbstractButton) { obj.element.addEventListener('click', function (ev) { let ae = new ActionEvent(ev.target.id, obj); window.actionPerformed(ae); } ); } else if (obj instanceof JSCanvas) { obj.element.addEventListener('click', function (ev) { const rect = obj.element.getBoundingClientRect(); window.canvasClicked(ev.clientX - rect.left, ev.clientY - rect.top); } ); } else if (obj instanceof JSTextField) { obj.element.addEventListener("keyup", function (ev) { if (ev.key === "Enter") { let ae = new ActionEvent(ev.target.id, obj); window.actionPerformed(ae); } } ); } }
Adding an EventListener for every widget seems a little overkill, but we do not expect very large programs to be written with this library.
Just in case you are curious, the ActionEvent is a really thin wrapper around two instance variables, command and source:
class ActionEvent { constructor(id, obj) { this.command = '' + id; this.source = obj; } getActionCommand() { return this.command; } getSource() { return this.source; } }
JSPanel
Now the one class that is a really tricky is the JSPanel class. From an HTML point of view it is just a <div> tag. What makes it complicated are two things: first, it can have child widgets, including JSPanels. And second, it can have three different layouts: flow, grid and border.
Layout
Let's talk about the layouts first: flow and grid are simple, all we do we set the style of the <div> tag. In the case for flow it is one line,
this.element.style.display = 'inline-block';
in the case for grid it is four lines:
this.element.style.display = 'inline-block'; this.element.setAttribute('class', 'wrapper'); this.element.style.display = 'grid'; this.element.style.gridTemplateColumns = 'repeat(' + _cols + ', ' + Math.trunc(100 / _cols) + '%)';
If you know your CSS the above should make some sense.
The tricky one is the border layout. For this we use the flex layout. This can be horizontal or vertical. So we are not doing a real border layout, but more of a box layout. But if you combine a horizontal box layout and nest inside it a vertical box layout, you do have a border layout.
So the first thing we do, we dynamically add a style to an existing HTML page, by inserting it into the <head> tag:
let stle = document.createElement("style"); stle.innerHTML = ".flexbox-parent { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: flex-start; align-items: stretch; align-content: stretch;}"; stle.innerHTML += ".flexbox-item-grow { flex: 1;}"; stle.innerHTML += ".fill-area-content { overflow: auto;}"; document.getElementsByTagName("head")[0].appendChild(stle);
Now, this is totally not needed, and I am sure will cause real problems if you had several of them, but I just thought this is so cool...
Anyways, once we have the style, we use it:
this.element.style.display = ''; this.element.setAttribute('class', 'flexbox-parent'); if (_cols == 'horizontal') { this.element.style.flexDirection = 'row'; } this.north = new JSPanel('flow'); this.north.element.setAttribute('class', 'flexbox-item header'); this.north.element.setAttribute('align', 'center'); this.element.appendChild(this.north.element); this.center = new JSPanel('flow'); this.center.element.setAttribute('class', 'fill-area-content flexbox-item-grow'); this.center.element.setAttribute('align', 'center'); this.element.appendChild(this.center.element); this.south = new JSPanel('flow'); this.south.element.setAttribute('class', 'flexbox-item footer'); this.south.element.setAttribute('align', 'center'); this.element.appendChild(this.south.element);
Again, if you know your CSS, this is pretty straight forward. But you also see, why this is a little complicated, we introduce three new JSPanels, one for each region, north, center, and south.
Child Widgets
Adding the children is trivial for flow, and grid: you simply call this.element.appendChild(). For the border layout, we need to make sure we add it to the correct child:
add(obj, where) { ... if (this.layout == 'border') { if (where.toLowerCase() == 'north' || where.toLowerCase() == 'west') { this.north.add(obj); } else if (where.toLowerCase() == 'south' || where.toLowerCase() == 'east') { this.south.add(obj); } else { this.center.add(obj); } } else if (this.layout == 'grid') { this.element.appendChild(obj.element); } else { this.element.appendChild(obj.element); } }
What we did not show, is code that adds the event listeners and treats the case where we add a JSPanel to a JSPanel, but you can look it up in the code.
JSCanvas
The other class I would not want to mess around with much, because I am happy that it kind of works, is the JSCanvas class. It allows us to draw graphics inside a UI application. For this it uses the canvas tag from p5.js:
class JSCanvas extends JSObject { constructor() { super(); // get the p5js canvas: this.element = document.getElementById("defaultCanvas0"); this.element.style.padding = DEFAULT_PADDING; this.element.style.margin = DEFAULT_MARGIN; this.element.style = 'border: 1px solid green;'; this.gobjects = []; } ... }
As you see it gets a handle on p5.js's canvas, which happens to be called "defaultCanvas0". This is not really documented, and as soon as p5.js would change the name, this wouldn't work anymore. You also see the array of gobjects, and most of the methods are one-to-one equivalents to the graphics program functions. One things that is different, however, is the updateJSCanvas() function, take a look if you are interested.
.
Util Library
What is the point of the Util library? Well, as I stated before, I wanted to transfer my existing Java code to JavaScript with as little changes as possible. For the standard Java classes, I was first considering to use one of a transpilers like J2CL [1], JSweet [2], or the old GWT [3]. But the JavaScript code these transpilers generate is ugly as hell, and humongous in size. Since I needed just a handful of classes, I decided to write them myself, as there are:
- RandomGenerator
- StringTokenizer
- ArrayList
- HashMap
- HashSet
- FileReader
- FileWriter
- StorageReader
- StorageWriter
- URLReader
The last three do not exist in Java, but lend themselves to the use in a browser. Again, here ES6 came in real handy, and what other people might call syntactic sugar, I call a life safer.
The classes are really trivial. The RandomGenerator is a wrapper around Math.random(), the StringTokenizer a wrapper around JavaScripts string functions, and ArrayList, HashMap, and HashSet wrap their corresponding JavaScripts classes Array, Map and Set.
Similarily, FileReader is a wrapper around an XMLHttpRequest, and FileWriter is an <a> tag, stupid as it may sound, but works. One word on the FileReader: in ES6, there exists a class with the name FileReader in the new file API, hence we had to call ours Utils.FileReader, which on the other hand allowed us to introduce the concept of what we call namespace. Actually, all the different libraries should be made namespaces, but that would reduce the simplicity of use of our libraries by quite a bit.
include()
I wanted to call it import, but that name was taken. The idea was to import JavaScript classes as you can in Java using the import statements at the top of a Java file. Usually to import another JavaScript file you have to do this in the HTML header:
<html>
<head>
<script src="./libraries/utils.js"></script>
<script src="./ticTacToeLogic.js"></script>
</head>
...
But I did not want to have to modify the HTML file everytime I wanted to just import a little class. As usual the internet has the answers to all your questions:
/* https://www.educative.io/answers/how-to-dynamically-load-a-js-file-in-javascript */ function include(file, async = false) { let script = document.createElement("script"); script.setAttribute("src", file); script.setAttribute("type", "text/javascript"); script.setAttribute("async", async); document.body.appendChild(script); // success event script.addEventListener("load", () => { //console.log("File loaded") }); // error event script.addEventListener("error", (ev) => { console.log("Error loading file", ev); }); }
Now all you have to do, add this line at the top of you code,
include("Pr4_Agrar/ticTacToeLogic.js");
and you can use your own handwritten classes.
.
[1] J2CL, https://github.com/google/j2cl
[2] JSweet: http://www.jsweet.org/
[3] GWT: https://padlet.com/lofidewanto/gwtintro
[4] FileAPI, https://w3c.github.io/FileAPI/
.
Karel Programs
Last, but not least we come to Karel. It is how we start to learn programming, and hence you would think it is the easiest to implement, but quite the opposite.
The core problem is that you do not want the code to run through at once, but instead, you want one line to be executed, then wait a second, then go to the next line, wait a second, and so on. JavaScript does not have a pause() method. And while doing so, you want to see the actual state and position Karel is in. JavaScript is not multithreaded. And in addition you have if-conditions and for- and while-loops in the Karel code, which makes simulating it a difficult task. Here JavaScript does have a very attractive feature: the eval() function. So, how do you circumvent the problems, and use the features?
From our point of view Karel programs are simply graphics programs. Karel is represented by four GImages, one for each direction he can move to. The p5.js draw() function looks like this:
function draw() { eval(codeLines[codePointer]); codePointer++; if (codePointer >= codeLines.length) { noLoop(); } update(); }
We basically set the framerate to one frame per second, and every second we execute one line of Karel code, which has been parsed into an array. This actually solves our first and second problem.
For each of the Karel commands, like move() or putBeeper(), we have a function. For instance, for move it looks like this:
function move() { if (karelIsAlive) { if (frontIsClear()) { switch (karelDir) { case 0: // this.karel.x += SIZE; karel.move(SIZE, 0); break; ... }
That is what allows us to simply use JavaScript's eval() function.
The one problem that remains, are infinite loops. Hypothetically, one could just ignore the problem: it does not happen often, and when it does, you just restart the browser. But from a didactic point of view, and that is what Karel is all about, this is not very helpful.
However, one can tackle the problem. If we realize, that it is basically only the while-loops that can be problematic, we only need to look at those. One could try a static code analysis of while loops. To me, however, this looks quite a bit like the famous halting problem (I said Karel is tricky). Instead, we insert a little piece of code after every while we encounter. The following two lines of regular expression magic do that:
const regex = /(while\s*)\(([ a-zA-Z\(\)]+)\)\s*\{/g; code = code.replaceAll(regex, " $1 ( $2 ) { catchInfiniteLoops();");
The catchInfiniteLoops() function is a simple counter:
let infiniteLoopCounter = 0; function catchInfiniteLoops() { infiniteLoopCounter++; if (infiniteLoopCounter > 200) { throw new InfiniteLoopError("Most likely infinite loop!"); } }
We simplify our halting problem: if this method has been called more than 200 times, we say we have an infinite loop and throw an error. Thus executing the code in a simulation run with eval(code), will always stop, even if there is an infinite loop. And we can tell our users that their code most likely has an infinite loop.
There are a few more code gems in Karel (or hacks as some might call them), but you have to discover them for yourself!
.