DEV Community

Cover image for Rewriting an old project! Part 2: JavaScript
Ken Bellows
Ken Bellows

Posted on

Rewriting an old project! Part 2: JavaScript

Note
This is Part 2 of a miniseries in which I'm revisiting an old weekend hack of mine from college, which I called jSphere (because I was lazy and couldn't come up with anything more interesting).

If you haven't read Part 1, which was all about the HTML and CSS, I recommend giving it a skim, or at least reading the Backstory section at the beginning.

As I said last time, I'm not going to discuss what the code does in this post, just its structure and syntax. But I do plan to write a proper walkthrough and tutorial later.

Last time...

In Part 1, time we went over the markup and styling of , and I already found that pretty interesting. But, I mean, this project is primarily a JavaScript project. It's all about that <canvas>!

For reference, here's what we're rebuilding:

Okay. Where to begin? Man, there was so much to change about this code, and a lot of it was interwoven, which makes it tough to really talk about step-by-step. So what I'll have to do is just... go for it, I guess.

Please, feel free to critique or otherwise comment on the code here! I love feedback and (constructive) criticism, I'm always interested in how other people would approach the same tasks, and I've even been known to enjoy a little bikeshedding now and then.

Minutia: nah.

Let me mention up front here that there are some changes I won't explicitly discuss, because they're too minor. Yes, I changed all the vars to consts and lets, I reordered some functions, I changed all the TitleCasedMethods to normalCamelCase (why did I capitalize the first letter of all my methods? was I insane?). But you don't care about all those things. I'm going to focus on the really large things, the things I found interesting or funny, and the things that really demonstrate how far the language has come in seven years.

Let's start with a funny one.

Bro, do you even loop?

I fixed a spot where I had done something super weird, maybe because I was rushing and didn't think it through. This is in Sphere's constructor, when I'm generating all the Dot instances needed to represent the points on the surface of the sphere. I use a nested for loop to move around the sphere in rings, top to bottom, adding dots to its surface as I went. This is fine, but I wrote the loops in a super weird way:

this.points = new Array();
var angstep = Math.PI/10;
var i = 0;
// Loop from 0 to 2*pi, creating one row of points at each step
for (var angxy=0; angxy<2*Math.PI; angxy+=angstep){
    this.points[i] = new Array();
    var j=0;
    for (var angyz=0; angyz<2*Math.PI; angyz+=angstep) {
        // Loop from 0 to 2*pi, creating one point at each step
        var px  = r * Math.cos(angxy) * Math.sin(angyz) + x,
            py  = r * Math.sin(angxy) * Math.sin(angyz) + y,
            pz  = r * Math.cos(angyz) + z,
            pfg = pz > z;
        this.points[i][j] = new Dot(px,py,pz,pfg);
        j++;
    }
    i++;
}

What's up with the i and j variables there, hanging out outside of the loops? Why aren't they created and incremented within the loops? I definitely knew that a for loop can have multiple variables; why didn't I just do this?

for (var i=0, angxy=0; angxy<2*Math.PI; i++, angxy+=angstep) { ... }

But also, why do I even need those index counters in the first place? Why do I use this.points[i] = and this.points[i][j] = to add to the arrays? Why aren't I just using [].push() to add stuff to the array? And why on Earth did I use new Array() instead of []? How much OOP was I on???

So I got rid of all that nonsense. I also decided to make this.points a single flat list of points, rather than a 2D matrix with rows, because I realized while refactoring some other code that the only way I ever referenced this.points was with a couple of nested loops to reach each point, usually with weird for ... in loops like this:

for (var i in this.points) {
    for (var j in this.points[i]) {
        this.points[i][j]...
    }
}

First, it does work, but using for...in loops on arrays like this is strange, and it's unexpected to say the least. I was very confused when I first saw it, and I imagine any other devs reading it would be thrown off as well.

Second, I never used i or j for anything other than indexing into this.points, which makes this a great candidate for the much more elegant for...of loop, which didn't exist when I originally wrote this code.

