aboutsummaryrefslogtreecommitdiff
path: root/posts/SDL2_devlog.md
blob: 04ac92871f8e6a58040693d41df4a23fc8723238 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
I have been working on an editor for the [One Bit
Image](https://git.peppe.rs/graphics/obi/about) file format
in Rust and SDL2. This entry in my blog follows my progress
on the editor. The days are listed in reverse chronological
order, begin from the bottom, if this is your first time on
this page.

### Day 18

I added basic support for guides, they can be added and
activated from the `lisp` REPL. Another long standing
improvement I wanted to make was reworking the pixmap
drawing procedure. The old procedure draws a square for each
pixel in the pixmap, coloured according to its value in the
pixmap. Naturally, this means, for an **NxN** pixmap, there
are **N²** calls to SDL! I reworked this procedure to
compress each line of the pixmap using RLE (run length
encoding), and call out to SDL for each run in the line.
This drastically improved drawing speeds on larger grids.
The following is a comparison between the two procedures,
the leftmost picture is the rendered image, the middle
picture is the optimized drawing procedure (draws each run
instead of pixel), and the right most picture is the
primitive drawing procedure (draws each pixel):

![Day 18](https://u.peppe.rs/U4B.png)


### Day 17

I decided to give the text-only statusline a touch up, by
adding a active color and dither level preview. Aligning the
"widget" to the right of statusline involved a lot more than
I thought, so I created a ghetto CSS-like rectangle
placement system to position containers inside containers:

```rust
// roughly something like this
let statusline = 
    Container::new(Offset::Left(0), Offset::Bottom(40))
    .width(Size::Max)
    .height(Size::Absolute(20));
    
let mut primary = Container::uninit()
    .width(Size::Absolute(16))
    .height(Size::Absolute(16));
    
container.place(
    &mut padding_box,
    HorAlign::Right,
    VertAlign::Center
);
```

The result (brush preview on the bottom right):

![Day 17](https://u.peppe.rs/OtU.mp4)


### Day 16

The embedded lisp is coming along nicely, users can load
a custom `rc.lisp`, which is evaluated on startup. To
disable to grid on start, for example:

```scheme
;;; rc.lisp
(toggle-grid)
```

Some aliases to switch between brushes:

```scheme
;;; rc.lisp
(define (brush kind)
  (cond
    ((eq? kind 'f) (brush-fill))
    ((eq? kind 'c) (brush-circle))
    ((eq? kind 'l) (brush-line))
    ((eq? kind 'l+) (brush-line-extend))
    (else (brush-circle))))
```

The following script draws a straight line along a given
axis, at a given distance from the canvas boundary:

![Day 16](https://u.peppe.rs/b3i.mp4)

### Day 15

I began writing a standard library for the lisp, in lisp. It
includes basic list operations: `car`, `cdr`, `null?`,
`list`, higher order functions: `map`, `filter`, `fold`:

```lisp
(define (member? item ls)
  (fold #f
        (lambda (acc x) (or acc (eq? item x)))
        ls))
```

### Day 14

I attempted a [small art
piece](https://peppe.rs/art/ramen_noodles.png) using the
editor, while it was largely usable, I felt a certain lack
of feedback. The brushes just didn't relay as much info as
I'd have liked, for example, the approximate points of the
line or the angle made by the line against the x-axis.
Unfortunately, the existing infrastructure around brushes
and line drawing didn't easily allow for this either. I went
ahead and reimplemented brushes, and added a new flood fill
brush too:

![Day 14](https://u.peppe.rs/8q.mp4)


### Day 13

I added a few more forms to the `lisp` evaluator. It handles
recursion, definitions, variable mutation and more. The
prelude contains 20 subroutines so far, including
comparision and logic operators. The REPL interface on the
SDL side requires some UX tweaks; environment based
completion, readline motions sound doable.

![Day 13](https://u.peppe.rs/u3.mp4)

### Day 12

I lifted most of
[murarth/ketos](https://github.com/murarth/ketos) into the
editor. `ketos`'s implementation of `lisp` is too vast for
my use case. For example, the editor does not need data
types to handle raw strings or byte strings. I have got a
basic evaluator running inside the SDL2 context (notice the
`lisp` REPL at the bottom of the window). Over the
following days, I intend to create a set of prelude
functions to manipulate the pixmap. Users can implement
their own brushes, dithering patterns, keybinds and more
(hopefully).

![Day 12](https://u.peppe.rs/y0.mp4)

### Day 11

I intend to supplement the editor with scripting language
and an inbuilt REPL for the same. I began by implementing a
text box widget from scratch, with history and readline like
editing:

![Day 11](https://u.peppe.rs/Mh.mp4)


### Day 10

I started reading up on dithering methods and half-toning, I
wanted to create a dithering brush that would automatically
produce popular dithering patterns. The method that caught
my eye (and also the one used most often in pixel art), was
Bayer's ordered dithering. When applied to a black and white
image, each pixel, based on its intensity, is mapped to a
4x4 grid of pixels. A completely empty (completely black)
4x4 grid represents zero intensity, and a filled 4x4 grid
represents full intensity. Bayer's ordered dithering can
produce 15 steps of intensity between zero and full (by
switching on exactly 1 pixel more at each level), thus,
being able to draw 17 "shades" from white to black. Creating
a dithering brush from here was fairly trivial. Our pixmap
is supposed to represent the final dithered image, it must
be divided into 4x4 grids. Each grid is colored based on the
intensity of the brush passing over it:

![Day 10](https://u.peppe.rs/Mn.png)


### Day 9

I started working towards an interface. I like the idea of a
largely read-only HUD, i. e., an interface that simply
describes the state of the application. Changes to this
state are initiated via keybinds or text commands. I am
proud of the symmetry indicator; `-` for horizontal
symmetry, `|` for vertical symmetry, `+` for radial
symmetry.

![Day 9](https://u.peppe.rs/hx.png)

### Day 8

One of my favourite features of GIMP was symmetric editing.
I added some coordinate geometry primitives to my pixmap
abstraction, allowing for mirroring and reflecting figures
about lines or points. The result was an ergonomic function
that applies symmetry to any painting operation, (undo/redo
works as expected):

```rust
let line = self.pixmap.get_line(start, end);
let sym_line = self.symmetry.apply(&line);
for point on line.extend(sym_line) {
    // draw to window
}
```

![Day 8](https://u.peppe.rs/B1.mp4)

### Day 7

Bresenham saves the day again! This time, I implemented his
line drawing algorithm, to, well, draw lines. Each point on
the line is then "buffed" based on the active brush size.
Today's changes fit in very well with the undo system and
the brush size feature. Creating the right abstractions, one
at a time :)

![Day 7](https://u.peppe.rs/xt.mp4)


### Day 6

I extended Bresenham's algorithm to draw not just circle
outlines, but also generate their fills. Unlike Bresenham's
algorithm, this variant generates points for two quadrants
at once, these points are mirrored over the dividing axis to
generate the other two quadrants.

![Day 6](https://u.peppe.rs/f3.png)

### Day 5

I discovered and implemented Bresenham's algorithm for
efficient circle drawing. The algorithm allowed for sized
circular brushes, something I really liked from GIMP. Very
convenient that the Wikipedia page for Bresenham's algorithm
also includes a section about optimizing for integer based
arithmetic. I managed to abstract out another giant
component of the application, the pixmap. Any image is just
a grid of pixels (a pixmap), where the pixel's value is
decided by the application (1-bit in my case). I could
potentially extend the application to a 24-bit image editor!

![Day 5](https://u.peppe.rs/Kh.mp4)


### Day 4

I created a generic "undo stack" data structure that allows
for infinite "undos" and "redos". Every modification
operation to the grid is persisted to the application state.
A couple of keybinds allow the user to revert and re-apply
these operations! I expect abstracting this component will
come in handy down the line.

![Day 4](https://u.peppe.rs/w5.mp4)


### Day 3

I implemented the bare minimum required to call the program
an "editor". The application displays a grid, tracks mouse
events, paints white to the canvas on left click, and black
to the canvas on right click. I created a make-shift MVC
architecture à la Elm in Rust.

![Day 3](https://u.peppe.rs/GF.mp4)

### Day 2

I started figuring out event handling today. Implemented a
couple of keybinds to zoom in/out of the drawing area.
Conversions of SDL2 coordinates (measured in signed 32 bit
integers) to my internal "drawing area" coordinates
(measured in unsigned 32 bit integers) is very annoying.
Hopefully the unchecked conversions won't haunt me later.

![Day 2](https://u.peppe.rs/L4.mp4)

### Day 1

Getting started with Rust and SDL2 is very straightforward.
The `rust-sdl2` library contains some detailed examples that
allowed me to get all the way to drawing a grid from a
`Vec<bool>`:

![Day 1](https://u.peppe.rs/Ma.png)