Building a Paint App in TypeScriptHow to leverage modern tools for high-performance web appsKenneth ReillyBlockedUnblockFollowFollowingMay 8Screenshot of a simple paint app developed in HTML5, CSS3, and TypeScriptIntroductionSince being introduced to TypeScript back in early 2013 when it was around version 0.
3, I have used it at every opportunity I could get my hands on, from refactoring the front-end of a hybrid desktop app, to building things like simple games, websites, and even high-volume production services such as automated voice communication systems — with a high degree of success.
Last year I built a simple paint program called paintbros as a hobby project to create raster graphics in a custom format for developing an experimental 2D game engine.
While not feature complete, it demonstrates the concept of creating highly interactive web experiences using TypeScript and modern features of HTML5 and CSS3 — which we will examine in further detail.
The Basic SetupDue to the custom nature of this application and its only system requirements being a modern browser on a full-screen device, no pre-existing frameworks were used.
The idea is to utilize as many built-in features of modern browsers as possible — with the only dev dependencies being Node and a TypeScript compiler.
There is a very good reason for doing it from scratch, as these performance and best practices metrics in Google Lighthouse reveal:Example of the performance that can be achieved by keeping it simpleThis is stock performance with no real optimizations or other improvements required, which is a pretty good position to be in.
Hitting 99% on an alpha MVP is like starting at the finish line.
This demonstrates the kind of power available when choosing lightweight tools and technologies like TypeScript and leveraging the powerful features available within HTML5 and CSS3.
Let’s take a look at the project layout, and our two project configuration files.
From here we can see the app has a typical website structure, a handful of files in TypeScript, some basic asset folders, and project configuration settings:Project layout, generated package.
json, and tsconfig.
json with compiler optionsThe lack of external runtime dependencies makes this an incredibly clean and lightweight application, but it also means that everything will have to be defined from scratch.
Fortunately, there are many great advantages to working with this setup, especially from within VS Code, which was designed from the ground-up to support TypeScript and includes powerful built-in code completion and refactoring tools for modern web technologies.
Creating the basic UX structureWith the config files in place, the next item on the agenda is to define the basic HTML structure of the application.
Included are a header with buttons, the editor area, and a sidebar with containers for a palette, swatch, and tools:HTML definitions for the editor surface and some rudimentary toolsThese components serve as the workspace of the application’s main (and only) screen.
It’s pretty straightforward, with a logo and standard commands at the top of the screen and some stuff to select colors and tools over to the side.
Not too original, but the idea is to keep it really simple.
Beyond this, we have a footer with copyright information and a few modal dialogs for handling basic operations such as creating, loading, or saving images:Static footer and simple dialog components for file operationsThat’s basically it for the application HTML (minus the load file dialog which is mostly another copy of the first two, and the about dialog which just displays some informational text).
I could go a step further and create an abstract component for the dialogs, but it’s not really worth it for a grand total of three of them in the application.
Any more, however, and it would be a good investment to create something that can be configured and re-used.
The CSS is pretty lean as well, with again no pre-existing framework used to allow for 100% customizable UX from scratch.
We’ll use CSS3 features such as variables and calc() to define some global properties to use through the app:Utilizing powerful features of CSS3, including variables and the calc() functionHere we have definitions for color values, margins, palette component sizes, and a 45-degree striped editor background to make it obvious when any pixel is occupied by any color.
Defining CSS variables ahead of time like this goes a long way towards prototyping the UX of a graphical application where you might have to change the layout details dozens of times before getting it right.
We won’t dive into the entire CSS file (you can check out the project source here), but let’s take a look at a few key concepts that form the bulk of the UX:Fullscreen body with static header and command button stylesSince this is a fullscreen web app that doesn’t scroll like a content-based website would, we set our height and width to 100% (I typically use vh units but I banged out this project within a few days’ time so the code isn’t polished up).
Right away we’re able to benefit from our CSS variables without having to add another dependency for a CSS preprocessor such as SASS, which I usually try to reserve for larger and more complex projects.
Next we’ll take a look at one of the most important concepts in the app, the editor surface style:Styles for main display and editor containers, the editor itself, and editor “pixels” (.
editor > i)Flexbox is employed to position and stretch elements horizontally and vertically within their respective parent containers.
The display, editor container, editor, and sidebar components are defined, forming the core structure of the front-end layout.
The editor pixels wrap horizontally within the editor and everything stretches to fit its parent in the desired manner.
This wraps up our definitions for presentation and style using HTML5 and CSS3 respectively, and it’s easy to see why it renders quickly and hits performance metrics out of the ballpark.
There’s a little more to it but not much— a few items that more or less replicate the concepts we’ve covered.
Now that we have the structure and graphical elements out of the way, let’s dive into the process of building out the app logic in TypeScript.
Defining palette colors, interfaces, and enumsFor this stage, the first task was to create the file src/colors.
ts, paste in 256 color values copied over from this xterm color cheatsheet, and export it as a var.
Next up is src/types.
ts, where we export some interfaces and enums:Xterm’s super-retro 256-color palette and a handful of convenient definitionsHere we have a few basic concepts that drive the rest of the application:ImageSize: the height and width of an image (a pixel unit is assumed)ImageContent: structure of the image for loading, saving, and editingPaintTool: list of tools from which the currently selected tool is chosenToolMode: not used, intended for area operations (to be implemented)The main application fileNext we will take a look at the source behind build/paintbros.
js, the file we imported as an ES6 module as described above, which subsequently loads other modules as necessary.
The source for this file is in src/paintbros.
ts:PaintBros class with buttons, dialogs, and static initializationsThe PaintBros abstract class contains static references to buttons and dialogs for file operations, and initializes other application components, such as the palette and editor.
Here we begin to see some of the benefits of TypeScript.
This code is largely self-documenting and it’s easy to tell what goes where, and why.
From here, we’ll start looking into the core editing features.
The Palette UXNext up, we’ll check out our first basic controller, in src/palette.
ts:Palette class showing element references, data accessor property, and init methodsThe Palette class is the controller for the palette and swatch UX, with static references to the palette element itself, the elements to display the current and previously selected colors, and a string containing the currently selected color (i.
The accessor swatch_data() is used to retrieve the most recent colors used, to allow saving them along with the image file later.
The init() method retrieves our DOM element references and draws the palette itself by creating each element, binding it to an event handler, then appending each color element to the palette.
Also, we can see the init_swatch() and reset_swatch() functions, which handle those tasks.
Let’s check out the other methods defined within the Palette class:Methods within the Palette class for working with the swatch and handling click eventsAlso within this class we find the load_swatch() method for loading colors from a previously saved file (we’ll get to file operations later), along with make_color_el() which generates a swatch item element, and the on_click_color() method which serves as the event handler for — you guessed it — clicking on a palette color.
When a color is clicked, if it matches the currently selected color, the event is discarded — otherwise the last item in the swatch is removed, the current color is pushed onto the front of the swatch stack, and the newly selected color is set as the current color.
The Tools UXMoving on to the next component of our app, we’ll take a look at the controller that allows the user to select between the brush, eraser, and so on:The Tools class with its init() and on_click_tool() functions — not much to this oneThe default tool assigned to current_tool is the paintbrush, and we can see that the init() function selects the tool buttons and binds each of them to the on_click_tool() handler, which alerts a “Not yet implemented” when a tool that isn’t built yet is selected (this was put together almost overnight after all).
When the user selects a tool that exists, it becomes the selected option.
The EditorThis is where most of the action happens.
The editor contains the actual image information itself, along with the means to display the image, and allows it to be edited by someone using the program.
Here is src/editor.
ts:The Editor class, with image_data() accessor and init() method in viewThe editor has properties for the image dimensions, DOM element references to the editor element and its parent container, and a boolean to keep track of the mouse down state.
For simplicity, the actual working image data is stored within the pixel elements themselves as a dataset property and only retrieved when necessary through the image_data() accessor, which works just like the one from the swatch, extracting data to be used within a file save operation.
The clear(), load_image(), and new_image() functions within the Editor classHere we have methods for loading previously saved images from an object that implements ImageContent and drawing it onto the editor, and for creating a new blank image from an ImageSize.
Various components of both the Palette and the Editor are updated to reflect each scenario’s required outcome, while individual “pixels” are created and appended to the editor.
The advantages of TypeScript are becoming more clear as we get into more complex parts of the app.
Rather than having to read through spaghetti code line-by-line serially to piece together the different concepts of how the editor works, we get more of a graphical representation of how everything is assembled, and the developer can infer what something is within a fraction of a second without necessarily having to dig around.
The key here is context, as each line is packed with enough information to help understand it properly.
The remaining methods of our Editor class: handlers for resize and click eventsFinally, our Editor class contains methods for resizing when the screen is drawn or screen dimensions are changed, for updating a “pixel” when clicked, and for handling mouse move events (mainly to prevent the mouse from being “stuck” in the down position and drawing randomly on the editor).
Modal Dialog UXNext up is src/modal.
ts with Modal, an abstract class our dialogs will extend:The base Modal class which the individual dialog box classes extendThe Modal class contains properties for the modal container and dialog, and a static reference to the currently visible instance that is used to globally hide any open dialog when the user clicks outside of it, which is handled by the hide_listener static property.
Also, we have our show() and hide() methods, a static hide() method that implements the global dialog hide feature, and an internal querySelector to conveniently select child elements of the dialog:An example modal dialog, the NewFileDialog classThe NewFileDialog class is our first implementation of a class that extends our abstract Modal base class.
We have a property for our dialog fields, a default handler for on_request_new_image (which is replaced with an actual handler set by the PaintBros class during dialog initialization to handle real events), click handlers for OK and Cancel which either calls the attached handler for on_request_new_image and/or hides the dialog, and a basic constructor() which passes the dialog title up to the base Modal class with super() and initializes DOM references, event handlers, and fields.
We won’t dig into the two other dialogs, LoadFileDialog and SaveFileDialog, which are effectively copies of NewFileDialog with a few modifications, hooked up to event handlers which handle those respective functions.
We will, however, look into the implementations of those functions next.
File OperationsFinally, we come to our last-but-not-least feature, file operations for saving and loading images.
This is implemented within file.
ts:The entire contents of src/file.
ts and File class, which handles all the app file operationsThe File class is pretty lean as well, handling 100% of the file load and save operations for this program in 37 lines of self-documenting code.
The load() method makes use of the FileReader web API to asynchronously load the contents of an image JSON file from a Blob returned by a file input element on the LoadFileDialog, which passes the data to the on_load handler, in turn parsing the JSON into an object and passing it to Editor.
The save() method performs the opposite, as expected, serializing the image name along with the image data pulled from the editor, packing it into a Blob and saving that to the local machine by triggering a file download click event.
That’s about it for the file operations.
Currently the only supported format is the custom “format” which is just the image information serialized into JSON and stored on the local machine as a .
json file, however I do have plans to implement the option to save and load BMP files at some point in the future.
While this program isn’t likely to become the next big thing in raster graphics editing, it shows just how much can be done with only a handful of small TypeScript files and a few lines of clean, well-defined HTML5 and CSS3.
I hope you enjoyed this article, and good luck on your next TypeScript project!.