I refactored to fix all of these problems, and here's where I wound up. The constructor loops were updated to this (which also includes a change to the Dot constructor that I'll talk about in a bit):

// The angle delta to use when calculating the surface point positions;
// a larger angstep means fewer points on the surface
const angstep = Math.PI/10;
this.points = [];
// Loop from 0 to 2*pi, creating one row of points at each step
for (let angxy=0; angxy<2*Math.PI; angxy+=angstep){
    for (let angyz=0; angyz<2*Math.PI; angyz+=angstep) {
        // Loop from 0 to 2*pi, creating one point at each step
        this.points.push(new Dot({
            x: r * Math.cos(angxy) * Math.sin(angyz) + x,
            y: r * Math.sin(angxy) * Math.sin(angyz) + y,
            z: r * Math.cos(angyz) + z,
            fg: Math.cos(angyz) > 0
        }));
    }
}

And the loops over the elements of this.points now look like this:

for (const point of this.points) {
    point...
}

Ahh... So much better. ๐Ÿ˜Œ

Classes!

Since this project was all about this interactive sphere composed of a bunch of dots on its surface, and since I was doing a CS degree in 2012 and the Functional Revolution of the last few years hadn't begun yet, the project is largely composed of two high-level object types:

  • Dot - represents a single dot on the surface of the sphere
  • Sphere - represents the whole sphere, with all of its Dots

In 2012, the native class was yet to be standardized, so I wrote everything in the old-school function-as-a-constructor style. For exapmle, here's part of the Sphere constructor:

function Dot(x,y,z,fg,fgColor,bgColor) {
    // Default Values:  { x: 0, y: 0, z: 0, fg: true, fgcolor: #7EE37E, bgcolor: "#787878" }
    this.x = typeof x !== 'undefined' ? x : 0;
    this.y = typeof y !== 'undefined' ? y : 0;
    this.z = typeof z !== 'undefined' ? z : 0;
    this.fg = typeof fg !== 'undefined' ? fg : true;
    this.fgColor = typeof fgColor !== 'undefined' ? fgColor : "#7EE37E";
    this.bgColor = typeof bgColor !== 'undefined' ? bgColor : "#787878";

    this.Draw = function(ctx) {
        // Store the context's fillStyle
        var tmpStyle = ctx.fillStyle;
        // Set the fillStyle for the dot
        ctx.fillStyle = (this.fg ? this.fgColor : this.bgColor);
        // Draw the dot
        ctx.fillCircle(x,y,this.fg?10:5);
        // Restore the previous fillStyle
        ctx.fillStyle = tmpStyle;
    }
}

Now, there's another issue here, which probably had to do with my limited understanding of JS prototypes: every instance of Dot defines its own copy of the Draw function, rather than referring to a class method. What I should have written is this:

function Dot(x,y,z,fg,fgColor,bgColor) {
    // ...
}
Dot.protoype.Draw = function(ctx) {
    // Store the context's fillStyle
    var tmpStyle = ctx.fillStyle;
    // Set the fillStyle for the dot
    ctx.fillStyle = (this.fg ? this.fgColor : this.bgColor);
    // Draw the dot
    ctx.fillCircle(x,y,this.fg?10:5);
    // Restore the previous fillStyle
    ctx.fillStyle = tmpStyle;
}

This declares a single copy of the function, rather than wasting memory on a new function every time.

But that's a side issue anyway, since the native classes introduced in ES2015 (aka ES6) handles that under the hood!

But before we get to that, let mention one more thing: ES2015 also introduced object destructuring, function parameter default values, and using both of those things together to basically get the equivalent of named parameters with default values, as seen in languages like Python and Ruby!

Aaaaand one more thing: ES2015 also introduced Object.assign(), which I like to use to initialize this with properties much more succinctly than the traditional one-by-one method. (Man, ES2015 was some good stuff!)

Using these two techniques together, I can eliminate this nasty snippet from the the Dot constructor:

