Comments:"2013-05-13: Writing a Vi-like Graphics Editor in Racket"
URL:http://jeapostrophe.github.io/2013-05-13-vi-post.html
2013-05-13: Writing a Vi-like Graphics Editor in Racket
If you know me well, you know I hate mice and love keyboards. Naturally, this means I love things likevi,Emacs,Conkeror, andxmonad.
Unfortunately, there are not a lot of keyboard-based graphics editors, and I am in the need of some help creating sprite graphics for myGet Bonus project. So, I wrote my own editor in Racket:apse - the Animated Paletted Sprite Editor.
There are a few things interesting about it, but in this post, I focus on the core of its job: dropping pixels and changing colors. The best part is how the minibuffer works.
- 1 FrameworkThe first thing we need to do is to create a window (called aframe) that contains a canvas and a status line.
However, if we want to be able to receive input events, we’ll need to customize the on-char handler of the canvas and give it focus. We have to create a sub-class of canvas% to do that:
Next, we should customize the canvas further so that we are in control of how it draws. This is done through an method calledon-paint which is responsible for doing the drawing. We need to make sure to regularly refresh the canvas to cause it to redraw and the key event handler is a convenient place to do that.
This is the basic framework of everything we’ll add.
2 Version One: Movement and a BitmapThe next step will be to change so that we are editing a bitmap and displaying it.
The first step is to represent the bitmap, where the cursor is, and what the current drawing color is. For convenience, we’ll make the bitmap always a nice even 64. (Very large for a sprite, actually.)
Next, we’ll need to customize the on-char handler so you can use the arrow keys to move around, use the space bar to drop a pixel, and press q to quit.
Finally, the on-paint method must change to actually draw the bitmap on the canvas. We’ll draw it scaled (with an integer scale) and in the center of the canvas. This will help it maintain crispness, while keeping it easy to see.
There are a few cute things about this drawing routine: It saves the transformation matrix to return the state back to the beginning, so we don’t repeatedly zoom in. It uses the 'unsmoothed mode to get deliciously jagged pixels. It gets the canvas dimensions every draw to deal with window resizing.
This all gets inserted into our framework:
As an exercise, you should add something to display an outline around where the cursor is. You’ll want to use draw-rectangle.
3 Version Two: Using the Status LineLet’s use the status line to communicate with the user about simple things, like where the cursor is and what color they just wrote. For fun, we’ll add how long the command took to execute. We just need to customize the on-char handler for that: we’ll have the key-code match return a string which will be the new status text.
It fits in the framework just as before:
4 Version Three: Implementing the Mini-BufferThe only remaining things we’ll want to add to the editor is a way to save the image and a way to change the color. Unlike our previous commands, these both require more important from the user: the file name and the new color. One obnoxious way to handle these would be with a pop-up textbox, but our goal is to implement something like how vi/emacs/etc work, where the user types at the "minibuffer"—which is like our status line.
It would seem that we must add some sort of global state to our program that recognizes when we are attempting to communicate with the user, and if so, handle keys differently, and then after it’s done remember why we were trying to interact and handle it appropriately. The code would look something like this:
Obviously, I wouldn’t be writing this if we were really going to do something so ugly. Instead, we’ll write code like this—focusing on the first two cases:
The key is the with-minibuffer form that allows the use of the minibuffer and the minibuffer-read function which prompts the user for input.
The main idea of these functions is that with-minibuffer sets up a continuation prompt and gives control to the minibuffer code if there is a minibuffer-read call active.
It is the responsibility of minibuffer-read to capture the continuation back to the prompt, then escape back to the prompt with the initial prompt. When the return-to-minibuffer-call continuation is called, it uninstalls itself and returns the value from the read interaction.
The body of the minibuffer handler is fairly routine: It is in the context of input-so-far, which a string it uses to track what the user has typed. It looks at the key event and implementsreturn as a signal to return the value, backspace as removing the last character, and otherwise accumulates characters. The only interesting part is the way it handles the escape key as canceling the interaction, so it uses a pun on the use ofinput-so-far to give a cancellation message.
When we plug this in to our framework, we have a basic key-oriented image editor.
5 Full VersionThe full version of theminibuffer code (only 176 lines) adds a lot more: tab completion using a prefix trie, controlling valid characters and accept predicates, etc.
The full version of theimage editor (only 593 lines) adds even more: palettes, view the image at different resolutions, constructing animations, etc.
I made the only system fairly modular so I could re-use a lot of code and create asprite sheet cutting tool at a very low cost: only 315 lines.
If you’d like to run this code at home, you should put it in this order: