Log In  

I wanted to create background images w/o having to use tiles/sprites for that.
So I'm wroting a small python script, that takes a 128x128 png, reduces it to the most commonly 16 used colors from the pico8 palette, does RLE encoding on them and outputs the values as a string.

This string can then be imported into pic8 and used to render your background image.

The python part is not yet perfect (I need to write the custom palette generation code), but if you limit it to one of the two pals, it kinda works.

End even when re-drawing the bg from the data every frame, it's kinda ok.

The encoder and the functions to use the result can now be found at: https://github.com/iSpellcaster/pico8rle

Here's the code of the latest version.
Includes base64 encoded of variable sized sprites than can be flipped. Should run after copy and paste, nothing else required.
To encode your own images, check the github link above.

function _init()
 state  = "title"
    title = explode64(kanji_rle)
 batman= explode64(bat_rle)
 antiriad = explode64(antiriad_title_rle)
 _draw = draw_title
 start_t = time()

function _update()
 if btnp(❎) then
    if state == "title" then
        _draw= draw_batman
    elseif state == "batman" then
     _draw= draw_antiriad
   state = "antiriad"
     start_t = time()
   _draw= draw_title
   state = "title"
     start_t = time()

function draw_title()
 local i,ofs,y
 ofs = flr((time()-start_t)*45)
 ofs2= flr((time()-start_t)*2)
    y = ofs%250-100
    if y > -127 then
 cprint("press ❎",nil,110,{9,10,9})

function draw_batman()
 cprint("press ❎",nil,110,{9,10,9})

function draw_antiriad()
 cprint("press ❎",nil,110,{9,7,9}) 

-- util
base64str='[email protected]#$%^&*()_-+=[]}{;:<>,./?~|'

bat_rle="20201,g61,461>g1461>471>471>471<481<481<481:491:491:491;g1491;4a1;4a1;4a0%411c4b0%420441164b0%47164b0%43g143154c0$44k243144c0$45g142g1144c0$49g112g14c0#4b124d0#4b124d0p47064b10g14e0o4a034b0|g14g0n4p0?4j0n4o0.g14l0m4o0.4n0m4p0>g14n0l4q0>g14n0l4r0<4o0l4s0;g14o0k4v0}g14o0k4x0[[email protected]_g14p0i4#0_gq0i4a014r0_g24o0i49034r0(g44n0h4a044v0$gr0f4b064%0tgr0f4a085bgk0f49075hgg0e49075mgc0e49075pg90e48085tg50d48095wg20d[email protected]0d47065s88g1810e46055t81g186g1810f44065tg186g2810o5v86g1820o5v86g1820n5x880n5y83g1830m5z83g1830m5qg182g146860l4b035c8744g1850k4b045b8a43g1840k4a055ag18b43840j49074/844ag18d41g1830i49084-034a85g1488j0i48094-054886g147g18i0h480a4-064781g185g1478i0f490c49024z08458946g18h0d490e49034z08448ag1458h0c490f49044p024807g1438c4589c185c10b490g49054n05460782428e4389c185c10a490h48074m07450584418f4387c285c109490i48084m08430584g18gg14187c185c209490i48094l0a42048uc185c2084a0i48094m0b41028hc185c285c284c308490j470b4d05430e8ac185c285c285c284c3074a0j460d4b08420d8ac185c284c384c384k1c207490k460e4a09420c84c184c285c284c483c1k1c183c1k1c18106490l460f490m84c284c284c285c7k2c5k1c18106480l470g4401430m84c283c285c284c8k1c5k2c18105480l470i4204420l83c284c284c8k1c6k1c5k2c18105480l470i4205410kc282c284c482c8k1c5k2c5k2c205470m470j420pc683cek2c5k2c4k2c304470m480k410pcgk1c6k1c6k2c4k2c304460n480l420mchk1c5k2c5k2c5k2c303460o480m420lcak1c5k2c5k2c5k2c4k3c303450p470o420kc4k1c5k1c5k2c4k3c4k3c4k3c34102440q470)c5k1c4k2c5k2c4k3c4k3c3k3c4470q470)c4k2c4k2c4k3c4k3c4k3c3k3c4460r470)c4k2c4k2c4k3c3k3c4k4c3k3c4490p460)c4k2c3k3c3k3c4k3c4k3c3k3c1814181c14c0m460(c4k3c3k3c3k3c4k3c3k4c5g144g14f0j460(c4k3c2k3c4k3c3k4c3k3c182g1484h0h460(c3k3c3k3c4k3c3k3c582g14b4k0e460*c4k3c3k3c3k3c4k3c181g14g4n0c440(c4k3c3k3c3k3c4k1c1814j4q09440(c3k4c2k5c1k4c182g14m4s07440*c4k4c2k4c2k281g14q4v04440*c4k4c1k4c182g14t4y01440*c3k4c3k1c1814x4$0&[email protected]$0&c6814%4^0%83g14*4(0zg14+4-0tg14gg14gg44e4[0ng14ig348gc43g2484{0gg445g147gy484:0a81g;484,0481g>48g14?gu8kg186g14883g14?gl8xg14885g14?g28[4988g14?g18+498bg14/g18_498e4?g18&g1498gg14?g18$4a8j4?g184c186c18p4a8j5082c186c18p4a8j4383g14.g186c186c185c18bg14a8ig14386g14/82c285c285c18b4b8i44894/81c185c285c18b4b8i43g18bg14.g184c284c284c2854b"

antiriad_title_rle="20202020202020202020200jg1w5g105w#0.0jg1w6g104g1w!0/0kg1w6g104g1wy0?0lg1w6g10gw50lg20:0mg1w70fw50kw40;0ew6g102g1w706g1w403w503w4g102g1w404w607g1w40cg1w6g102g1w6g10b0dg1w703w805g1w403w502g1w4g102g1w403w806g1w40cw7g102g1w70b0cg1w803w8g104g1w403w502g1w4g102g1w402wa05g1w40bw8g102g1w80a0cw903w9g103g1w403w502g1w4g102g1w4g1wc04g1w40bw8g102g1w8g1090bwa03wag102g1w403w502g1w4g102g1w902w703g1w40aw9g102g1w9090ag1wa03wc01g1w403w502g1w4g102g1w804w702g1w409g1w9g102g1wa080awb03w5g1w7g1w403w502g1w4g102g1w706w701g1w408g1wag102g1wag10709g1wb03w4g101g1wb03w502g1w4g102g1w6g107w6g2w408wbg102g1wb0704g1w1g101g1w6g1w503w4g103wa03w502g1w4g102g1wk01g1w403g103w7g1w4g102g1w4g2w60604w3g1w701w503w4g103g1w903w502g1w4g102g1wj02g1w402g1w2g101w6g2w4g102g1w4g101w6g10503wb02w503w4g104g1w803w502g1w4g102g1w4g1wd03g1w401g1wag102w4g102g1w4g102w60502g1wag102w503w4g105g1w703w502g1w4g102g1w402g1wa04g1w4g1wb03w4g102g1w4g102g1w60402wb03w503w4g106g1w603w502g1w4g105w203g1w805g1w4g1wa04w4g102g1w4g103w6g10303g1w804w503w4g107g1w503w502g1w40181+58104w706g1w402w904w4g102g1w4g104w60304g1w6g104w503w4g108g1w403w502g1w2g101+904w705g1w403w7g104w4g102g1w4g104w70204w8g103w503w4g108g1w403w502g1w1g101+1810181+4s1}1+104w704g1w403w8g103w4g102g1w4g105w6g10103wag102w503w4g108g1w403w4g103g101+20381g1s3g1+204g1w603g1w402wag102w4g102g1w4g105g1w60102g1wbg101w503w4g108g1w403w30181+20281+302g1w2!1s1+3810181+2w702g1w401wcg2w4g102g1wh0102wj03w4g108g1w403g202+38101+581g1w1g1+58101+4g1w5g101g1w3g101wig102g1wg0201g1w601g1wb03w4g108g1w40281+382s282+6g2w1+601+1s281+3}1w3g101g1w1g101w7g1wbg102g1wf0301w603g1wa03w4g108g3}1+4s3+101+381+482g20181+581+381+1s3+3}18103g1w603wag102g1we0401w604g1w903w4g10781+4s6+201+3820e+382+3s4+5g1w5g104w9g102g1wd0502w406g1w803w4g105+4s4+48401+307g107+385+4s4+381w306w8g102g1wc0603w2g107g1w703w4g10581+1s2+485+301+3810481+30581+282+385+4s2+1}1w208w7g102g1wb070fg505g30781+284+882+28104}1s2+1}104+382+883+38101g10ag604ga0806gs0282+d0181+203+2s2+2810281+282+c8201w103gq0b06wug181+c83+28101+4s1+3810181+283+b82w2g101ws0a07wv0f+301+4s1+401+30ew401g1wr0b08wu028302820a+4s1+40983028301w301g1wr0c09ws02+58409+90984+504g1wq0d0#81+5810181+188+28105+387+18201+60(0#81+5810281+88102+2s1+1810181+98102+6810*[email protected]+2s1+4810381+58103+68103+60481+70&0z81+3s1+381018308+a81088202+4s1+30^0z81+802+286+4810481+487+1810181+80^0z81+80281+981028502+a0281+8810%0z85+3810381+6810281+7810281+68103+4850%0%81+2068304+b04830581+20_0!81+382+10a81+403g10281+4810a+10181+3810^0z81+5098201+4g881+3830a+50^0z+609+28101+20a81+201+309+5810%0y81+281+1820881+30181+1g109}1+18101+30982+40%0y81+4810981+281028102g203g202810281+20a+4810%0x81+681088101+304g1!103!105+3820881+60$0x+80581+68104g1s1!2s2g104+70581+80#0v81+48102+30481+68204g2!1g205+705+30281+4810!0t81+70181+30481+704g2!1s1g20481+705+382+70z0t81+a810581+6}1g202!1s1!3s1g101g1s2!2}1+406+481+60z0u81+6830681+4!1s2!1g102g1!2g1!202g1!2s3g1+30783+6810z0v+28202830681+3!1s2!1g104g1!303g2!1s4}1+207830281+30!0_81+2g1s4!1g104g1!1g105!1s4}1+1810}0)+481/1s3g309g3!1s1/1!181+3810[0(+501!1s2!1g1!208!1/1s1!4g101+50=0(+502g1!1g1s3!204g1!1s4!1g103+50=0(+504!1s5g103s6!2g3}1+40=0(+501g201g1s5/1!101!1s5!2s2!2g1+40=0(+5g1s2g101g1!1s3/1!101!1s3/1!1g2s4g1+40=0(+5s4g101g2!3g3!3g201g1!1s2!1g1+40=0(81+183s3!10hg1!4}1830=0+g1s2!1g106g407g1!1s2!10{0+!3g106g1!2g1s1g303g1s4g10{0-g1s1g1038103g1!5g302g1s3/1!20{0_81s2g2!1018202g1!301!302s3!401810}0_81s2g1!1g1018104g301g3s3!2g301+20}0)81g1s2!20181+18103g1!2g3s2!1g304+2810]0_!1s1!2g182+18103g301g2!2g10781+1810]0*81+181!1s2!10281+181s1}20h81+3810+0&81+281s2!101+28103s2}2820c+6810-0)!1s2!101+382s1}1s1820e+4820+0&8101!1s2!1g101+1810181}181s1}2g1}2810g820-0%+3g1!2g201+181g40181g1810181}1810b81+7810)0$+401g1!1g5!1s2!101g2s2g10c81+9810(0#81+18401g201g1!1s4/1g10181}1g382}10882+2870(0#+38402g1!1s5!1g2018102}1s1}10989+30*0r83}281}1w2}10181+7g1!1s4!2g402g30982+a8101w4}1w1}3830u0l82}3/1w1/9810186w1!1s2!4s2!10g82+487}1/1s1/3s1/3w3}4810p0g}4w1/c85/1}18103g1!3g2s3!2g1098307820581}1/2s1/5s1/9w1}2820l0b81}4w1/5}1w1/2}181}2w2/5s4/3w1g101g3s1!1g1048104s1/3s18104/3g1}1w2/4w1}186}1/f}1830h0883}2w1/8}2w2/4s1/1sc/105g104}1/204s4/104}1s1/3w5/bw3}3w7}481}1820f0883}2w2/6w1}2w1/6s7/1s5/18107}1/38104}1w1/30481w2/7sb/3}2/9w1}281}1820f0784}1w2/bw2}2w1/cw1}2810381}1/5w1088105w1/3sc/3s1/4}1/2s2/8w1}18201830c07}183}1w2/8s3/3}282w2/4w1}182g1s8/3w1810dg1!1w2/5s9/1s2/4}1/2s4/8w1}181}2820c08810182}1/1w1/b}2/5}5/3}2/1s4/1g1}1810g!1s2g103w2/3s7/6}1/2s6/7}2830e0983}4w2}1w1}1w2}1w1/3s5/3s1/1s2/2w181}2810kg1!1s2!1g10581}1/aw1}1w1/3s2/7w3}281}1810f0c}181}5w3/5s2/1s9/2}20p!1s3!107}1/6w1}1w1/3s4/9w1}3820g0e83}1w3/bs7/2}10qg1!1s3!10481w2}5/1w1}2w1/dw2}1820j0i81}3/1w1/3s1/4s1/1s1/80q!2g1!2g1w2/9s2/381}1/8w1}28101810l0k810182}3w2/1w1/d}10k81w1}101g1!3g1w1/1s5/3s3/2w1/3w1}6840o0q810184w5/1w1/7w182}1/3}1810a}1/2}1g1!1g2}1/1w1/ag1/1w1}6810w0y810182}7w4/5w10a}1/3w1}181}1w4}981}2810#0-81}70b}281}582}18101820-2020202020202020202020202020202020202020202020"

function test(x)
 print("hi "..x)

base64str='[email protected]#$%^&*()_-+=[]}{;:<>,./?~|'

function explode_hex(s, delimiter)
 local retval,i=split(s,delimiter,false)

 for i=1,#retval do
  retval[i] =("0x"..retval[i])+0
 return retval

function explode64(s)
 local retval,lastpos,i = {},1,2

 while i <= #s do
  add(retval,base64decode(sub(s, lastpos, i)))

  lastpos = i+1
  i += 2
 return retval

function base64decode(str)
    for i=1,#str do
     for a=1,#base64str do
        if c==v then
            val *= 64
            val += a-1
    return val

function base64encode(val)
 local res,cur,i="", val
 while cur > 0 do
 return res

function spr_rle(table,_x,_y)
 local x,y,i,col,rle,w=0,0,3,0,0,table[1]
    while i <= #table do
        col = shr(table[i] & 0xff00,8)--% 16        
        rle = table[i] & 0xff
        if col!=0 then
            --rectfill is slightly faster
        if x >=w then
            x = 0
            y += 1

function spr_rle_flip(table,_x,_y)
 local x,y,i,col,rle,w=0,0,3,0,0,table[1]
    while i <= #table do
        col = shr(table[i] & 0xff00,8)--% 16        
        rle = table[i] & 0xff
        if col!=0 then
            --rectfill is slightly faster
        if x >=w then
            x = 0
            y += 1