// Gross ๐Ÿคข
this.x = typeof x !== 'undefined' ? x : 0;
this.y = typeof y !== 'undefined' ? y : 0;
this.z = typeof z !== 'undefined' ? z : 0;
this.fg = typeof fg !== 'undefined' ? fg : true;
this.fgColor = typeof fgColor !== 'undefined' ? fgColor : "#7EE37E";
this.bgColor = typeof bgColor !== 'undefined' ? bgColor : "#787878";

Putting all these improvements together, I can rewrite the Dot like this:

class Dot {
    constructor({x=0, y=0, z=0, fg=true, fgColor='#7EE37E', bgColor='#787878'}={}) {
        Object.assign(this, {x, y, z, fg, fgColor, bgColor});
    }

    draw(ctx) {
        // Store the context's fillStyle
        var tmpStyle = ctx.fillStyle;
        // Set the fillStyle for the dot
        ctx.fillStyle = (this.fg ? this.fgColor : this.bgColor);
        // Draw the dot
        ctx.fillCircle(x, y, this.fg ? 10 : 5);
        // Restore the previous fillStyle
        ctx.fillStyle = tmpStyle;
    }
}

So much cleaner!

Re-separating my concerns

As I was rewriting my classes as I just described, I started to find places where Dots were doing stuff that was better handled by Sphere, and Sphere was repeatedly performing operations on each Dot that could be much more cleanly handled internally by each of them.

First, that Draw method of the Dot class up there? Yeah, it was never actually being called. It turned out that I needed some context from the Sphere about each Dot in order to draw it correctly, namely where it was located on the surface of the sphere and how the sphere was currently rotated. And rather than pass all that context around to each Dot, it was much cleaner to do all my drawing right from the Sphere class. So... I deleted the Draw method on Dot, as well as the fgColor and bgColor properties that went with it.

Second, there are two major ways that the Dots are changed to move the Sphere around: they're translated (moved around), and they're scaled (grown or shrunken). I had a bunch of repeated code in my event handlers where each Dot's properties were being manually recalculated and updated in the same way, so I figured I'd DRY up my code and move the logic to change a Dot into the Dot class itself. I gave Dot two new methods: scale and translate.

After all that, my Dot class looks like this:

class Dot {
    constructor({x=0, y=0, z=0, fg=true}={}) {
        Object.assign(this, {x, y, z, fg});
    }

    /**
     * Scale this dot's position by multiplying all coordinates by the given scale factor
     * @param {Number} scaleFactor
     */
    scale(scaleFactor) {
        this.x *= scaleFactor
        this.y *= scaleFactor
        this.z *= scaleFactor
    }

    /**
     * Move this dot to a new position indicated by the given x, y, and z distances
     * @param {Number} [x=0]
     * @param {Number} [y=0]
     * @param {Number} [z=0]
     */
    translate({x=0, y=0, z=0}) {
        this.x += x
        this.y += y
        this.z += z
    }
}

Don't mess with the prototype!

I made several changes that came from experience. Before I left college, I really had little to no knowledge of accepted community best practices, or why they were important. For example, I have a few utility functions for drawing on the canvas. In my old code, I added them to the CanvasRenderingContext2D prototype directly, as follows:

Object.getPrototypeOf(ctx).fillCircle = ctxFillCircle;

This is not ideal. Suppose a real ctx.fillCircle were added to the spec one day, and it was different from my code. In this minor demo it probably doesn't matter much, as I'm the only one working with the code, but in a project with multiple authors, it could be very confusing to come across a standardized method that looks different than it should. Even future me might be confused!

So I refactored: a ctx parameter was added to each function, all references to this in the functions were changed to ctx, I removed the above lines that modified the CanvasRenderingContext2D prototype, and I changed the code to call the functions directly and pass ctx as the first argument rather than calling them as a method of ctx.

Removing jQuery

Finally, jQuery. I love it, it did a lot of good for the web, but in most cases, including mine, it's no longer needed.

