Log In  

Cart #wobblepaint-6 | 2020-11-04 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

Wobblepaint started as a secret cartridge in my 2019 Advent Calendar entry, but I think it's time for a proper release! This version has some extra controls and nicer, less crinkly wobble.


Your brush has a size, colour, pattern and shape that can be adjusted separately. There are 4 presets you can select and modify using keyboard shortcuts, or by clicking and dragging the top menu bar down to reveal a palette of attributes.


CTRL-Z, CTRL-Y (or S,F) to undo/redo
CTRL-C, CTRL-V to copy and paste between doodles
W,R to switch between doodles (or use the menu buttons)
TAB to toggle menu
Mouse wheel (or e,d) to change brush size
RMB to pick up a colour
RMB in menu colour palette to select secondary colour (used for patterns)
LMB+RMB in menu colour palette to set the background colour

To save all doodles, use the cartridge icon button in the pull-down menu.

Wobblepaint saves data to itself. To start a new wobble cart, type LOAD #WOBBLEPAINT from inside PICO-8 and then save it as something. The data storage is reasonably efficient so you can get around 20~100 doodles to a cart depending on complexity.

To save a gif to desktop, use the gif button to record a second of looping wobble. If you want to record multiple doodles (e.g. for an animation or story), press tab to hide menu, CTRL-8 to start a gif, W,R to flip through the doodles, and then CTRL-9 to save the gif.

Gamepad controls

Turn off the devkit input in the options menu ("turn off mouse") and use a gamepad:

LRUD to move the cursor
[X] to paint
[O] + L/R to undo/redo
[O] + U/D to adjust brush size
In the menu, [X] and [O] behave the same as LMB,RMB

Using Wobblepaint doodles in your cartridges

CTRL-C copies doodles in a text format that can be pasted into code (or bbs posts)

Paste the code from tab 5 into your cartridge to load and draw them:

str_to_mem(wobdat, 0x4300)
mywob = wob_load(0x4300)
function _draw()

Or alternatively, copy the binary data straight out of the spritesheet and use load_library (tab 2) to load all of the doodles into a table.


v1.5: fixed uneven frame times when recording gif and increased length to 2 seconds (was 1)

P#83422 2020-10-29 03:14 ( Edited 2020-11-05 07:20)

:: zep
P#83452 2020-10-29 10:23 ( Edited 2020-10-29 10:24)
:: Numzyx
P#83458 2020-10-29 15:04
P#83472 2020-10-29 21:24
:: cubee
P#83483 2020-10-30 05:55

+second palette 👀

BTW I had no idea CTRL+key and TAB had their own ORD values. Is there more information about that somewhere? Maybe a list? :)