function draw_rle(table,_x,_y)
 local x,y,i,col,rle,w=0,0,3,0,0,table[1]

    while i <= #table do
        col = shr(table[i] & 0xff00,8)--% 16        
        rle = table[i] & 0xff
        --rectfill is slightly faster

        if x >=w then
            x = 0
            y +=1

function setpal(palstr)
    local i,palindex
    for i=1,#palindex do

function pal2()
    local i
 for i=0,15 do

function cprint(txt,x,y,cols)
 local len,org=#txt*4+4,clip() 
 local a
 x=x and x or 64-len/2
 for a=1,3 do

Is there interest for something like this?
Not sure if I should invest the time to make the python script "presentable" - right now it's quite a mess, and like I said not feature complete.

P#79494 2020-07-17 13:28 ( Edited 2020-07-20 17:22)

This looks good, very straightforward and lightweight system. Using "line" is a great idea, and seems so simple, I'm surprised I haven't thought of that before.

P#79501 2020-07-17 16:23

I had the idea while creating the BG for my karate animation project.
People said it reminded them of IK+, so I wanted to add a fitting bg. But I didn't want to use sprite data for that. So I used primitives, and rendered the floor (randomly with vert lines) and the mountains in the bg with horiz lines.

That gave me the RLE idea.
I fiddled with the code a bit, and now I can render them including transparency, so it can be used for title texts, etc. as well as backgrounds.

P#79503 2020-07-17 17:12

That's really cool. I've had some similar ideas, and I'm working on a system that draws an arbitrary number of various shapes directly from strings, I just didn't think of combining drawing functions with rle.

I was also thinking that this could offer transparency just by not drawing a given line, that's really useful. I already have some ideas for some further optimizations, and depending on how well they work, this might prove to be a just plain better overall alernative to the logo rendering system I just uploaded.

P#79508 2020-07-17 19:17

Hehehe, same here.
I just reduced the character count of the RLE emcdoded imaages a bit.
I also think that I'll switch from fullscreen images to custom size images, to get a bit more flexibility.

How does your code work?
I was thinking about something like "drawing processor" that would use opcodes for the commands. It could reduce the number of tokens, if you have a lot of primitive drawing.
Something like
0 0 - clear screen col 0
1 0 0 127 127 2 - draw a rect from (0,0) to (12,127) in col 2
That would allow to store the "drawing code" as a string of int values.

I guess your code does something similar to that?

P#79511 2020-07-17 19:35

Yeah, it works something like that in principle, but the string formatting is a lot more compressed. I believe it only takes about 65 tokens for the decoder function, also. The tricky part is that now I have to make an editor to encode the strings, because it's very cumbersome to do by hand.

P#79522 2020-07-17 21:42 ( Edited 2020-07-17 21:42)

Okay, I got a revision that uses much more compact strings, but it's not quite the unqualified success I had hoped for.

It uses the extended character set to encode values from 0-159 in each character. A single character can hold the color value as a multiple of 10, and the run length in the ones place. Ones-place values of 1-8 encode short run lengths along with color in a single character, while if the length is 9, that triggers the algorithm to look to the next character for the run length, and if it's zero, the tens place value is used for length and it skips ahead without drawing a line, providing transparency.


function _init()
 for i=0,15 do

function _draw()
local i,x,y=1,0,0
 while i<#str do
  local v,gap=ord(str,i)-96,0
  local col,len=v\10,v%10
  if(len==0) gap=col
  if(len==9) len=ord(str,i+1)-96 i+=1
  if(x>=128)x=0 y+=1

The result is no need for an 'explode' function, a savings of about 20 tokens, and an image string that's much smaller than before because it only needs 1 or 2 characters for each color segment and no commas, though this only results in something like a 20% compressed size reduction. This would be fine, and maybe it will offer more savings for images with shorter color spans, but for some reason the CPU power required has jumped from 14.5% to 23.8%, which I really wasn't expecting. The ORD function must be more power hungry than directly reading from tables, I guess. If you have any other ideas about that, I'm open to suggestions.

Curiously, all those righward pointing arrows in the string actually correspond to a value of 49, which means the color is 4, and look ahead to the next character to the right for the run length. ;)

P#79537 2020-07-18 03:46 ( Edited 2020-07-18 04:17)

rectfill is (used to be?) faster than line - give it a try

P#79540 2020-07-18 05:38

It uses the extended character set to encode values from 0-159 in each character

That's something I tried, but lead to larger results, since I had to reduce the actual run length for this two work. Leading to more Run lengths. Which lead to more data than I was saving with the encoding.
I might try to encode them as base-32. This would allow me to store my 3 nibbles in two characters and getting rid of the separator.