I basically used jQuery for three things in this project, two of which are trivially replaced:

  • I used the $(function() { ... }) wrapper to delay code execution until the page loaded. As I discussed in Part 1, this is unnecessary if you add the defer attribute to any <script> tags that need to wait. (Thanks again, @crazytim!)
  • I used the classic $() selector function in several places to get elements. This is now (and actually was then, though I was unfamiliar with it) a native part of the platform with document.querySelector and document.querySelectorAll.
  • The tough one: I used the jquery.events.drag plugin to handle mouse-based interactivity. This one takes slightly more doing to replace.

The first two are super basic, and I won't bother showing them. Instead, I'll focus on that third one: the jquery.event.drag event plugin.

Here's how I was using it:

$("#canvas")
    .drag("init", function(e){
        startX = e.clientX;
        startY = e.clientY;
    })
    .drag(function(e){
        if (e.ctrlKey) {
            if (e.altKey) sphere.HiddenFun2(e.clientX-startX,e.clientY-startY);
            else sphere.Zoom(e.clientX-startX, e.clientY-startY, -1);
        } else if (e.shiftKey) {
            if (e.altKey) sphere.HiddenFun1(e.clientX-startX,e.clientY-startY);
            else sphere.Pan(e.clientX-startX,e.clientY-startY);
        } else {        
            sphere.Rotate(e.clientX-startX,e.clientY-startY);
        }
        startX = e.clientX;
        startY = e.clientY;
    });

So when dragging begins, store the starting position of the cursor, then when dragging continues, perform certain actions based on the modifier keys used, and update the cursor position for the next movement.

So how would we do this with regular old mouse events?

I implemented it in three stages: drag start, drag, and drag end. The drag start event is mousedown; we'll record the starting position of the cursor, and add the drag and drag end event listeners. The drag event will be mousemove; here we'll do all the logic to check for modifier keys and actually manipulate the sphere. Finally, I'll use two events for drag stop: mouseup and mouseout. That way, we don't get that weird case where you move your mouse out of the window and then let go, and the app is stuck in the dragging state until you click again. Those events just remove the event handlers registered on drag start.

I decided to add mousemove, mouseup, and mouseout to the window so that you can move your mouse around the page after clicking. And I had to suppress the mouseout event on the canvas and body elements using event.stopPropagation() to prevent prematurely triggering the drag stop when moving your mouse in and out of the canvas.

Here's what all that looks like:

let dragOrigin;
canvas.addEventListener('mousedown', dragStartHandler)
function dragStartHandler(e){
    dragOrigin = {
        x: e.clientX,
        y: e.clientY
    };
    document.addEventListener('mousemove', dragHandler);

    window.addEventListener('mouseup', dragStopHandler);
    window.addEventListener('mouseout', dragStopHandler);

    canvas.addEventListener('mouseout', stopPropagation);
    document.body.addEventListener('mouseout', stopPropagation);
}

