Can I fix the Pixl.js font?

Posted on
  • Just got my pixl.js and the first thing I notice was how poorly the font renders. I took the clock example, added two buttons to increase/decrease the font size to see if it was just a scaling problem. Nope: the font looks pretty pixelated no matter what size you use.

    I did a quick browser through the github but couldn't find the 'font resource' that defines the font used when you call setFontVector (which I assume is the right way to set the font?)

    Clearly this is a vector based solution, so there will be pixelating artifacts but my guess is that even cleaning up the vector a bit should help the font scaling quite a bit.

    Either way, i'd like to help. What is the right files to be playing with if I'd like to improve the font legibility?

  • Hi Scott,

    That'd be great! Yes, the vector font is a bit of a pain - it was an attempt to balance flash memory usage with visual quality, but at the end of a day it was made with an algorithm and there wasn't as much effort put in as there could have been :)

    To add to this at one point in the past the font had quite a few more points in it, but then I had to make it lower resolution because on some platforms I was running out of flash memory.

    We could potentially create a new font with higher quality and use it on platforms that do have enough memory though.

    I guess you figured out that there's a built-in (small) bitmap font as well, and you can drag in other bitmap fonts if you need? http://www.espruino.com/Fonts

    The actual vector font is at:

    https://github.com/espruino/Espruino/blo­b/master/libs/graphics/vector_font.h

    and the rendering code is:

    https://github.com/espruino/Espruino/blo­b/master/libs/graphics/graphics.c#L416

    As far as I recall:

    • The data is in a .h file - there's no good reason for that :)
    • vectorFonts[ch - vectorFontOffset] contains the width and the number of vertices. We just iterate through summing the vertex count to get the actual offset in vectorFontPolys (rather than using a 16 bit index for each character)
    • vectorFontPolys contains the vertices as 7 bit unsigned X/Y pairs. It's just a series of regular polygons, and the top bit of 'Y' marks when one polygon ends
    • The actual font is only 96 high out of the 128 value range to allow for some overshoot - eg on W where the font is wider than it is high.
    • The font code is scattered with IN_FLASH_MEMORY and READ_FLASH_UINT8 defines - these are just for the ESP8266 as it hates reading unaligned data from flash memory.

    The code I used to generate it isn't shared as it relies on a whole bunch of code I had lying around from another project that may not be shareable. I could send it to you personally but it's obviously not doing such a good job, so it may be better to just start from scratch.

  • Yeah, i was hoping, as a UX designer, this was a 'data cleanup' job. However, as I've played with this a further, it's clear a fairly complex programming task with two separate tasks:

    1. The vectors can't increase in size due to ram limits
    2. The rendering code appears to round poorly

    A quick fix would be to try smaller 'angular vector' replacement. It would be ugly but at least clean(er). The rounding code appears like a slippery slope problem. My guess would be to render pixels to a greyscale value for edge cases and test clamping to Black/White pixels to various values.

    Sorry for the noob question but is there is page detailing how to build the Espruino core and how best to download it to the device? I appreciate this is a dawdle for you but as someone drawn to the Espruino due to it's ease of programming, "replacing the image" feels like a big step!

  • A more angular font would be a great idea!

    I'm not sure I understand about the greyscale value? On something like the Pixl sadly the LCD will only handle black or white (there is a hack to get 1 level of greyscale, but it uses loads of power & CPU).

    I guess one option would be to make a bit of JavaScript to render the existing font data (or to tweak the firmware such that we could have custom vector fonts), and then you could iterate quite quickly with different data?

    var VECTOR_FONT_POLY_SEPARATOR = 128;
    
    function drawChar(c, x, y, scale) {
      var poly = [];
      for (var i=0;i<c.length;i+=2) {
        poly.push(x+(c[i]&127)*scale,y+(c[i+1]&1­27)*scale);
        if (c[i+1]&VECTOR_FONT_POLY_SEPARATOR) {
          g.fillPoly(poly);
          poly = [];
        }
      }
    }
    
    var char = [
    // Character code 50
    	16,34,
    	23,21,
    	19,12,
    	10,21,
    	7,32|VECTOR_FONT_POLY_SEPARATOR,
    	19,12,
    	23,21,
    	36,17|VECTOR_FONT_POLY_SEPARATOR,
    	19,86,
    	55,50,
    	44,47,
    	9,83,
    	4,96|VECTOR_FONT_POLY_SEPARATOR,
    	19,86,
    	4,96,
    	63,96,
    	63,86|VECTOR_FONT_POLY_SEPARATOR,
    	62,39,
    	61,23,
    	51,27,
    	44,47,
    	55,50|VECTOR_FONT_POLY_SEPARATOR,
    	36,17,
    	51,27,
    	61,23|VECTOR_FONT_POLY_SEPARATOR,
    	53,13,
    	32,8,
    	19,12,
    	36,17,
    	61,23|VECTOR_FONT_POLY_SEPARATOR,
    ];
    
    g.clear();
    drawChar(char, 0,0, 0.6);
    drawChar(char, 64,0, 0.3);
    drawChar(char, 96,0, 0.2);
    drawChar(char, 96,32, 0.1);
    g.flip();
    

    For instance if you try the above, it'll print 2 a few times at different sizes. You can see the annoying 'pip' on the top of it, and that's the highest point (which has a y value of 8). If you make that 12 you can get rid of the pip, but then it's flat on top.

    As far as I know, the fillPoly and line handling is pretty standard - but I'd love to be proven wrong. If it could be improved I'd love to tweak it.

    As far as building, check out https://github.com/espruino/Espruino/blo­b/master/README_Building.md#for-nordic-s­emiconductors-nrf51nrf52-series-devices - there's some info there. Realistically unless you want to buy an nRF52DK you'll have to update via Bluetooth LE, which will be slow.

    But there's no need to build it for Pixl.js itself. You can do a build for Linux or Raspberry Pi (or I think Mac OS) just by cloning the repo and doing make. You can compile in SDL graphics support, or you could just run a command-line script that created an offscreen buffer and then wrote it out to an image that you could then view in an image viewer. That'd make changes really easy to view.

  • If you built it for Linux/Mac/Pi/etc, something like this works just fine:

    var g = Graphics.createArrayBuffer(64,32,1);
    g.clear();
    g.setFontVector(32);
    g.drawString("Hi");
    for (var y=0;y<g.getHeight();y++) {
      var s = "";
      for (var x=0;x<g.getWidth();x++)
        s += " #"[g.getPixel(x,y)];
      console.log(s);
    }
    
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####
        ####               #####
        ####               #####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ########################    ####
        ########################    ####
        ########################    ####
        ########################    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
        ####               #####    ####
    
  • You're right, fillPoly gets the rounding totally wrong. Try this:

    g.drawPoly = function(p) {
      g.moveTo(p[p.length-2],p[p.length-1]);
      for (var i=0;i<p.length;i+=2)
        g.lineTo(p[i],p[i+1]);
    };
    
    function getSquare(r,x,y) {
      var poly = [];
      for (var i=0;i<4;i++) {
        var a = r + (i+0.5)*Math.PI*2/4;
        poly.push( 
            x + 20*Math.sin(a),
            y + 20*Math.cos(a)
          );
      }
      return poly;
    }
    
    g.clear();
    var r = 0.02;
    g.fillPoly(getSquare(r,32,32));
    g.drawPoly(getSquare(r,96,32));
    g.flip();
    

    That should be pretty easily fixable, and I bet it would improve the font rendering a lot.

    Edit: this is what's at fault: https://github.com/espruino/Espruino/blo­b/master/libs/graphics/graphics.c#L352

    By adding some rounding (+128) as you suggested matters can be improved a great deal. There's still a little more magic required though - in that little test I can get 3 edges of the square rendering right, but not all - so I'd definitely appreciate some input :)

    Also - thanks! This code has been around since 2014ish and nobody noticed fillPoly actually had rendering errors :)

  • Hey! I'm glad we're making progress here! (even if I haven't lifted a finger yet ;-)

    If I read you right, I could test the rounding issue in JS which would be great as I could try a range of chars, tweak the values quickly and then let you take care of all the messy firmware build stuff (another smiley)

    I think you got the gist of my rounding comment.+128 is clearly the halfway point but I suspect that tweaking that cut off value will also improve things. If you've ever used photoshop/gimp and had to adjust the antialiasing of the font rendering (crisp vs smooth), that's what this is all about. My hunch is that making the rounding conservative (e.g. only pixels >60% grey into black instead of 50%) might give you a better outcome.

    Let me know what works for you. If you think this is a simple C fix you can make in the rendering engine we might just be done. If you'd like me to tweak and test the rounding value in JS, happy to oblige.

    Scott

  • Looks like you've given me code to inspect and draw rectangle (both helpful) but not what I thought: a way to test clamping of low level drawing/filling.

    We could be done actually, but in case you want any help on the clamping value (not just 50%) let me know if I could do this in JS (even if slow)

  • My gut feeling is that if you do fillPoly and we assume a 50% clamping value, the exact same pixels should be filled around the edge as if you draw lines between all the points. At the moment that's definitely not happening - and even the slightly broken hacks I tried so far really improve font rendering.

    I reckon when I fix that, the fonts could still look better than they do - and at that point it'd be great if you could look at what needs changing on the actual vector fonts themselves. You could actually do that already by taking the code from http://forum.espruino.com/conversations/­321462/#14260812 and just drawing lines (which work) rather than fillPoly which doesn't.

    In terms of the clamping value - I'd love to be able to tweak that, but right now everything works without any antialiasing. I guess to do the clamping properly we'd need to have an antialiased renderer and then do the clamping as the final stage (effectively doing a normal render and then a threshold).

    Antialiased rendering would be amazing (especially for when people connect displays that can actually handle grayscale), but unfortunately it seems I'm incapable of creating algorithms for drawing aliased polygons properly so I don't have much faith in my ability to make them antialiased :)

    If someone wanted to contribute code for antialiased polys, lines and circles that would be absolutely amazing though ;)

  • OK, I think we're good. I'll wait for the next release and will flash my pixl with that and then start testing out the various vector chars. I'm hoping that your 'slightly broken hacks' will already make a noticeable improvement.

    With that new baseline, I'm happy to then revisit the current font vectors.

  • Ok, I've just fixed the polygon drawing in a less hacky way - if you do a firmware update with a 'cutting edge' build then it'll have the changes in.

    It's debatable whether it really is an improvement in font rendering, but at least it is doing the fill 'properly' now.

    Before:
                             ####                                                   
                             ####                                                   
                             ####                                                   
                             ####                                                   
                             ####                                                   
                             ####                                                   
                             ####                                                   
                             ####                                                   
             #########       ####   ######               ########                   
         ##############      #### ##########           ###########                  
        ################     #################        #############                 
       ##################    #######    ######      ######   ######                 
       #####        #####    ######       #####    #####      ######                
      #####         #####    ######       #####   #####        #####                
      #####         #####    #####        ######  #####         #####               
                    #####    #####         #####  #####          #                  
                    #####    ####          #####  #####                             
              ###########    ####          #####  #####                             
         ################    ####          #####  #####                             
        #########  ######    ####          #####  #####                             
       ######      ######    ####          #####  #####                             
      ######       ######    ####          ####   #####                             
      ######       ######    #####         ####   #####          ####               
      #####        ######    #####         ####   #####         ####                
      #####        ######    ######        ####    #####       #####                
      #####        ######    #######     ######     #####     #####                 
      ######    #########    #########  ######       ##############                 
      ###################    ################         ############                  
         ##########    ##    #### ##########          ###########                   
           ######       ##   ####   #                    #######                    
                                                           #          
    
    After:
                             ####                                                   
                             ####                                                   
                             ####                                                   
                             ####                                                   
                             ####                                                   
                             ####                                                   
                             ####                                                   
                             ####                                                   
           ############      ####  ########             ##########                  
         ###############     ################          ###########                  
        ################     #################       ##############                 
        #################    #########  #######     #######  #######                
       ######      ######    #######     ######    #######    ######                
       #####        #####    ######       ######   #####      #######               
      ######        #####    ######        #####  ######       ######               
                    #####    #####         #####  #####         ####                
                #########    #####         #####  #####                             
             ############    #####          ####  #####                             
        #################    #####          ####  #####                             
        ##########  #####    ####           ####  #####                             
       #########    ######   ####           ####  #####                             
      #######       ######   #####         #####  #####                             
      ######       #######   #####         #####  ######        #####               
      ######       #######   ######        #####   #####       ######               
      ######       #######   #######      #####    ######      #####                
       #####     #########   #########   ######     ########  ######                
       ######  ###########   ########## ######       ##############                 
       ###################   #################        ############                  
        ############  ####   ################         ############                  
          #########     ##   ####  ######               #########                   
                                                          ####                   
    

    So if you're up for it, the vector fonts could probably do with some tweaking.

    I guess another option is to revisit how the fonts are stored and drawn. We could move to a stroke-based font (in fact I believe there are open fonts already ) which may well scale to smaller sizes better?

    I could add the ability to create custom fonts by specifying a function that'd draw each character. It'd be a relatively easy addition and might make it easier to play with different fonts and drawing styles.

  • Hmm. Maybe a stroked font is the best bet. I just had a play - it's slow, but quite interesting:

    font = new Uint16Array(E.toArrayBuffer(atob("EIAKgA­VFhQjFSQQKRQoGCsUJEIAERcQGDEXMBhWAC0QEDB­FECgxER1IHw0jRCBSACERICwxETAvRRU8FDAUIBU­UFwwVDBsQGBQdHB80HDwhQCNEIkQkPCkwKSAoFCo­MJGIAVRUMKCEWKBQoGiQbHBsUGQwbDBUQFBgUIBU­oFjQWQBVMFFQWRSM8ITgnOCVAKUgoUCpUJFQmTCJ­EIGoBXRxcH1gbVBhQHkwfRCI8JDQpLCkcKBQrECU­MJwwhECAUIDAfNBk4GzgVNBQsFSQXIBUgGCQfLB5­AJEgpUClYKFwrXCQqAhUVEBQUFRgXGBUUGhAYOgA­tEiQRHBUUGhAeECMUJxwqJCwsMDoADRIUERwVJBo­oHigjJCccKhQsDDBCAiEaICUNHzQhNR8MIGoDNRU­0KBEgWCAiARUmECUMJBAlFCcUJQwoagARIFggIgA­RJQwmECUUJBAkWgBREAgwUgAlFRgUEBkMHAwhECQ­YKSQpLCg4KUAkRCFEHEAZOBQsFCQUUgAZGyAULBU­sKFIBERgQGhQVGBQgFDAVOBY8FEAaQBg8HzQdDCl­EKFIAFRRAFCgcNB08HkAdRCNEIkAkOCksKSAoFCs­QJQwkUgA1FgwiSCA1FTQoUgA9FBQVEBwUHyAbLBg­4HkAdRCNEIkAkOCksKSAoFCsQJQwkUgNBFTwUMBQ­oFRwUFBkQHhAiFCQcKSgpLCg4KkAnRCJEI0AdOBw­sHCgdHB8UHhAgUgBFFRwoDRREFFIAIRUUFxAVEBs­UGBwdLB44HEAiRCFEJ0AkPCkwKSAoFCsQJQwmDCA­QIhgdJBw0HzwZQBtAFTwUMBQgFFIDQRo8HDQhKCE­kIBgiEB8MGgwbEBUYFCQUKBU0FzwXQBhAITwkNCk­oKSAoFCoQJCIBER4MHxAeFB0QHBElDCYQJRQkECQ­iAREeDB8QHhQdEB0VJhAlDCQQJRQnFCUMKGIDURQ­QIVAoagERHVgfESNYIGIDERRQIRAoSgENGAwaEBU­UFBwULBU0FjgUPBo8GDgdNB8kHiQjJSQgKSQoKCs­kJG4ASR5EGTwZMBooGyQaIB0gIyQgLCQ4J0AhRCE­xGygaJB0kIyggLCVJGUQjRCBMJFQmXCNgHWAeXBh­YGlAVSBQ8FDAVJBYcFBQaEBkMHAwjECEUJxwkJCk­wKTwoSCtQJlQlTRlII0ggTCRKACUVBCglFUQqESI­4IFYAERUQKBEUNBVAFkQUSBpIGEQdQB40HhEeNB9­AHEQiSCFIJ0QkQCk0KRAoVgFJG0QVPBQ0FCQVHBc­UFRAYDB0MIBAmFCQcKSQpNCg8KkQkSCRWABEVECg­RFCwVOBdAFUQYSB1IIEQmQCQ4KSwpEChOABEVECg­RFEQWER4wHREpRChKABEVECgRFEQWER4wHFYBSRt­EFTwUNBQkFRwXFBUQGAwdDCAQJhQkHCkkKTQoPCp­EJEglSCE1IUggWgARFRAoSRVIKhEeSBwiABEVECh­CADEUMCcsJCgpICkYKBArDCQIJgggVgARFRAoSRY­QISUdSChGABEVECkRKUAoYgARFRAoERUwKFEVMCh­RFVAoWgARFRAoERVIKEkVSChaACUVHBcUFRAYDB0­MIBAmFCQcKSQpNCg8KkQkSCVMIEwdSBtEFTwUNBQ­kFFYAERUQKBEUNBVAFkQUSBtIGUQeQB80HxAcWgA­lFRwXFBUQGAwdDCAQJhQkHCkkKTQoPCpEJEglTCB­MHUgbRBU8FDQUJBUxJ0goVgARFRAoERQ0FUAWRBR­IGkgYRB1AHjQeEB4tHUgoUgNFFTwUMBQgFRQXDBU­MGxAYFB0cHzQcPCFAI0QiRCQ8KTApICgUKgwkQgA­hFSAoBRQ8FFoAERcQIhQkHCkoKTAoPCpEJ0ggSBR­KAAUVJChFFSQoYgAJFRwoMRUcKDEVRChZFUQoUgA­NFUQoRRUMKEoABRYkHSQoRRYkHFIARRUMKA0URBU­NKUQoOgAREBAwFRAUMBEQLBARMCwwOgABFDgsOgA­lECQwKRAoMA0QKBANMCgwQgIhEAAiIRBAIEoAATB­IMCIBFRsMGQweEB0UHBAdDBxOAz0ZPCo9HDQfLBs­gGBgeEB0MIwwiECQYKSApLCg0KjwkTgARFRAqERw­YHyAbLBg0HjwdQCNAIjwkNCksKSAoGCoQJEoCPRw­0HywbIBgYHhAdDCMMIhAkGCkgKSwoNCo8JE4APRU­8Kj0cNB8sGyAYGB4QHQwjDCIQJBgpICksKDQqPCR­KAQ0hPCM8HTgcNB8sGyAYGB4QHQwjDCIQJBgpICk­sKDQqPCQyACkUIBUYFBQZFCsJGyQYTgM9GzwqOC8­0LCwwIDMYLj0cNB8sGyAYGB4QHQwjDCIQJBgpICk­sKDQqPCROABEVECsRHBwfJBswGDgfPB08KCIADRU­QFBQXEBAMFxEZECgqABUVGBQcFxgQFBcZGBgvFCw­MMAQwRgARFRArORkQJSEhPCgiABEVECh6AxEZECs­RHBwfJBswGDgfPB08Kz0cSB9QG1wYZB9oHWgoTgM­RGRArERwcHyQbMBg4HzwdPChOAyEYGB4QHQwjDCI­QJBgpICksKDQqPCdAIUAiPBw0HywbIBhOAxEYEDI­RHBgfIBssGDQePB1AI0AiPCQ0KSwpICgYKhAkTgM­9GDwyPRw0HywbIBgYHhAdDCMMIhAkGCkgKSwoNCo­8JDYDERkQKREiFBwcHyQbMBhGAjkcNB8oGxwYEB4­MHBAhGCIsIzQhOCY4JDQpKCkcKBAqDCQyABUVFCQ­YKSApKCsJGyQYTgMRGRAkFCkcKSgoMCk8Jz0ZPCh­CAwkZICs5GSAoWgMNGRwrLRkcKy0ZPCtNGTwoRgM­NGTgrORkMKEIDCRkgKzkZICkYLxAsCDAEMEYDORk­MKw0bOBkNKTgoOgAlERwSGBAUFhQUGBkcGyAZIB8­YHR0TGBEYFxwUIBokGCQeIBwQIiAgJCYkJCApHCs­YKRgvHC0ZIyAhICccJBgqFCgULhgvHCwkMCIAERA­QMDoAFREcEiAQJBYkFCAZHBsYGRgfIB0dEyARIBc­cFBgaFBgUHhgcKCIYIBQmFCQYKRwrICkgLxwtISM­YIRgnHCQgKiQoJC4gLxwsFDBiAw0hDCIQHRgdIB4­oHTgiQCJIIVAjVB0NIxAeGB4gHygeOCNAI0giUCN­UHVQcQgABFQApBCgEFAgVCCkMKAwUEBUQKRQoFBQ­YFRgpHCgcFCAVICkkKCQUKBUoKSwoLBQwFTApNCg­0FDgVOCk8KDwUQBVAKAIA=")));
    
    function drawChar(c,ox,oy,s) {
      c-=32;
      if (c<0 || c>95) return 0; // no char
      var i = 0;
      while (c)if(font[++i]&32768) c--;
      var width = font[i++]&63;
      var f;
      while (!((f=font[i++])&32768)) {
        var x = (f&63)*s + ox;
        var y = ((f>>6)&63)*s + oy;
        if (f&16384) g.moveTo(x,y); else g.lineTo(x,y);
      }
      return width*s;
    }
    function drawString(str,x,y,s) {
      x=x||0;
      y=y||0;
      s=s?(s/22):1;
      str=""+str;
      y -= 20*s;
      for (var i=0;i<str.length;i++)
        x+=drawChar(str.charCodeAt(i),x,y,s);
    }
    g.clear();drawString("Hello",0,0,10);g.f­lip();
    

    It uses half the space of the current vector font, and it scales down really well:

     #   #      # #
     #   #      # #
     ##### #### # #  ####
     #   # #### # # #   #
     #   # #    # # #   #
     #   # ## # # #  # ##
     #   #  ##  # #   ##
     
     #      #           #   #
     #      #           #   #
     #      #           #   #
     #      #    ####   #   #    ####
     ########   #    #  #   #   #    #
     #      #   #    #  #   #   #     #
     #      #  #######  #   #   #     #
     #      #  #        #   #   #     #
     #      #   #    #  #   #   #    #
     #      #    ####   #   #    ####
    

    It's just a matter of making the lines bigger when the text is rendered bigger.

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

Can I fix the Pixl.js font?

Posted by Avatar for scottjenson @scottjenson

Actions