Quantcast
Channel: Hacker News 50
Viewing all articles
Browse latest Browse all 9433

2013-05-13: Writing a Vi-like Graphics Editor in Racket

$
0
0

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 Framework

The 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 Bitmap

The 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.

(define/override (on-paint)  (define dc (get-dc))  (define it (send dc get-transformation))  (send dc set-smoothing 'unsmoothed)   (define cw (get-width))  (define ch (get-height))   (define the-scale    (floor (min (/ cw w) (/ ch h))))  (send dc translate        (/ (- cw (* w the-scale)) 2)        (/ (- ch (* h the-scale)) 2))   (send dc set-scale the-scale the-scale)   (send dc draw-bitmap the-bm 0 0)   (send dc set-transformation it))

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 Line

Let’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-Buffer

The 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:

(define doing-something-else #f)(define/public (on-char ke)  (match doing-something-else    ['saving     ....     record key presses     (when hit-enter?       do the save)     ....]    [#f     ....     [#\s      (set! doing-something-else 'saving)]     ....]))

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 Version

The 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:


Viewing all articles
Browse latest Browse all 9433

Trending Articles