I wasn't able to record a gif via BBS, it says "saved on desktop" but the file doesn't show up anywhere (I'm using Windows 10)...

P#83496 2020-10-30 13:12
:: machi
P#83497 2020-10-30 14:00

Oh hey @machi ! I think you were on sheezyart ?? Not sure. I know we both share a mutual friend (shaddowkarate/newrem) though. I was darkscythe.

Great drawing!

P#83500 2020-10-30 15:22

@BoneVolt same here. The gif showed up on the left side of the bbs page, and I had to right click and "Save image as..." to download it. Maybe the save to desktop only works if you run the cart in standalone pico8.

P#83505 2020-10-30 15:55

@ianjsikes ooh, I didn't notice it before, thanks!

P#83506 2020-10-30 16:09

Happy Halloweenies

P#83508 2020-10-30 17:04 ( Edited 2020-10-30 17:12)
:: dimAI
P#83509 2020-10-30 17:12 ( Edited 2020-10-30 17:13)
:: machi

I never heard of Sheezyart, must be someone else, Im afraid :v

P#83513 2020-10-30 18:00
P#83518 2020-10-30 19:46
:: pilaf
P#83651 2020-11-03 08:04

How do i download the gifs ;-;

P#83737 2020-11-05 00:13
P#83739 2020-11-05 00:32

i made a little dandelion...
and a fatty bee, why not :3
and just to say, im here cuz of a brazilian youtuber called Goularte, and i loved this site!

P#83757 2020-11-05 01:32 ( Edited 2020-11-05 01:33)

I also watch that guy. He made a video about PICO-8? Cool.

P#83842 2020-11-06 03:02

Id just like to say that zep, youre such a wonderfull guy. The BBS has helped me so much, its just a great place thriving with creativity and amazing people, a place that you created alongside the community. I just want to say, thank you zep, we all love you! <3

P#83843 2020-11-06 03:25 ( Edited 2020-11-06 03:26)
:: Skykai
P#83855 2020-11-06 12:35

isso é bom!!!

P#83883 2020-11-06 23:21


P#83962 2020-11-08 00:45 ( Edited 2020-11-08 00:52)


P#83963 2020-11-08 00:46
P#83972 2020-11-08 10:06

Wow! I really enjoy reading Zep's carts source code because it teaches me some new tricks. But! This one has put me down... I can't figure out how the editor save the states and undo/redo them.

Could someone explain the basics about data compression and everything subject related and implemented in this cart?

P#84046 2020-11-09 16:50
:: kikito

@gcuellar The function doing the parsing is wob_load. Inside there, getval is used to extract numbers from memory, bit by bit. It does so by calling peek, which reads Pico-8 internal memory and returns it, but byte by byte. So sometimes the numbers read by getval require several bytes read (for example a number can take the last two bits from 1 byte and then the first 3 bits of the next byte).

Back into wob_load, the main thing it does is using getval to fill up a table called scn.

The first thing it does in order to do this is read the first 16 bits, which indicate the size of the scene in bits (variable dat_len). Then it reads the scene background color (4 bits) and stores it in scn.background_color.

Then it reads a bunch of "curves" using a loop which exists using the aforementioned dat_len var. Each curve has a bunch of properties, encoded as numbers (color, size, shape, etc). Some curves have "segments", which require an extra loop.

Curves are stored in the array part of scn, and segments are stored in the array part of each curve. So what you end up at the end is a scn table which looks like this:

scn = {
  background_color = 16,
  [1] = { -- first curve
    col = 15,
    shape = 3,
    [1] = { ... } -- first segment
    [2] = { ... } -- second segment

With this in mind, you can probably imagine how to undo works: it takes the last curve from scn, and puts it on another table called undo_stack.

"Redo" does the opposite - move the curve from undo_stack back to scn.

To load from a string, you first put that string into memory with this game's str_to_mem, and then read the memory.

The other interesting function is wob_draw, which is able to read the curves of a scn table and perform the relevant pico-8's functions. There is a table called funcs which is used to select how to draw every "shape", depending on the "shape number" of each curve. For example, shape number "0" (the first one in funcs is a circle). The functions read their parameters from the scn table. Each "curve" and "segment" end up calling the relevant pico-8 function or functions.

wob_draw is also in charge of doing the "wobbly effect" by altering the dots positions by random amounts. Picking the amounts involves doing something tricky with magic numbers, in the nrnd function.

With regards to "data compression"... I would call it "efficiency". Everything is stored as numbers, but they are densely packed to their size, with no wasted space. For example: consider the background color. In pico-8 there are only 16 colors. If you wanted the background color to be number 12, and stored that in plain text, you would need the character "1" followed by "2". That's 2 bytes/16 bits total. But by handling stuff in binary you can dedicate exactly the 4 bits you need for that, which "saves" 12 bits.

I hope it helps!

P#84610 2020-11-22 23:33 ( Edited 2020-11-22 23:38)

. @kikito Kudos for you, mate. Your explanation was very useful.


P#84651 2020-11-24 09:22

Having an absolute blast with this cart. Thanks, zep!

Maybe a long shot, but perhaps someone with knowledge of the GIF format can help figure out a problem I've found. I haven't figured out the exact circumstances that lead to it, but in this exported GIF, some colors are dropped/replaced when attached to a message in iMessage on iOS. See the attached example. I've sent other exports to friends and colors have been fine, but in this export the colors are consistently dropped. All the colors are rendered correctly on macOS in Preview, just not inline in the actual iMessage thread.

Any clues?

P#85075 2020-12-05 16:09

Okay, so after a lot of learning about and experimenting with the GIF file format, I found a partial fix and a workaround. The nature of the fixes provide some clues into what's going on which led me to a further line of investigation related to the macOS Core Graphics framework.

Partial fix

Removing the global color table and adding a local color table to every image (based on the global table) produces a file that works with Messages on macOS but, I interestingly, NOT on iOS. Here's one way I came up with to do it using Go:

package main

import (

// usage: go run global2local.go input.gif output.gif
func main() {
    inputName := os.Args[1]
    outputName := os.Args[2]

    image, err := load(inputName)
    if err != nil {

    // Remove the global color table, coercing the encoder into
    // writing a local table for each image.
    image.Config.ColorModel = nil

    err = save(image, outputName)
    if err != nil {

func load(name string) (*gif.GIF, error) {
    file, err := os.Open(name)
    if err != nil {
        return nil, err
    defer file.Close()

    return gif.DecodeAll(file)

func save(image *gif.GIF, name string) error {
    file, err := os.Create(name)
    if err != nil {
        return err
    defer file.Close()

    return gif.EncodeAll(file, image)

Lossy workaround

Resizing the output down 50% with gifsicle produces a working file:

gifsicle --resize 128x128 -o output.gif input.gif

Further research

Discovering the problem was related to the global color table led me to a Stack Overflow question which describes very similar symptoms indicating a possible weird interaction with global color tables and Core Graphics in certain circumstances. I have no experience with Core Graphics and so haven't investigated further.


Wobblepaint delegates GIF rendering to PICO-8, so any change to the encoding would need to happen there. For reference, replacing the global color table with local tables in my example file adds 5,509 bytes. I'm not an image processing/GIF expert by any stretch so other than output file size, I'm not clear what implications there might be to changing the GIF encoding in PICO-8 to stop using global color tables. I'm not sure whether the optimization global tables represent are considered important in the context of PICO-8.

It's also not sure whether there's some acknowledged bug in macOS/iOS that will ever be fixed.

I need to do more experimentation to understand why the local color table rewriting fix doesn't seem to be effective only on iOS.

Anyway, hope someone finds this info useful. Thanks again, zep!

P#85094 2020-12-06 20:22 ( Edited 2020-12-06 20:30)
P#85418 2020-12-15 10:27
P#85435 2020-12-15 17:32
P#85472 2020-12-16 14:29 ( Edited 2020-12-16 14:30)
:: moss
P#86692 2021-01-22 23:24
:: Yo

Little bear

P#88448 2021-03-03 14:09

Color pad button doesnt seem to be working :\
still able to make some things tho...

P#88451 2021-03-03 15:04

Im an idiot. I didnt see click and DRAG. sorry

P#88453 2021-03-03 15:16

Took me quite long to realize there are other tools lol
My first time using this

P#88817 2021-03-11 00:15 ( Edited 2021-03-11 00:18)

I drew Shellos from memory, I think it turned out pretty good

P#91160 2021-04-26 22:21
P#91740 2021-05-10 06:35
:: Czarlo

I was just testing this and made this just to see what it looked like lol

P#92166 2021-05-19 01:25
:: Czarlo

figured out how to change colors, made Mario to see how it would look with a black outline.

P#92167 2021-05-19 01:39
:: RobEX
P#92344 2021-05-22 06:06

[Please log in to post a comment]

Follow Lexaloffle:        
Generated 2021-07-29 15:26:36 | 0.196s | Q:94