function dragHandler(e){
    if (e.ctrlKey || e.metaKey) {
        if (e.altKey) sphere.hiddenFun2(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
        else sphere.zoom(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y, -1);
    }
    else if (e.shiftKey) {
        if (e.altKey) sphere.hiddenFun1(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
        else sphere.pan(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
    }
    else {
        sphere.rotate(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
    }
    dragOrigin.x = e.clientX;
    dragOrigin.y = e.clientY;
}

function dragStopHandler(e) {
    document.removeEventListener('mousemove', dragHandler);
    document.removeEventListener('mouseup', dragStopHandler);
    document.removeEventListener('mouseout', dragStopHandler);

    canvas.removeEventListener('mouseout', stopPropagation);
    document.body.removeEventListener('mouseout', stopPropagation);
}

function stopPropagation(e) {
    e.stopPropagation();
}

So even that wasn't too bad! But then I realized I had forgotten about...

Touch support for mobile ๐Ÿ˜ญ

After implementing my updated code, I visited the page on my phone; nothing worked. Then it hit me: phones don't have mouses! ๐Ÿคฆ

Some quick research reminded me of the relatively recent pointer events (pointermove, pointerup, etc.), which are made to unify lots of different kinds of pointer-ish interactions: mouse, touch, stylus, new stuff we haven't thought of yet. The problem (for now) is that mobile support is spotty: pointer events work on Chrome for Android, Opera Mobile, Android Browser, Samsung Browser, and IE Mobile (???), but not Firefox for Android, iOS Safari, or most other mobile browsers..

The other problem is that the controls to do anything other than spin the sphere currently require modifier keys on the keyboard to be pressed (Shift+drag to pan, Ctrl+drag to zoom). So pointer events on their own will only enable rotating, nothing else.

A proper solution would be to learn about touch events and how to implement gestures like pinch-zoom and 2-finger-pan, but I'll be honest, I looked into it a little and it's so complex! More than I want to get into right now. So the pointer events will have to do as a half-measure that only fixes rotating and only works on some mobile devices. And I'm not thrilled about it ๐Ÿ˜ž

Anyway, here's what all of that looks like. I'm testing for pointer event support by checking whether the <body> has an onpointermove property, and if not I fall back to mouse events, since there are desktop browsers that don't support pointer events, too (looking at you, Safari ๐Ÿ˜ก).

// Prefer 'pointer' events when available
const pointerEvent = (
  'onpointermove' in document.body
    ? 'pointer'
    : 'mouse'
);
// Shorthands for 'mouse' or 'pointer' events
const [downEvt, upEvt, moveEvt, outEvt] = (
  ['down', 'up', 'move', 'out']
    .map(evtType => pointerEvent + evtType)
);

// Drag events
let dragOrigin;
canvas.addEventListener(downEvt, dragStartHandler)
function dragStartHandler(e){
    dragOrigin = {
        x: e.clientX,
        y: e.clientY
    };
    document.addEventListener(moveEvt, dragHandler);

    window.addEventListener(upEvt, dragStopHandler);
    window.addEventListener(outEvt, dragStopHandler);

    canvas.addEventListener(outEvt, stopPropagation);
    document.body.addEventListener(outEvt, stopPropagation);
}

function dragHandler(e){
    if (e.ctrlKey || e.metaKey) {
        if (e.altKey) sphere.hiddenFun2(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
        else sphere.zoom(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y, -1);
    }
    else if (e.shiftKey) {
        if (e.altKey) sphere.hiddenFun1(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
        else sphere.pan(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
    }
    else {
        sphere.rotate(e.clientX-dragOrigin.x, e.clientY-dragOrigin.y);
    }
    dragOrigin.x = e.clientX;
    dragOrigin.y = e.clientY;
}

function dragStopHandler(e) {
    document.removeEventListener(moveEvt, dragHandler);
    document.removeEventListener(upEvt, dragStopHandler);
    document.removeEventListener(outEvt, dragStopHandler);

    canvas.removeEventListener(outEvt, stopPropagation);
    document.body.removeEventListener(outEvt, stopPropagation);
}

function stopPropagation(e) {
    e.stopPropagation();
}

By the way, while working on this I discovered that Chrome's dev tools are very nice for emulating mobile devices with touch events!

Oh, one final thing. I had to add a CSS rule to the canvas to make touch work properly: touch-action: none;. This basically prevents the browser from capturing touch events to try and scroll the page, and lets me use them in my JavaScript.

Finished product

It was a lot of iteration, and to be honest I tweaked it a bunch more while writing this article, but it's out there! Here's what it looks like with all the updates:

Conclusion

There are still things I'd like to fix. The biggest issue I have is that it's not mobile friendly, really at all. Not only do zoom and pan not work on mobile, but the webpage itself is almost completely non-responsive. I'd like to add some CSS to handle especially small page sizes more cleanly. Maybe I'll tweak it some more after this ๐Ÿค” As I said at the top, I'd love any feedback you might have! Heck, if you're feeling generous, make a PR against the repo!

But here's my overall takeaway: HTML, CSS, and JavaScript have improved a ton in the last 7 years, and so have I as a developer. Experience is an amazing thing!

In the final part of this series, which I'll write...eventually, I'll walk through the JavaScript itself and talk about how it works. That one will really be a standalone tutorial on how to build a thing with <canvas> in vanilla JavaScript, not really dependent on these previous two posts.

Top comments (17)

Collapse
 
misterwhat profile image
Jonas Winzen

Awesome! Thanks for sharing!
Canvas is an interesting and powerful thing and we're using it way less it deserves to be used. Maybe a proper UI library that eliminate the verbosity of working with canvas would make it more attractive.

The expressiveness of Javascript increased orders of magnitude more, than anyone would expect. The rewrite of the Dot constructor makes it immediately visible. It's shorter, easier to understand and less error prone, than the ternary typeof checks of arguments. A single ! there is easily overseen or accidentally introduced and yields somewhat around an hour of debugging fun.

I'm looking forward to your next article. ๐Ÿ‘๐Ÿ˜

Collapse
 
rhymes profile image
rhymes

Jonas, you mean something like d3.js or chart.js or an actual UI library with components and such?

Collapse
 
misterwhat profile image
Jonas Winzen • Edited

Well both libraries serve the needs for data visualizations.
What I meant, were libraries to build full blown interactive UIs, based on canvas. In the (not so recent) past, there was a boom of super fancy Web Apps Sites using Flash only to render their contents.
Building user interactions within canvas is quite verbose, same for rendering UI elements (accessibility could be shimmed by an underlying html construct to support screen readers and text selection).

From what I've heard there is a React renderer that renders to canvas. I don't know how usable it is in reality - but I think that goes into the direction I was thinking of.

Edit: For drawing UI elements I found React Canvas and for drawing interactive graphic content I found React Cova. Their use cases seem to be orthogonal to each other i.e. you cant replace one with the other).

Thread Thread
 
rhymes profile image
rhymes

What I meant, were libraries to build full blown interactive UIs, based on canvas. In the (not so recent) past, there was a boom of super fancy Web Apps Sites using Flash only to render their contents.

I have only one question: why? Why this would be something that's needed? I'm not sure I understand. Thank you!

Thread Thread
 
kenbellows profile image
Ken Bellows

Games are the first thing that come to my mind. Title screens and menus and such with clickable components. Entire web apps in a Flash style are hopefully gone for good though ๐Ÿ˜ฌ

Thread Thread
 
misterwhat profile image
Jonas Winzen • Edited

Google Maps is a prominent example, that uses interactive canvas to render large pieces of the UI. Typical use cases would include any interactivity, that goes beyond the capabilities of boxes and text (like zoomable super high resolution images, virtual flip-charts or street maps.)

Thread Thread
 
misterwhat profile image
Jonas Winzen

Entire web apps in a Flash style are hopefully gone for good though ๐Ÿ˜ฌ

Hahaha, yeah ...hopefully ๐Ÿ˜‚

Thread Thread
 
rhymes profile image
rhymes

@kenbellows right, games! I have the feeling that we'll skip canvas UI libraries to go straight to WebAssembly for that type of apps.

For general apps (games included) I'm curious to see if Flutter's Hummingbird will amount to something. With Flutter they are targeting Skia, a 2D graphics library Google built in C++. With Hummingbird they might be able to target the Canvas instead.

Thread Thread
 
rhymes profile image
rhymes

BTW it seems that the HTML5 standard is quite strongly against text editing controls implemented in the canvas.

Thread Thread
 
kenbellows profile image
Ken Bellows

For good reason, I've tried it and there are so many problems that have to be solved just to get it to act like a regular text input, not to mention the accessibility nightmare it creates! (Although I suppose any predominately canvas based app is going to face huge a11y issues...) If I ever need text input in a mostly-canvas-based situation again, I'll just absolute-position an actual <input> tag where I want it and set focus to it or something

Thread Thread
 
kenbellows profile image
Ken Bellows • Edited

As for WASM, I haven't looked into this topic yet, but I have two questions:

  1. How much canvas access does WASM have? Wouldn't you still need JS for the actual draw actions, even if some other heavy processing of game states happened in WASM? Is there a planned future where WASM has canvas access, if it doesn't now?

  2. How does how does WASM relate and compare to WebGL for heavy canvas processing? I've actually seen some significant use of WebGL over the last few years for browser games, FPSes and such, and the results have been pretty impressive.

Thread Thread
 
rhymes profile image
rhymes

For good reason

Agreed, the list of reasons not to do that it's quite long on the HTML5 spec :D

How much canvas access does WASM have? Wouldn't you still need JS for the actual draw actions, even if some other heavy processing of game states happened in WASM?

Yeah, wasm still needs JS. It doesn't have direct access to the canvas but you can access the wasm memory. This is an example of the game of life written in Rust and the canvas API.

Is there a planned future where WASM has canvas access, if it doesn't now?

They are working on something to help with that. Right now wasm only understand numbers and that doesn't really help if the goal is to have it talk to the DOM. Look for "Easy and fast data exchange" in this article.

How does how does WASM relate and compare to WebGL for heavy canvas processing?

I think you can access WebGL from WebAssembly:

There's a lot going on :D

Collapse
 
kenbellows profile image
Ken Bellows

You know, now that I'm thinking more about it, there actually is a group of libraries that attempts to do this, and I think they use the canvas: CreateJS, and specifically EaselJS, their main drawing library. I played around with it several years ago and remember having a pretty good experience with it, but I didn't do anything super advanced with it.

Collapse
 
kenbellows profile image
Ken Bellows

Thanks for the compliment! ๐Ÿ˜

One thing I find really interesting is the emphasis that the TC39 working group (that is, the folks that develop the ECMAScript spec that becomes JavaScript) often put on intentionally writing very low-level APIs, with the express intention that community members should layer more convenient libraries on top of them. This way everyone isn't stuck with a high-level API that a lot of people dislike. A good recent example of this kind of low-level API is the Shared Memory & Atomics proposal that adds some very important concurrency features to the language. Using these APIs directly would probably be horrible, but there are lots of ways for people with deep understanding of concurrency to layer very useable convenient concurrency libraries on top.

All that to say, I think you're right on the money about Canvas: it could really benefit from some higher level libraries layered on top of it to make it more usable. It's rather tedious to use by hand for anything more complicated than jSphere, but I think that's by design. We really need someone to come along and publish that perfect library that will make the canvas easier to use...

Collapse
 
misterwhat profile image
Jonas Winzen

Thanks for your answer! ๐Ÿ˜ƒ

Low level APIs are the very common across recent Browser/Web APIs. Working directly with those low level APIs usually works out until you approach a certain degree of complexity. At some point you are required to build an abstraction layer on top of these low level apis, that serves your needs.
The Shared Memory and Atomics, you mentioned is one example. Another currently very emerging example are Web Component. Writing Web-Components with the browser APIs only is certainly not as verbose as fighting with concurrent memory management, but starts becoming verbose as soon as you want to sync more than two attributes with props of your web component.
Within the last year a lot of libraries for building web-components were released. Each with their own benefits and promises.

So you're absolutely right: low level APIs are a very common and necessary pattern (to serve everyones needs).

Collapse
 
rhymes profile image
rhymes

Nice adventure Ken!

Have you considered using a library for drag & drop and touch handling? It seems like the sort of thing that a library that's already worked out the kinks an compatibility issues would provide for "free".

Collapse
 
kenbellows profile image
Ken Bellows

Oh yeah, there are several libraries out there. If I decide to come back to it I might use one, though I may just use it as an excuse to learn the details of touch gestures, which I expect would be tedious, but worth doing once for the deep knowledge