rectfill is (used to be?) faster than line - give it a try
Thanks a lot!
I gave it a try and it does reduce the CPU load slightly. I guess the line implementation got improved for trivial cases :)
But even a small improvement (for no extra costs) is a welcome improvement! :) :)

P#79541 2020-07-18 06:03

Yeah, thanks, I never would have thought of that.

Interesting, it seems a big part of optimizing for storage efficiency in Pico-8 comes down to experimentation, as we're basically dealing with multiple layers of compression stacked on top of each other. I'll have to test with some more visually dense images to see how much of an advantage the support for single-character short spans can give, as the current image is relatively sparse. Also, I think I can reduce CPU use in general by setting one color to transparent, as Pico 8 does. I think I over designed a bit with an a mechanism for setting transparency that's independent of color, because I just realized there's no mechanism there to encode for it without doing so by hand. o_O

P#79545 2020-07-18 09:27 ( Edited 2020-07-18 10:01)

Alright, guess my over-thinking was part of the reason for the increase in CPU load. After removing the useless code, it's down to 20.8% (19.8% if setting black to transparent) and if you leave out the palette change and cpu readout, token count is down to 87. Since single characters can now handle spans up to 9 pixels instead of 8, the string length went down 3% to 1537, and compressed size is approx. 7.71%, down from 9.55% for your intial string. This is a 19.3% decrease, and the effective compression ratio is 6.8:1! Curiously, I'm not seeing any decrease in CPU load from using rect() or rectfill() instead of line.


function _draw()
local i,x,y=1,0,0
 while i<#str do
  local v=ord(str,i)-96
  local col,len=v\10,v%10
  if(len==0) len=ord(str,i+1)-96 i+=1
  if(col!=0) rectfill(x,y,x+len,y,col)
  if(x>=128)x=0 y+=1
P#79548 2020-07-18 10:34 ( Edited 2020-07-18 13:29)

I'd guess line would be hw accellerated, so I might want to check the GPU load as well.

Back in the old days adding a negative number was also faster than subtracting a positive number, but I guess this kind of optimization is not needed anymore.
But it might be worth a try, just to see if it matters.

You should move your locals out if the inner loop though. Declare them at the start of the function, so you don't have the overhead of creating/ releasing the vars inside the loop.

P#79554 2020-07-18 11:56 ( Edited 2020-07-18 11:57)

Hmm. I tried defining the local variables at the start instead of inside the loop, but that actually increased the CPU load slightly, oddly enough. The negative number strategy didn't seem to have an effect. Thanks for the suggestions, though. I'm thinking the issue is probably the fact that each value read requires a calculation, as well as each value being split up via calculation into two smaller values. These things make the smaller string and compressed data sizes possible, though, so maybe there's just going to be a tradeoff?

P#79555 2020-07-18 12:55 ( Edited 2020-07-18 14:14)

You could split it once, an cache the split values. I don't have other ideas at the moment...

P#79559 2020-07-18 13:03

gpu load? - nah, pico8 is a vm - what is reported is a fake cpu usage, not reflectting at all what the host is doing.

P#79561 2020-07-18 13:55

I thought I saw a GPU stat in the API, but I guess I was mistaken... Sry

P#79563 2020-07-18 14:03

I am using something similar to @JadeLombax idea.of primitives drawn from string data. Wrote a bit about it here:


And I'm still improving it for my barbarian remake backgrounds. Can share some additional details if you're interested

P#79567 2020-07-18 15:46

Wow that's a great article!

Thanks for writing and sharing!
Didn't know about the env variable, that gives me a few ideas already.

And I think I want to check out pico8 draw now :)

P#79569 2020-07-18 15:57


Looks like you've spent a lot more development time on the idea than I have, and have something that works pretty well. Your program is pretty much exactly what I've been thinking about making, but hadn't made yet. Here's my current decoder function with a very short example string of a few shapes, I don't claim that it's the most advanced or polished, but the function and the string format are pretty compact and efficient, just 64 tokens total and 5 characters to define each object using one of several drawing functions, including sprites. I'm thinking that with a little adjustment, it could be useful for tweetcarts. =)


function _update()
 local t,ft={},{pset,line,oval,ovalfill,rect,rectfill,spr}
 for i=0,#s-1 do v=ord(s,i+1)-96
  if(i%5==4) ft[v\16](unpack(t))
P#79570 2020-07-18 16:37 ( Edited 2020-07-18 17:00)

I'm glad I joined this discussion.

While unfortunately picodraw is not 100% ready for utilization by others, one of the missing 2 critical features (final string compression and triangles support) will benefit greatly from a couple ideas I got seeing @JadeLombax 's code.
Then there's still an annoying bug that randomly pops-up while using it and I need more time to fix it, as I am still developing it in parallel with the game and will release it when everything is done.

I'll instead share some ideas here and see if they can be expanded further:

Right now, I don’t use pset and sprites command, but I used maps. This is just for my game needs and I think it makes sense instead to support all command before release.
I am using a 2 layers system, because the bottom layer can be mirrored as discussed here:


