Learning Web 01: JavaScript Canvas
I haven’t done web development in 15 years at least. I’ve made my little websites and even made my own themes, but I haven’t really done anything significant in this field in a long time. For a number of reasons it feels like something I need to get back into, so this is the start of my progress on that end.
What I already know
I’ve done HTML5 so I know semantic HTML, or at least that it exists and is preferable to the older ways. I did JavaScript a long time ago and made some funky stuff back in the day that had PHP generating Javascript that generated HTML. That’s all completely different now though. Also did some JavaScript when I helped out an overloaded testing team some years back. I’ve looked at TypeScript a bit when they threatened to switch to that from C++ for slot machine games.
I did some SQL stuff back in the day also. I’m not really going to mess with that right now though. SQL also is not necessarily the right database system to use, with NoSQL options being often prefered. Again though, don’t really know much there and it’s not my target yet.
I’ve used bootstrap before and know about tailwind. I don’t really know css that well but I do well enough to tweak things and get them into a shape I’m mostly happy with. I’m not necessarily great at making things look good.
The project
I’m one that kinda needs a project to work on to learn stuff. I learn by doing. By grinding my face on the pavement until I can pick it up a bit.
I want a system that integrates ticketing, diagraming, and code all within git. I want to be able to draw my UML diagrams and have them refer to actual things like classes and requirements. I want to be able to query the repository for information and generate quality control reports from it.
Very first thing I need from that is the ability to make requirements diagrams. The requirement should be an md file or something on disk that is committed to git. Containment links can be subdirectories?
Should be able to see a visual display of the diagram and/or generate something printable. Also should be able to get a tabular view of requirements. In any view I should be able to add new requirements, link requirements, and edit or delete them.
What do I need to know?
I need to be able to draw shapes on a canvas at calculated locations and then lines between those shapes.
I need to be able to click on these shapes and trigger some sort of modal dialog to edit them.
Also need to be able to trigger this same dialog directly on the canvas during a creation process.
Need to be able to cause persistent change. Since I’ll be in a browser this isn’t necessarily directly possible like a normal application. I probably need a backend, which I’m better at anyway.
Need to be able to test the program. This is just a learning project, so I don’n necessarily need to test it but since I’m trying to learn web development that’s something I need to learn so yeah, I do need to test it. Usually when you think you don’t need to test you’re very wrong. I actually don’t know what the good web testing frameworks are anymore. Used to use stuff like Cucumber if didn’t just do it by hand.
The plan
- Draw boxes in a canvas with some text in them.
- Programatically draw the lines between boxes - just do straight for now. Include termination icons like “contains”.
- Edit dialog.
- backend/persist
- testing
Usually you want to do your tests first but I don’t know enough to do that yet. When in a state of total ignorance it can be better to try stuff out a bit to get a feel for how you’ll write the test. I’ll still be able to do a lot of test design before hand–at least to the point of knowing what I’ll test, why, and some description of the steps– but implementation of the tests will have to come after I know what I’m doing enough to code them.
Initial setup
I can do my initial testing and learning right here in my blog. I just have to tell hugo to let me embed html in my markdown and I should be able to do all the things. Here’s a canvas:
Setup instructions are here. I went with the configuration setting that just lets me be unsafe.
Code for the above came from here and was slightly modified to be:
<canvas id="derp0" width=200 height=100 style="border: 1px solid #ffffff;"></canvas>
<script>
canvas = document.getElementById("derp0");
ctx = canvas.getContext("2d");
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 100, 75);
</script>
Note that I removed const because I’m going to re-use these variables for other things later. Normally you wouldn’t
do that.
Achievement
At this point I just wanted to get the canvas up and draw something in it to show I could do it. It was especially useful to learn I could do it here (as expected) after some minor settings adjustments.
Rectangle
First iteration of drawing a rectangle. Just display it in the exact center of the canvas. Rounded rect with “derp” text inside.
<canvas id="derp1" height=200 style="border: 1px solid #ffffff; width: 100%;"></canvas>
<script>
canvas = document.getElementById("derp1");
ctx = canvas.getContext("2d");
x = canvas.width / 2 - 50;
y = canvas.height / 2 - 50;
ctx.font = "12px Arial";
ctx.fillStyle = "red";
ctx.textAlign = "center";
ctx.textBaseline = "middle"; // valign
ctx.fillText("derp", canvas.width / 2, canvas.height / 2);
ctx.strokeStyle = "blue";
ctx.roundRect(x,y,100,100, 10);
ctx.stroke();
</script>
In this case I mostly looked at the canvas API reference and searched for key things like:
- “javascript canvas text color”
- “javascript canvas rounded rectangle”
- “javascript canvas multiline text” <- needs a library or do by hand.
Note that setting the canvas width to 100% had to happen in CSS. The value is assumed still to be px if you use the attribute.
achievement
Really what I did here was just familiarize myself with rounded rectangles and text. Barely above the basic but I had to seek out different sites for roundRect. In the end I’m not sure why I thought I wanted rounded rectangles. SysML doesn’t use them for requirements. I think I was thinking of Acorn.
Size the rectangle based on text content
Will have something like a “requirement” that has a name, an id, and some text. Text can be long so need to come up with some wrap methods. For now though I’m happy with having the box size based on the text content. The user can use ‘\n’ to indicate line change and I’ll calculate the line count from there.
<canvas id="derp2" height=200 style="border: 1px solid #ffffff; width: 100%;"></canvas>
<script>
canvas = document.getElementById("derp2");
ctx = canvas.getContext("2d");
function textSize(ctx, text) {
let metrics = ctx.measureText(text);
return [metrics.width, metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent];
}
function calculate_req_area(ctx, req) {
ctx.font = "10px Arial";
// https://stackoverflow.com/questions/1134586/how-can-you-find-the-height-of-text-on-an-html-canvas
let [w, h] = textSize(ctx, "<<requirements>>");
ctx.font = "18px Arial";
let m = textSize(ctx, req["name"]);
h += m[1];
if (m[0] > w) w = m[0];
h += 5;
lines = req["text"].split("\n");
ctx.font = "12px Arial";
for (let i = 0; i < lines.length; ++i)
{
m = textSize(ctx, lines[i]);
h += m[1];
if (m[0] > w) w = m[0];
}
return [w,h];
}
function draw_req_text(ctx, req, x, y) {
let area = calculate_req_area(ctx, req);
let tx = x - area[0] / 2;
let ty = y - area[1] / 2;
ctx.fillStyle = "white";
ctx.textAlign = "center";
tx = x;
ctx.font = "10px Arial";
let m = ctx.measureText("<<requirement>>");
ty += m.actualBoundingBoxAscent;
ctx.fillText("<<requirement>>", tx, ty);
ty += m.actualBoundingBoxDescent;
ctx.font = "18px Arial";
m = ctx.measureText(req["name"]);
ty += m.actualBoundingBoxAscent;
ctx.fillText(req["name"], tx, ty);
ty += m.actualBoundingBoxDescent;
tx = x - area[0] / 2;
ctx.textAlign = "left";
ctx.font = "12px Arial";
ty += 5;
ctx.beginPath();
ctx.moveTo(tx-2, ty - 3);
ctx.lineTo(tx + area[0] + 2, ty - 3);
ctx.stroke();
let lines = req["text"].split("\n");
for (let i = 0; i < lines.length; ++i)
{
m = ctx.measureText(lines[i]);
ty += m.actualBoundingBoxAscent;
ctx.fillText(lines[i], tx, ty);
ty += m.actualBoundingBoxDescent;
}
}
requirement = { "name": "derp", "text": "hello,\nthis is a\nrequirement." };
// let's use 12 for normal and 18 for the title. 10 for '<<requirement>>'
let text_area = calculate_req_area(ctx, requirement);
let rect_area = [text_area[0] + 4, text_area[1] + 4];
let loc = [canvas.width / 2, canvas.height / 2];
ctx.strokeStyle = "blue";
ctx.rect(loc[0] - rect_area[0] / 2, loc[1] - rect_area[1] / 2, rect_area[0], rect_area[1]);
ctx.stroke();
draw_req_text(ctx, requirement, loc[0], loc[1]);
</script>
achievement
I was able to use the font system to calculate exact placement and sizes. This went way better than most frameworks I’ve worked with that make this an estimation game at best. It wasn’t super easy to figure out and isn’t intuitive, but it works very well. I need to add some padding probably but it looks good. I can definitely design something around this.
Click the rectangle
Need to be able to click on the requirements in the diagram. Here I make it so I can click or double click inside the square to cause it to color inside or outside the square respectively.
<canvas id="derp3" height=200 width=500 style="border: 1px solid #ffffff;"></canvas>
<script>
canvas = document.getElementById("derp3");
ctx = canvas.getContext("2d");
ctx.strokeStyle = "white";
ctx.fillStyle = "blue";
rect = [10, 10, 100, 20];
function fill_in()
{
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(rect[0], rect[1], rect[2], rect[3]);
}
function fill_out()
{
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.clearRect(rect[0], rect[1], rect[2], rect[3]);
}
function click_handler(event) {
if (event.offsetX < rect[0]) return;
if (event.offsetX > (rect[0] + rect[2])) return;
if (event.offsetY < rect[1]) return;
if (event.offsetY > rect[1] + rect[3]) return;
fill_in();
ctx.fillText(event.offsetX + " > " + rect[0] + " + " + rect[2] + " = " + (rect[0] + rect[2]), 100, 50);
}
function double_handler(event) {
if (event.offsetX < rect[0]) return;
if (event.offsetX > (rect[0] + rect[2])) return;
if (event.offsetY < rect[1]) return;
if (event.offsetY > rect[1] + rect[3]) return;
fill_out();
}
canvas.addEventListener("click", click_handler, false);
canvas.addEventListener("dblclick", double_handler, false);
ctx.strokeRect(rect[0], rect[1], rect[2], rect[3]);
</script>
i
achievement
The biggest achievements here were to learn how to tie into the event system, which is pretty straight forward, and realizing I was scaling the canvas instead of sizing it with CSS. This became really obvious when the mouse location values didn’t match the size of the box at all. At first I thought this was because I’m using an HiDPI laptop screen and have scaling on the desktop settings. Nope. When you size from CSS it is like sizing an image and you are not adding/removing actual pixels from the canvas.
modial dialog
Need to be able to open an edit dialog for the requirement. If you click in the square here a dialog pops up.
<dialog id="dialog0">
<p>DERP!!!!</p>
<button commandfor="dialog0" command="close">Close</button>
</dialog>
<canvas id="derp4" height=200 width=500 style="border: 1px solid #ffffff;"></canvas>
<script>
canvas = document.getElementById("derp4");
ctx0 = canvas.getContext("2d");
rect0 = [50, 50, 100, 50];
function show_dialog() {
dialog = document.querySelector("#dialog0");
dialog.showModal();
}
function click_handler2(event) {
if (event.offsetX < rect0[0]) return;
if (event.offsetX > (rect0[0] + rect0[2])) return;
if (event.offsetY < rect0[1]) return;
if (event.offsetY > rect0[1] + rect0[3]) return;
show_dialog();
}
ctx0.fillStyle = "red";
ctx0.strokeStyle = "blue";
ctx0.strokeRect(rect0[0], rect0[1], rect0[2], rect0[3]);
canvas.addEventListener("click", click_handler2, false);
</script>
achievement
Super basic dialog creation that’s really easy. It’s not pretty because my theme doesn’t know about dialogs but I can address that when it comes to building the real thing.
Lessons learned and notes
The measureText function used to only return width but now returns various bounding box metrics as well
that can be used to calculate height and exact text placement.
I need to learn JavaScript scoping rules and how to declare variables correctly. Just not saying what the scope is
by not using var or let or const or whatever else there might be isn’t at all correct but it works well
enough here. Messing with it without understanding it doesn’t.
Actually, not using var or let caused me problems in the last section because scoping caused variables
like ctx and rect to reseat with relation to functions defined in earlier sections.
The css width property actually alters the scaling of the canvas!! There doesn’t appear to be a good way
to set the width to 100% of available pixels. Another example of AI sending me on a wrong path – not only that
but all the “help” sites say the same thing but no, in fact it is going to scale if you use the css property.
There’s alternative “pointer” event handling I may want to be using instead of mouse click/dblclick for tablets.
