Goodbye HTML. Hello Canvas!
Part 6: The Boxes & Beginning A Real Application
You can read the previous article here.
Introduction
The content I’ve promised for this article became too big. I had to split it. Also, I’ve spent more time than I expected writing the code. I made a LOT of refactorings, searching for the best design/greatest utility.
I had finished the code for a full-page engine, like BobSprite. Then I realized that it would be more useful for us if I could convert the engine into a modular format: a box; which you can use as the only element of a special web age or as a special part of a common web page (like a video tag); actually, you can use many boxes per web page.
Then I resurrected the idea of making everything in pure canvas style… and it worked! Using standalone boxes is easier than submitting to the rules of a monolithic engine (which I’ve discarded).
Right now, I still have to create more widgets and utilities. Anyway, it is good enough to be the base of today’s article.
The Real Application
I chose a mosaic generator, with simple image editions, for our real application because it is useful and uncommon, and its code will not misguide us from the study of the box itself. The real app is NOT finished yet.
The Library
I tried my best to make the use of the library extremely friendly/intuitive/safe/non-verbose. These objectives lead to
- few functions, with clear names
- a clear and simple structure of your application
- no optional function arguments (multiple ways to code cause confusion)
- the box encapsulates everything it cans, (almost) not giving you the chance to cause an internal error
- the library tries to catch every possible error that you could do — it stops running immediately and writes a clear message in the console; the message always starts with “Uncaught” and is followed by 2 traces (“ — ”)
- very few, simple rules that you must follow
Note: the library is somewhat different from its description in previous articles.
The (Partial) Demo
I like single-page apps. But I love single-file apps; especially for simple demos. This format is bad for studying code, I’m addressing this issue at the end of the article.
This demo is a single file app of 170.1 kB — uncompressed; no big deal. Besides the library and the application code, it includes embedded images and fonts.
This is the link for the demo.
The demo was built using traditional HTML elements (<h1>, <div>, <br>) plus two GoodbyeHtml boxes and one GoodbyeHtml label. The purpose is to show that now we can choose between:
- a pure GoodbyeHtml app (just one big box, like BobSprite)
- a traditional HTML page that includes as many GoodbyeHtml boxes as we want
Each box is constructed using only the canvas element. The browser sees just one canvas. But internally, there are more canvases, to store and manipulate images. All of them are drawn onto the main canvas of the box.
Today we will not look at the code of the library. Our focus is on learning how to use the library.
Some readers were worried that it would be hard to write a canvas-based web page. I hope you find that it is very easy, using this library.
Now, let’s study a simplified version of the code of our real (incomplete) application.
The Application Code
// (embedded fonts here)
// (embedded images here)"use strict"var goodbyefunction main() {
//
goodbye = createGoodbyeHtmlLibrary()
loadResources()
}
We start creating the library and assigning it to a global variable, so it is available everywhere in our code.
Then we need to load the resources.
Loading Resources
Our canvas-based web page is like the software of a video game: it needs to load all resources upfront. A computer video game just loads the resources from the computer disk.
As our application loads the resources over the web, we must have a way to check whether the resources are loaded or not, before going on.
The browser loads the resources asynchronously, even the embedded ones.
That’s the reason why we have a library utility called loader.
function loadResources() {
//
const loader = goodbye.createLoader()
//
loader.loadFont("black", fontSourceProSansBlack)
loader.loadFont("white", fontSourceProSansWhite)
//
loader.loadImage("help-black", embededImages["help-black"]) }
loader.loadImage("load-black", embededImages["load-black"]) }
//
loader.ready(resourcesLoaded) // must send a callback
}
function resourcesLoaded() { // the callback
//
// processing the images, if needed
// (more code here)
Note that using the loader requires 3 steps:
- creating the loader
- asking to load ALL resources
- telling the loader that there are no more resources to load and which is the callback (the function in our code that the loader will call after all resources are loaded)
The loaded resources are stored in library variables called allFonts and allImages.
NOTE: the second article of this series teaches how to create fonts.
Other Library Utilities
Currently, besides the createLoader, the GoodbyeHtml library has these utility functions: cloneImage, createCanvas, fadeImage, createCheckerboard, negativeFromImage, createLabel, calcTextLength and createBox.
For example, using fadeImage we can create a disabled button image from a normal button image:
const disabled = goodbye.fadeImage(original, "black", 0.40)
Another example: Sometimes we have white-over-black icons; that when clicked become black-over-white icons — or vice-versa. We don’t need to load two sets of icons when one set is the exact negative from the other. We can use negativeFromImage, which works for font sheets as well.
const whiteIcon = goodbye.negativeFromImage(blackIcon)
The GoodbyeHtml Label
const label = goodbye.createLabel("white", "black", 10,10,5,5, "My label")
The GoodbyeHtml label is an image (more precisely, a canvas), that you treat as any other image. Its parameters are:
- the id (string) of the font to be used
- the background color (string)
- left padding (integer)
- right padding (integer)
- top padding (integer)
- bottom padding (integer)
- text (string)
You don’t directly choose width and height. Width and height are consequences of the chosen font and padding.
The Parameters Of The Functions
There is no optional parameter anywhere: no alternative ways to call a function. This design makes mastering the library easy for you.
Also, I tried to make the structure of the parameters pretty standard inside the library and compatible with common patterns.
All coordinates/dimensions are in the format left, top, width, and height.
There is one exception; one case where the parameters work like you think they would: the padding of the label: they don’t match the HORRIBLE CSS pattern for padding and margins.
The GoodbyeHtml pattern for padding starts to like the UNIVERSAL pattern.
It starts with the left. Then it expects right; thus it is easy to position/center the text horizontally.
Next comes top padding, followed by bottom padding; making it easy to position/center the text vertically.
A Glimpse Of The CSS Madness And Sadism
Why do we go to school? We go to school to learn patterns that will be very useful for the rest of our lives; because everyone else is learning the SAME pattern.
For example, a green sign means go, and a red sign means stop.
Another example is the Cartesian coordinates: X and Y; which often are translated to left and top. This is the universal pattern.
The x-coordinate always comes first, followed by the y-coordinate.
We can define a rectangle telling its upper-left corner and its lower-right corner: left, top, right, bottom (or left, top, width, height).
We see this everywhere, it is not a programming thing only. It is math!
But, enters CSS and says, “Forget everything you learned. The true coordinate system is top, right, bottom, left.”
Above we see a picture of the CSS creator, when he was 2 years old, playing with his most beloved puppet: a happy and naive web developer.
Besides conflicting with the universal pattern, it is nonsense. It is not like horizontal-first-vertical-after, which makes sense, and follows, as it can, the universal pattern.
At the age that I used to torture myself (learning CSS), I couldn’t memorize that STUPID “pattern”. Then, one day, reading a CSS book, I found a trick to help me remember the “pattern”.
Yes! CSS is not a language; it is a stinky and sticky bag of obscured tricks.
The trick is the word TRouBLe. I have to admit that this is the most honest trick that I have seen in my whole life.
Creating A Box
const parent = document.getElementById("first-box")const box = goodbye.createBox(1000, 600, parent)
You need 3 arguments to create a box: width, height, and the HTML container where the box will be placed.
You don’t append the box. The box appends itself. A simpler example:
const box = goodbye.createBox(1000, 600, document.body)box.setBgColor("#084d6e")
The box exposes 4 methods: setBgColor, initLayers, exchangeLayers, and getLayer.
Creating The Layers
// initLayers returns nothingbox.initLayers(["base", "over", "help", "alert"])
Now, all the layers of the box are defined. The layers are automatically appended to the box.
You cannot change or add layers. You can change only two things:
- the visibility of each layer
- the order of the layers
Allowing you to remove and add layers would make your code less readable and less maintainable because you could do it anywhere in your code. Also, those features are not necessary.
box.exchangeLayers(["help", "over", "base", "alert"]) // OKbox.exchangeLayers(["help", "over", "help", "alert"]) // error// message in console:Uncaught -- wrong argument ids for function box.exchangeLayers: duplicated item: help
There is a reason why the name of the function is initLayer, and not createLayer: it doesn’t return a “layers” object. You have to use the id (string) of each layer.
The layer object has 5 methods: createPanel, show, hide, visible, and log.
const a = box.getLayer("alert") // returns a layer objecta.hide()
a.show()a.log()let visible = a.visible()
Note: a layer has no left/top coordinates, width, height, or background color. Actually, it is a virtual layer.
Creating A Panel
const layer = box.getLayer("base")const panel = layer.createPanel(0, 0, 1000, 50, "black")
panel.paintRect(0, 0, 250, 50, "white")panel.setFont("black")panel.write(30, 10, "Mosaic Generator")
The parameters of createPanel are the classic left, top, width, height — and bgColor.
During its creation, a panel is automatically appended to its layer. And it is checked twice:
- if it fits entirely inside the box
- for collision against other panels in the same layer
const layer = box.getLayer("base")const panelA = layer.createPanel(0, 0, 100, 50, "black")const panelB = layer.createPanel(99, 0, 100, 50, "red") // error// message in console:Uncaught -- panel 2 clashes with panel 1 in layer base
The panel object has some methods (more to come): hide, show, setFont, write, clearRect, paintRect, paintImage, setBgColor, createButton, createSurface, and log.
Note: writing and painting on the panel don’t create widgets; thus there are no checks about fitting and clashing.
Creating A Button
const buttonOk = panel.createButton(460, 350, 68, 50, "dimgrey")const img = goodbye.createLabel("white", "black", 20, 20, 10, 10, "OK")buttonOk.setImageNormal(img)buttonOk.setOnClick(function () { box.getLayer("alert").hide() })
Creating a button is almost a copy of creating a panel.
The parameters of createButotn are the classic left, top, width, height — and bgColor. The button position is relative to its parent: the panel (not to the box).
During its creation, a button is automatically appended to its panel. And it is checked twice:
- if it fits entirely inside the panel
- for collision against other widgets in the same panel
const b = panel.createButton(460, "black", 68, 50, "grey") // error// message in console:-- wrong argument top for function panel.createButton: expecting integer >= 0, got string: black
At the current stage, the library doesn’t automatically insert text in the button; we have to proceed manually.
The button object has these methods: hide, show, setImageNormal, setImageActive, setImagePressed, setImageDisabled, disable, activate, normalize, setBgColor, setOnClick, setButtonText, and log.
The Button States
A GoodbyeHtml button has 4 states, and you can set an image for each state:
- normal
- disabled (does nothing)
- active (you must call button.activate) — means it has been clicked and some status has changed; clicking it again makes the button become normal and that status change to the previous value
- pressed — means it received a mouse down event and is waiting for the mouse up event to produce a click event
If the mouse leaves the button while the button is pressed, the pressed state is canceled and the future click event is aborted, in order to avoid accepting a pseudo-click. You should try this in the demo.
For a button, you can listen only to the click event. All other mouse events are handled internally only.
Creating A Surface Widget
Creating a surface is like creating a button. A surface is a raw widget, very customizable. It is purpose is to give you the freedom to do things that you could not do, normally, because of the rigid/safe structure of the box.
We don’t have time to discuss the surface now. Just imagine a special layer containing a single panel, with transparent background, covering the full box. And this panel has only one widget, a transparent background surface, that fully covers the box too. In this case, you can paint anything anywhere… and you can have more special layers like that.
For a surface, you must listen directly to all mouse events, except the click event, which is not available. You already have the mouse down and mouse up events to listen to. Let’s not be redundant.
The Hierarchy
library > boxes > layers > panels > widgets
It is that simple.
- You create the library.
- You use the library utilities, including the createBox.
- From the box, you create layers.
- From each layer you create panels.
- From each panel you create widgets.
It is NOT like creating an HTML object in JavaScript, where you first create it and then will append it to some container. Or forget to do it. Or do it twice (same container or not).
There are no lost widgets in GooodbyeHtml.
I don’t know how you feel it; but, for me, there is no way it could be more simple/robust/easy/intuitive/fast/readable/maintainable/etc.
What’s to come
I have to include more features in the library (including keyboard handling), publish it on GitHub, document it, and finish the mosaic generator.
The application code of the next demo will come apart from the library and the embedded resources, suitable for learning experiences.
This is the link to the next article of the series.
More content at PlainEnglish.io. Sign up for our free weekly newsletter. Follow us on Twitter and LinkedIn. Join our community Discord.