this allows you to only draw half shapes and mirror maptiles. The foreground layer drawn above is used to add unique details so it doesn’t really look like a mirrored image.

Here’s how I am storing data, 7 elements per shape because that's how many a map() command needs:


B and F signals background and foreground layers then numbers goes like this:
draw commands, x1,y1,x2,y2,color,fill pattern index so 6,15,38,47,76,7,0 is rectangle from 15,38 to 47,76, color 7 (white), 0 gradient, 7,53,77,66,88,9,10, is the last orange gradient filled oval.

And this is the 59 tokens decode function

function add_draw_commands(bg_commands,fg_commands,string)
  local cmd_table,dest_table,cmd,c_i = split(string),bg_commands,{},0    
  for i=2,#cmd_table do
    if cmd_table[i] =="F" then
      dest_table = fg_commands
      if (c_i==7) add(dest_table,cmd) cmd={} c_i=0

This is the updated 76 token drawing code, after implementing ovals and gradients:

function execute_draw_commands(commands)
  mode_string = split("rect,oval,line,map, ,rectfill,ovalfill,line,map, ,fillp")
  dither_p = split("32768,32736,24544,24416,23392,23391,23135,23131,6747,6731,2635,2571,523,521,9,1")
  for i=1,#commands do
    local cmd = commands[i]
    local c7 = cmd[7]
    if (c7!=0 and cmd[1] !=4) fillp(-dither_p[c7] +.5)

I am not using unpack() yet because there was a problem with maps and I didn’t want to change the code right now but it’s possible and will save a few tokens

I am using this uncompressed readable format for developing/debugging reasons but my goal is to use the same byte encoding as yours, with the following implementation details:
• Since gradient is an index 1-16 pointing to a premade gradient table, I will probably combine color and gradient info into a single byte and get down to 6 elements per shape
• Use chars from 33 to 256 so all cords will be encoded from -47 to 176, this gives a bit of “overscan”, useful for shapes and maps drawing with some parts outside visible area
• Remove the b/g parsing and instead store a value in command table index 0 to lark where the switch happens
• I plan somehow to also encode palette swapping, because it will make things much easier for maps/sprite while editing and redrawing, but this requires some UI to edit this in picodraw

BIG limitation

The biggest limitation of this system is that there’s no support for filled polygon like shapes, so that Batman background is close to impossible to replicate without sprites/map, BUT my plan is also to invest a few tokens for a fast filled-triangle function that can still be supported by the existing data encoding, with this I think the first command byte can also be reworked to allow for variable-length shapes data to increase compression even more

The upside is that with embedded fillp you get dithering which instead kills RLE compression

Drawing process:

In update() or whenever you change backgrounds, shapes are unpacked and the bg layer is drawn once and copied to ram mirrored, then NEED_COPY will be set to false

if (need_copy) add_draw_commands(bg_shapes,fg_shapes,gskull_shapes) draw_bg()

in draw() you just call draw_bg() again and this time it will raw both layers, with mirroring

function draw_bg() 
  copy_from_ram(8,119) –8,119 set from which to which line do the copy, to avoid identical symmetries

  --draw addition stuff over bg layer

  if (not need_copy) execute_draw_commands(fg_shapes)

  --draw stuff above fg layer


function copy_from_ram(start_y,end_y)
  if need_copy then
    for y=start_y,end_y do 
      for x=0,63 do
        local cc = pget(x,y)
      for y=0,127 do
        memcpy(0x4300+32*y, 0x6000+32+64*y, 32)
      need_copy = false
      for y=start_y,127 do
        memcpy(0x6000+32+64*y, 0x4300+32*y, 32)

I hope from this we can evolve picodraw together to make it more general purpose and usable by everyone

P#79589 2020-07-19 00:23

Wow, looks like you've got a pretty comprehensive system going here, hopefully it helps lessen some of the restrictions of Pico-8's tight cart size and makes some more kinds of projects viable for the average user.

I don't know if you're interested, but I have a very compact little right triangle function I made that uses standard 4 coordinate input formatting. Just right triangles, though, haven't figured out arbitrary angles yet.

P#79591 2020-07-19 01:20 ( Edited 2020-07-19 01:20)

There's a neat trifill algo here:

Guess you could use that.

P#79619 2020-07-19 18:20

@JadeLombax @TheRoboZ
I cleaned up the python code somewhat) and gave it a basic command line interface.
You can find the result at:

P#79627 2020-07-19 20:02
:: merwok

This tool from professoraction may be of interest!
It converts an image to the closest 16-color palette (selected from the 32 available colors).

P#79636 2020-07-19 21:44

@merwok - thanks for sharing. That's a great tool, esp. if you wantto use the sprite data to store the image.... I am so going to use that.
For my purposes, it's not optimal, since it dithers the image. Which is great for the look, but it will also create a lot of runs per line (which sucks for my algo) :)
Right now I'm converting it with my script (which will also reduce the colors, but w/o dithering), open the resulting image in an image editor and optimize that a bit (by trying to minimize the number of runs) and encode that image again...

