TinyCircuits / TinyCircuits-Thumby-Code-Editor

https://code.thumby.us/
GNU General Public License v3.0
28 stars 19 forks source link

Bitmap formatting for readability #3

Closed ace-dent closed 1 year ago

ace-dent commented 3 years ago

It would be great if the output bitmap 'array' (tuple) could be formatted to visually indicate the data structure / image. e.g.

# BITMAP: width: 8, height: 8
bitmapHero = (
    0,  # ▓▓▓▓▓▓▓▓
    0,  # ▓▓▓▓▓▓▓▓
   16,  # ▓▓▓░▓▓▓▓
  150,  # ░▓▓░▓░░▓
  254,  # ░░░░░░░▓
    0,  # ▓▓▓▓▓▓▓▓
    0,  # ▓▓▓▓▓▓▓▓
    0   # ▓▓▓▓▓▓▓▓
 )

Hex values (0xF4, #..., etc.) would be preferable for compatibility with Arduino IDE or other C++ systems (Arduboy). I appreciate this works best for small (height 8px /1byte or less) images. Perhaps add it as an option for 'verbose' / 'detailed' output?

raymond-li commented 3 years ago

I think this is a pretty cool idea! I played around with the concept and came up with some Python code that can spit out a similar format. It squashes the rows to half the original size so you lose information, but it can show an unrotated preview and follows the bitmap format a little better, I think. It definitely suits larger sprites better than smaller sprites like your example.

My take:

# Formats a Thumby bitmap so a rough image of it can be seen as a comment next to the bitmap data.
import re
import math

def unicode_braille_from_byte(b):
    # https://en.wikipedia.org/wiki/Braille_Patterns#Identifying,_naming_and_ordering
    # Braille dot numbering
    # 1 4
    # 2 5
    # 3 6
    # 7 8
    # Dot to binary index:
    # Dot: 8 7 6 5 4 3 2 1
    # Bin: 7 6 5 4 3 2 1 0
    # Unicode Braille Block base offset: 0x2800
    # 2 dots if white, 0 dots (whitespace) for black. Flip the logic in the b_list code if you want to invert
    braille_block_base = 0x2800
    braille_block_offset = 0
    b_list = reversed([True if e == '1' else False for e in list(f"{b:08b}")])
    row_to_dot_map = { # Essentially squashes the 1x8 image to fit in 1x4, biased towards displaying a dot if either row is white
        0: (1,4),
        1: (1,4),
        2: (2,5),
        3: (2,5),
        4: (3,6),
        5: (3,6),
        6: (7,8),
        7: (7,8),
    }
    dot_to_bin_map = {
        1: 0b00000001,
        2: 0b00000010,
        3: 0b00000100,
        4: 0b00001000,
        5: 0b00010000,
        6: 0b00100000,
        7: 0b01000000,
        8: 0b10000000,
    }
    # Kind of space inefficient but maybe easier to understand as a prototype?
    for r, bit in enumerate(b_list):
        if bit:
            for dot_num in row_to_dot_map[r]:
                braille_block_offset = braille_block_offset | dot_to_bin_map[dot_num]
    return chr(braille_block_base + braille_block_offset)

def parse_width_height(data):
    s = data["meta"]
    pattern = re.compile("#.+width\s*:\s*(\d+)\s*,\s*height\s*:\s*(\d+)")
    matches = re.match(pattern, s)
    width_s = matches[1]
    height_s = matches[2]
    return int(width_s), int(height_s)

def parse_bitmap(data):
    s = data["bitmap"]
    s_split = s.split('=')
    name = s_split[0].strip()
    bitmap_s = s_split[1].strip(' )(').split(',')
    bitmap = [int(i) for i in bitmap_s]
    return name, bitmap

def format_bitmap_with_preview_v2(data):
    # Data is formatted so each byte represents 1x8 (WxH), with the least significant bit as the top bit in the y direction.
    ROWS_PER_COL_BYTE = 8
    width, height = parse_width_height(data)
    bitmap_name, bitmap = parse_bitmap(data)
    value_padding = 3 # 0-255
    num_row_groups = math.ceil(height / ROWS_PER_COL_BYTE)

    formatted = f"{bitmap_name} = (\n"
    for row_group_i in range(num_row_groups):
        # New line of data
        row_str = "  " # Indent
        orig_row = ""
        row_preview = ""
        for col_i in range(width):
            v = bitmap[row_group_i * width + col_i]
            byte_preview = unicode_braille_from_byte(v)
            row_preview += byte_preview
            orig_row += f"{v:>{value_padding}}," # TODO: padding to the right
        # Surround the sides of the bitmap with '|' characters because whitespace is important
        row_str += f"{orig_row} #|{row_preview}"
        formatted += f"{row_str}|\n"
    formatted += ")"
    return formatted

def test_4x4():
    # BITMAP: width: 4, height: 4
    data = {
        "meta": "# BITMAP: width: 4, height: 4",
        "bitmap": "bitmap1 = (7,11,13,14)",
    }
    result = format_bitmap_with_preview_v2(data)
    print(result)

def test_bitmapHero():
    data = {
        "meta": "# BITMAP: width: 8, height: 8",
        "bitmap": "bitmapHero = (0,0,16,150,254,0,0,0)",
    }
    result = format_bitmap_with_preview_v2(data)
    print(result)

# Took this sprite from CoolieCoolster on the Thumby forum to show how larger sprites are clearer
def test_coolie_titlescreen():
    data = {
        "meta": "# BITMAP: width: 72, height: 40",
        "bitmap": """bitmap0 = (0,254,2,2,2,226,34,98,194,130,226,34,226,2,226,34,162,162,162,34,226,2,226,34,98,194,130,226,34,226,2,226,34,162,162,162,34,226,2,226,34,162,162,162,226,2,226,34,162,162,162,34,226,2,226,34,226,2,2,2,2,226,34,162,162,162,226,2,2,2,254,0,
            0,255,0,0,0,63,32,62,12,25,51,32,63,0,63,32,61,5,61,32,63,0,63,32,62,12,25,51,32,63,0,63,32,47,47,47,32,63,0,63,32,47,40,40,56,0,63,32,61,5,61,32,63,0,63,32,47,40,40,56,0,63,32,47,40,40,56,0,0,0,255,0,
            0,255,0,0,0,0,0,0,0,0,0,0,0,0,96,144,140,114,0,192,48,128,96,0,0,0,0,112,140,130,66,4,0,96,144,136,72,48,0,96,144,136,72,48,0,128,96,24,6,128,96,16,4,0,96,144,168,40,16,0,0,0,0,0,0,0,0,0,0,0,255,0,
            0,255,0,0,0,0,0,192,64,192,0,192,64,192,0,192,64,64,0,196,67,64,0,192,64,64,0,0,0,192,64,192,0,0,0,64,192,64,0,192,64,192,0,0,0,192,64,192,0,192,64,64,0,192,64,64,0,64,192,64,0,192,128,0,192,0,0,0,0,0,255,0,
            0,127,64,64,64,64,64,71,65,65,64,71,65,70,64,71,69,68,64,69,69,71,64,69,69,71,64,64,64,71,65,71,64,64,64,64,71,64,64,71,68,71,64,64,64,71,69,70,64,71,69,68,64,71,68,71,64,68,71,68,64,71,64,67,71,64,64,64,64,64,127,0)""",
    }
    result = format_bitmap_with_preview_v2(data)
    print(result)

test_4x4()
test_bitmapHero()
test_coolie_titlescreen()

Output. The 3rd sprite is very wide but is more interesting. Also, GitHub doesn't do fixed-width fonts for code unless you enable it in your settings so it might look funny:

bitmap1 = (
    7, 11, 13, 14, #|⠛⠛⠛⠛|
)
bitmapHero = (
    0,  0, 16,150,254,  0,  0,  0, #|⠀⠀⠤⣿⣿⠀⠀⠀|
)
bitmap0 = (
    0,254,  2,  2,  2,226, 34, 98,194,130,226, 34,226,  2,226, 34,162,162,162, 34,226,  2,226, 34, 98,194,130,226, 34,226,  2,226, 34,162,162,162, 34,226,  2,226, 34,162,162,162,226,  2,226, 34,162,162,162, 34,226,  2,226, 34,226,  2,  2,  2,  2,226, 34,162,162,162,226,  2,  2,  2,254,  0, #|⠀⣿⠉⠉⠉⣭⠭⣭⣉⣉⣭⠭⣭⠉⣭⠭⣭⣭⣭⠭⣭⠉⣭⠭⣭⣉⣉⣭⠭⣭⠉⣭⠭⣭⣭⣭⠭⣭⠉⣭⠭⣭⣭⣭⣭⠉⣭⠭⣭⣭⣭⠭⣭⠉⣭⠭⣭⠉⠉⠉⠉⣭⠭⣭⣭⣭⣭⠉⠉⠉⣿⠀|
    0,255,  0,  0,  0, 63, 32, 62, 12, 25, 51, 32, 63,  0, 63, 32, 61,  5, 61, 32, 63,  0, 63, 32, 62, 12, 25, 51, 32, 63,  0, 63, 32, 47, 47, 47, 32, 63,  0, 63, 32, 47, 40, 40, 56,  0, 63, 32, 61,  5, 61, 32, 63,  0, 63, 32, 47, 40, 40, 56,  0, 63, 32, 47, 40, 40, 56,  0,  0,  0,255,  0, #|⠀⣿⠀⠀⠀⠿⠤⠿⠒⠿⠭⠤⠿⠀⠿⠤⠿⠛⠿⠤⠿⠀⠿⠤⠿⠒⠿⠭⠤⠿⠀⠿⠤⠿⠿⠿⠤⠿⠀⠿⠤⠿⠶⠶⠶⠀⠿⠤⠿⠛⠿⠤⠿⠀⠿⠤⠿⠶⠶⠶⠀⠿⠤⠿⠶⠶⠶⠀⠀⠀⣿⠀|
    0,255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 96,144,140,114,  0,192, 48,128, 96,  0,  0,  0,  0,112,140,130, 66,  4,  0, 96,144,136, 72, 48,  0, 96,144,136, 72, 48,  0,128, 96, 24,  6,128, 96, 16,  4,  0, 96,144,168, 40, 16,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,255,  0, #|⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⣤⣒⣭⠀⣀⠤⣀⣤⠀⠀⠀⠀⣤⣒⣉⣉⠒⠀⣤⣤⣒⣒⠤⠀⣤⣤⣒⣒⠤⠀⣀⣤⠶⠛⣀⣤⠤⠒⠀⣤⣤⣶⠶⠤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀|
    0,255,  0,  0,  0,  0,  0,192, 64,192,  0,192, 64,192,  0,192, 64, 64,  0,196, 67, 64,  0,192, 64, 64,  0,  0,  0,192, 64,192,  0,  0,  0, 64,192, 64,  0,192, 64,192,  0,  0,  0,192, 64,192,  0,192, 64, 64,  0,192, 64, 64,  0, 64,192, 64,  0,192,128,  0,192,  0,  0,  0,  0,  0,255,  0, #|⠀⣿⠀⠀⠀⠀⠀⣀⣀⣀⠀⣀⣀⣀⠀⣀⣀⣀⠀⣒⣉⣀⠀⣀⣀⣀⠀⠀⠀⣀⣀⣀⠀⠀⠀⣀⣀⣀⠀⣀⣀⣀⠀⠀⠀⣀⣀⣀⠀⣀⣀⣀⠀⣀⣀⣀⠀⣀⣀⣀⠀⣀⣀⠀⣀⠀⠀⠀⠀⠀⣿⠀|
    0,127, 64, 64, 64, 64, 64, 71, 65, 65, 64, 71, 65, 70, 64, 71, 69, 68, 64, 69, 69, 71, 64, 69, 69, 71, 64, 64, 64, 71, 65, 71, 64, 64, 64, 64, 71, 64, 64, 71, 68, 71, 64, 64, 64, 71, 69, 70, 64, 71, 69, 68, 64, 71, 68, 71, 64, 68, 71, 68, 64, 71, 64, 67, 71, 64, 64, 64, 64, 64,127,  0, #|⠀⣿⣀⣀⣀⣀⣀⣛⣉⣉⣀⣛⣉⣛⣀⣛⣛⣒⣀⣛⣛⣛⣀⣛⣛⣛⣀⣀⣀⣛⣉⣛⣀⣀⣀⣀⣛⣀⣀⣛⣒⣛⣀⣀⣀⣛⣛⣛⣀⣛⣛⣒⣀⣛⣒⣛⣀⣒⣛⣒⣀⣛⣀⣉⣛⣀⣀⣀⣀⣀⣿⠀|
)
ace-dent commented 3 years ago

@raymond-li Nice idea. I've experimented previously with the braille unicode hack, but find it's too inconsistently displayed. Often the spaces between braille character blocks make the overall image incongruous. In my experience, the simpler byte representation with blocks is more consistent across fonts and platforms. e.g. https://github.com/filmote/Font4x6/blob/519060bc6f321c2a0c511e4e415cc55acb1c31c1/src/fonts/Font4x6.cpp#L33

raymond-li commented 3 years ago

@ace-dent , yeah the braille doesn't look great, especially for smaller sprites.

The issue I see with the inline blocks your describe is that it doesn't really work inline if the thumby sprite data is taller than 8 pixels, since each byte of the sprite data represents up to 8 rows of pixels. The other downside, to me, is that the inline block representation means the image is rotated.

If you forget about trying to put the preview inline, you can just have it printed before the sprite, which looks the best to me, but that could be unwieldy for large sprites and doubles the number of lines, which I wouldn't be that opposed to, personally. For larger projects, you'd probably want to put your sprites into a separate file anyways, although the Web IDE doesn't support multi-file projects yet, I don't think. Example:

# ▓▓▓▓▓▓▓▓
# ▓▓▓░░▓▓▓
# ▓▓▓░░▓▓▓
# ▓▓▓▓░▓▓▓
# ▓▓░░░▓▓▓
# ▓▓▓▓░▓▓▓
# ▓▓▓▓░▓▓▓
# ▓▓▓░░▓▓▓
# BITMAP: width: 8, height: 8
bitmapHero = (
    0,
    0,
   16,
  150,
  254,
    0,
    0,
    0 
)

Another problem is that the Web IDE/Emulator doesn't like these unicode characters in the comments. I get this:

import thumby

print("This script fails because of the unicode comment below")

# ▓▓▓▓▓▓▓▓ 

print("This line fails because of the unicode comment above")
import main
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "main.py", line 7
SyntaxError: invalid syntax
Emulator stopped

Maybe there's a way to work around it by putting a specific character after the unicode comment. Not sure.

ace-dent commented 1 year ago

I'm not sure how relevant this feature request is... general python style is for quite compact code. Feel free to Close.

ghost commented 1 year ago

I don't think we'll ever do this, unfortunately. Although cool, too much work to implement and it could cause additional editor lag.

See this page for an example of an editor that does this but causes lag: page