P#79639 2020-07-19 22:12

The Triangle rasterizer benchmark is exactly where i was looking, I checked the latest one and was thinking of asking shiftalow if I could use his 176 token one as it;s a good compormise. But right now I'll gladly take a look at JadeLombax, since with the 4 coordinates it would be much easier to plug in the program as it is now.

P#79641 2020-07-20 00:26

Alright, here it is, along with a little code snippet to test it out with mouse input.

function trifill(x1,y1,x2,y2,c)
 local inc=sgn(y2-y1)
 local fy=y2-y1+inc/2
 for i=inc/2,fy,inc do
  local nx=x1+(x2-x1)*i/fy
  if(abs(nx-x1)>abs(x2-x1)) nx=x2

function _update()

It's pretty lightweight at just 76 tokens, and it works pretty well. It's not quite perfect, though. There are some slight anomalies in the pixel patterns vs. the built-in Pico 8 line function for some angles, and that's probably due to my use of simple algebra with trial-and-error to correct for any rendering issues, without getting into any trig or vector math. It might be useful for development purposes, though, and maybe the quirks can be ironed out.

P#79646 2020-07-20 02:26 ( Edited 2020-07-20 04:23)

I'm so looking forward to this. Your editor / renderer opens up so many possibilities.

I think it's really amazing what tools and possibilities there are.

Take a look at the tool @mervok linked - there are a lot of neat ideas in there.

I also added a function to my code, that pads all rle entries to 3 hex characters (1 for the color, two for the length), which allows me to get rid of the commas. It a lot of cases this saves a couple of hundred characters.

In my current test image I saved more than 1000 chars using this method.

Will now implement base-32 encoding, which should reduce the space requirements even further.

P#79655 2020-07-20 07:27

Now with base64 encoding. Reduced the character footprint noticeably.

P#79662 2020-07-20 10:25 ( Edited 2020-07-20 10:25)

Looking good! I tested your new encoding on the Batman image and compressed size (the real metric, since the 65,536 character limit is just a theoretical maximum) is down 19.8%, to 7.66% of max. I think you're right that it's most efficient to use a char set size no larger than what you need to encode the desired number of bytes and/or nybbles needed, so I'll look at revamping my implementation.

P#79673 2020-07-20 13:19 ( Edited 2020-07-20 13:35)

I was mainly looking for a way to get rid of the comma, and that meant I had to ensure a fixed data size per run. And since I get 3 nibbles in 2 chars using base64 this reduced the character count and made the output way more readable.

How do you determine the memory impact? Sry for the noob question, but I'm pretty new to the pico8 :)

P#79675 2020-07-20 13:42

That's fine, I'm fairly new to Pico-8 myself (and for that matter I haven't been consistently coding very long, either, tbh, just for a few classes).

The memory thing is just kind of a hack I developed. I noticed a little while ago that (presumably to help users getting close to the limit be careful not to go over it) between 99% and 101% of max. compressed cart size, the compressed size percentage readout goes from whole numbers to 2 decimal places, so I've been taking a string and cutting it up into chunks that I can feed into a cart filled up to exactly 99.00% with random hex values, geting a measurement, repeating for each next chunk, then adding up the total. If there's a better way of doing this like a stat() command, that would be great, but I haven't seen one, and I haven't spent enough time thinking about the issue to program a script that could automate the process for me yet.


Okay, now the hack-ish nature of the thing came back to bite me. I was testing the Antiriad title screen from your cart with my variable-length-encoding algorithm to see how it compares to yours when there's more short spans, and the gross estimate is 12% compressed size for mine vs. 13% for yours, but when I cut it up into chunks, the total for yours is a lot more than 13%. I guess the compression efficiency depends on redundancy, and cutting it in chunks instead of the whole thing at one time messes with that, which also calls into question my previous measurements. Ugh. I think I'll hunt around for anyone else who might have found a way to measure compressed size more accurately, and if not, I'll start a thread suggesting that Zep add it as a feature.

Further edit:

After a quick scan of the manual, I figured it out. You can get an accurate compressed size readout (to the byte, no less!) just by typing in "info" on the command prompt. Now my noob status is clearly showing =).

P#79676 2020-07-20 14:06 ( Edited 2020-07-20 15:09)

Alright, measuring things properly, here's a string size comparison of what we have so far (all sizes are compressed values)


SC's original algorithm: 1356 bytes
SC's B64 algorithm: 1149 bytes
JL's VLE algorithm: 1111 bytes


SC's B64 algorithm: 2184 bytes
JL's VLE algorithm: 1940 bytes

So it looks like for more detailed images with more small spans, the variable length encoding helps more, but the CPU cost for mine is also about 1/3 higher. I'll see if I can cut size down further to compensate, I guess.

P#79678 2020-07-20 14:54 ( Edited 2020-07-20 15:15)

I think if size is a big concern, the next step would be to switch from rle encoding to a different form of "compression" - my main concern here wasn't really size, I'm basically just trying not to use more space than needed with a basic rle and that could be used w/o too much impact during draw.

Improvements that might work would be to handle multi line spans or rects. But these will be way less efficient than implementing other image encoding mechanisms.

But that requires more logic in the inner loop which has quite some impact on performance. But if you unpack them at he start of a level, it should be too bad. Something LZW based would be my first suggestion.

Right now I'm pretty happy with the current implementation. It's quite easy to use (which is one of my main points, since I mainly code for relaxation). The only open point on my list is variable size images. Which shouldn't take too long. And then, I'll stop worrying about the tools and start using them :)
until i run against a new wall :)

P#79679 2020-07-20 15:40

That makes sense, I was just interested in seeing if I could ensure that the algorithm could be used reliably for general purposes without worrying about string sizes blowing up if you run into something too detailed. I'm working on an idea right now to make the algorithm handle 2 differently-colored pixels per character in some cases, including dithering, which should largely ensure against that, but that's made the token count go up to about 160.

I also wanted to see if I could optimize this a bit more, since with a bit of improvement it could probably just replace the logo-rendering system I recently posted, as it's a lot simple to setup and use.

I'm sure it's possible to make something more space-efficient if CPU time is of no concern, but the ability to run things in real time just adds so much flexibility, it's like having a virtual spritesheet, and if the function is small and has a relatively low token count, it can easily be added to a lot of projects.

P#79682 2020-07-20 16:09 ( Edited 2020-07-20 16:24)

I like your approach a lot. And I hope to learn a thing or two from it :)

I think the code is now really "good enough" for me. I can render a couple of pretty large sprites w/o having to worry about cpu and memory too much, even when flipping them.
The flip function might also come handy when I have symmetry, to reduce memory even more.

P#79683 2020-07-20 17:16

I agree, even if nobody worked on this any more, this is a pretty cool tool we got here now.

I guess I just like to fiddle with and optimize things, and learn while doing it. That's part of the reason one of my projects was fitting a whole game including an animated title screen in about 700 characters =p.

Cart #cbex_01-1 | 2020-05-22 | Code ▽ | Embed ▽ | No License

P#79688 2020-07-20 18:56

Thanks fir the trifill function, ill check it out asap. Been a little swamped by things but i managed to fix the lobg standing picodraw bug and implement bubary steing compression. As soon as the triangle is in I can wrote some documentation and have you guys test it out

P#79845 2020-07-24 10:43

Sounds good, I look forward to that.

Before you add the trifill function to Picodraw, though, here's an updated, improved version. The diagonal edges it produces now match up perfectly with Pico-8's line function, it takes about 15% less cpu power, and token count is down from 76 to 63.=)

function trifill(x1,y1,x2,y2,c)
 local inc=sgn(y2-y1)
 local fy=y2-y1+inc/2
 for i=inc\2,fy,inc do
P#79922 2020-07-25 15:51 ( Edited 2020-07-26 08:16)

In order to allow myself and others to more easily add RLE sprite capability to various projects, I've come up with a new version that combines the performance advantage of pre-conversion to tables with my minified approach. It integrates all features besides palette swapping into just two small functions of much lower token count, while also including selectable transparency, real-time selectable horizontal and vertical flipping, and an image encoder that works right in Pico-8.

The basic functionality, with identical or slightly better cpu usage and compressed string size to Spellcaster's latest revision, takes just 104 tokens, and the draw function includes a new transparency parameter. If no argument is specified, all colors will be drawn, but if a value is entered, that color will be transparent.

function s2t(s)
 local t={}
 for i=2,#s,2 do
 return t

function rlespr(t,x0,y0,tr)
 local x,y,mw=x0,y0,x0+t[1]
  for i=3,#t do
   local v=t[i]
   local col,len=v\256,v&255
   if(col!=tr) line(x,y,x+len-1,y,col)
  if(x>mw) x=x0 y+=1

For 161 tokens, you get full horizontal and vertical flipping, which work just like they do with Pico-8's built-in sprite function. There's a very slight increase in cpu load vs. the basic function, but it's comparable to Spellcaster's flip-capable code (and if anyone's interested, I made a version with no performance hit for 18 tokens more). While running the cart, just press the X and O buttons to flip the images on each axis.

To encode a new image, just import, paste or draw your image in the spritesheet (if it's not full screen stick it in the top left corner), specify the width and height, then run the cart and follow the onscreen prompts to encode it into a string and post it to the clipboard.

Cart #rlesprite_01-0 | 2020-07-27 | Code ▽ | Embed ▽ | No License

P#79993 2020-07-27 14:45 ( Edited 2020-08-27 03:32)

As promised, I released my bg drawing tool here


as a work in progress

P#81236 2020-08-27 02:27

@JadeLombax - That's pretty awesome! :)

P#81245 2020-08-27 08:24


Thanks. I'm working on a project where I need an effective image and map decompressor with the smallest footprint possible, and working on this helped a lot.

P#81253 2020-08-27 12:22 ( Edited 2020-08-27 12:23)

[Please log in to post a comment]

Follow Lexaloffle:        
Generated 2021-01-16 15:33 | 0.280s | 2097k | Q:135