BPP and createArrayBuffer changing colours

Posted on
  • Hi All,
    I'm hoping someone can help me understand BPP better. I'm testing out a simple animation, based on the Slope Clock, to slide the current contents of the screen off, and show a new screen. I've had this running in the emulator, but some of the colours are wrong.

    It should show BLACK text (wibble) on GREEN, then replace it with WHITE text (flibble) on RED.
    When I take a screenshot of the screen with g.asImage, and put it in the image buffer with drawImage, the colour changes from GREEN to BLUE.

    I'm sure it's a simple fix somewhere, but I'm stuck!

    This is the code I've been trying:

    let g2 = Graphics.createArrayBuffer(g.getWidth() * 2, g.getHeight(), 8, {msb:true});
    let g2img = { width:g2.getWidth(), height:g2.getHeight(), bpp:8, buffer:g2.buffer };
    
    let R = Bangle.appRect;
    let x = R.w / 2;
    let y = R.y + R.h / 2;
    
    let animInterval;
    
    let animate = function(step) {
      if (animInterval) clearInterval(animInterval);
      slideX = -g2.getWidth()/2;
      animInterval = setInterval(function() {
        slideX += step;
        let stop = false;
        if (slideX>=0) {
          slideX=0;
          stop = true;
        }
        g.drawImage(g2img, slideX, 0);
        if (stop) {
          clearInterval(animInterval);
          animInterval=undefined;
        }
      }, 50);
    };
    
    g.reset().setBgColor("#0f0").clearRect(R­); // clear whole background
    g.setFontAlign(0, 0).setFont("Vector:30").drawString("WIBB­LE", g.getWidth()/2, g.getHeight()/2);
    let screengrab = g.asImage();
    
    // Draw to offscreen buffer
    g2.setColor("#f00").fillRect(0,0,g2.getW­idth(),g2.getHeight());
    g2.setColor("#fff").setFontAlign(0, 0).setFont("Vector:30").drawString("FLIB­BLE", g2.getWidth()/4, g2.getHeight()/2);
    g2.drawImage(screengrab, g2.getWidth()/2, 0);
    
    animate(3);
    

    3 Attachments

    • screenshot 1.png
    • screenshot 2.png
    • screenshot 3.png
  • I have had this exact problem on imageclock while working with 4 bit. Copying over my solution from there yields the correct result, but it seems a bit complicated. It essentially uses a palette to force the colors to be correct. I suspect there is an easier way.

        let colormap={
          "#000":0,
          "#00f":1,
          "#0f0":2,
          "#0ff":3,
          "#f00":4,
          "#f0f":5,
          "#ff0":6,
          "#fff":7
        };
    
        let palette = new Uint16Array([
          0x0000, //black #000
          0x001f, //blue [#00f](http://forum.espruino.com/search/­?q=%2300f)
          0x07e0, //green [#0f0](http://forum.espruino.com/search/­?q=%230f0)
          0x07ff, //cyan [#0ff](http://forum.espruino.com/search/­?q=%230ff)
          0xf800, //red [#f00](http://forum.espruino.com/search/­?q=%23f00)
          0xf81f, //magenta [#f0f](http://forum.espruino.com/search/­?q=%23f0f)
          0xffe0, //yellow [#ff0](http://forum.espruino.com/search/­?q=%23ff0)
          0xffff, //white [#fff](http://forum.espruino.com/search/­?q=%23fff)
          0xffff, //white
          0xffff, //white
          0xffff, //white
          0xffff, //white
          0xffff, //white
          0xffff, //white
          0xffff, //white
          0xffff, //white
        ]);
    
        let g2 = Graphics.createArrayBuffer(g.getWidth() * 2, g.getHeight(), 4, {msb:true});
        let g2img = { width:g2.getWidth(), height:g2.getHeight(), bpp:4, buffer:g2.buffer, palette: palette };
        let R = Bangle.appRect;
        let x = R.w / 2;
        let y = R.y + R.h / 2;
        let animInterval;
        let animate = function(step) {
          if (animInterval) clearInterval(animInterval);
          slideX = -g2.getWidth()/2;
          animInterval = setInterval(function() {
            slideX += step;
            let stop = false;
            if (slideX>=0) {
              slideX=0;
              stop = true;
            }
            g.drawImage(g2img, slideX, 0);
            if (stop) {
              clearInterval(animInterval);
              animInterval=undefined;
            }
          }, 50);
        };
        g.reset().setBgColor("#0f0").clearRect(R­); // clear whole background
        g.setFontAlign(0, 0).setFont("Vector:30").drawString("WIBB­LE", g.getWidth()/2, g.getHeight()/2);
        let screengrab = g.asImage();
        // Draw to offscreen buffer
        g2.setColor(colormap["#f00"]).fillRect(0­,0,g2.getWidth(),g2.getHeight());
        g2.setColor(colormap["#fff"]).setFontAli­gn(0, 0).setFont("Vector:30").drawString("FLIB­BLE", g2.getWidth()/4, g2.getHeight()/2);
        g2.drawImage(screengrab, g2.getWidth()/2, 0);
        animate(3);
    

    Edit: The forum search links in the comments are somehow autogenerated from the #colorvalues

  • Thanks for your help @halemmerich, that does work, but I agree it feels like there could be a simpler solution.
    Searching for 'BPP' in the forum (should have done that before I posted, sorry), shows quite a few people have had a similar problem.
    It all seems to stem from g.asImage() giving a 3 bpp buffer, but if I try to createArrayBuffer with 3 bpp it says: Uncaught Error: Invalid BPP.

  • Ok, just to try and strip this down to the actual problem, it's this?

    let g2 = Graphics.createArrayBuffer(g.getWidth() * 2, g.getHeight(), 8, {msb:true});
    let g2img = { width:g2.getWidth(), height:g2.getHeight(), bpp:8, buffer:g2.buffer };
    
    g.reset().setBgColor("#0f0").clear();
    // Screen is GREEN
    let screengrab = g.asImage();
    // Draw to offscreen buffer and back to screen
    g2.drawImage(screengrab, 0, 0);
    g.drawImage(g2img, 0, 0);
    // Screen is BLUE
    

    The issue is that when drawing a 3bpp image onto a 8bpp image, Espruino doesn't really know what to do about mapping colours - so it just copies the numeric color value straight across.

    As @halemmerich noted, the best method is really to try and use a 4bpp graphics (which saves a bunch of memory over 8bpp), and to then explicitly tell Espruino what color palette to use for it (because normally for 4bpp it uses the Mac color palette).

    His code works great, but you can also use toColor to avoid the cryptic constants:

    let g2 = Graphics.createArrayBuffer(g.getWidth() * 2, g.getHeight(), 4, {msb:true});
    let g2img = { 
      width:g2.getWidth(), 
      height:g2.getHeight(), 
      bpp:4, 
      buffer:g2.buffer,
      palette : new Uint16Array([
          g.toColor("#000"),
          g.toColor("#00f"),
          g.toColor("#0f0"),
          g.toColor("#0ff"),
          g.toColor("#f00"),
          g.toColor("#f0f"),
          g.toColor("#ff0"),
          g.toColor("#fff"),
          0,0,0,0,0,0,0,0,
        ])};
    
    g.reset().setBgColor("#0f0").clear();
    // Screen is GREEN
    let screengrab = g.asImage();
    // Draw to offscreen buffer and back to screen
    g2.drawImage(screengrab, 0, 0);
    g.drawImage(g2img, 0, 0);
    // Screen is GREEN still
    

    I've just added some color mappings from 3->4 and 3->8 bit into the Espruino firmware, and this should fix your original code on 8 bit, and will work on 4 bit too

    ... but then it'll fail on older firmwares. If you use my or @halemmerich's code then at least it'll be backwards compatible.

    It's also worth adding that g2.setColor(colormap["#f00"]) isn't needed. g2.setColor("#f00") should work just fine - it'll search the color palette to find the closest matching color

  • ... and yes, having a 3bpp arraybuffer graphics would probably be preferable for memory usage and for ensuring a nice clean color mapping.

    But doing unaligned 3 bit memory accesses is not super easy (or fast), so right now ArrayBuffer graphics only supports BPPs that are byte-aligned (1,2,4,8,16,24,32).

  • Thanks for explaining that Gordon, I think I understand better now. The new color mapping will be helpful to make sure no one else trips up on this in the future, but you're right, I'll do it with a palette so it works more broadly.

  • So now I've got colours working correctly (thanks again Hal and Gordon), I need to work on speed. See code below, but in the emulator, doing a full screen wipe takes ~6ms/frame. Exactly the same on the actual B2 takes ~228ms/frame, i.e. very very slow.

    Is there anything I can do to speed this up, or is that just how long it takes to draw the whole screen? Doing only part of the screen definitely speeds things up, but not enough to look smooth.

    let colormap={
      0: 0,
      31: 1,
      2016: 2,
      2047: 3,
      63488: 4,
      63519: 5,
      65504: 6,
      65535: 7
    };
    
    let palette = new Uint16Array([
      g.toColor("#000"),
      g.toColor("#00f"),
      g.toColor("#0f0"),
      g.toColor("#0ff"),
      g.toColor("#f00"),
      g.toColor("#f0f"),
      g.toColor("#ff0"),
      g.toColor("#fff"),
      0,0,0,0,0,0,0,0
    ]);
    
    let animInterval;
    let time_array = []
    
    let animate = function(step) {
      if (animInterval) clearInterval(animInterval);
      slideX = -R.w;
      animInterval = setInterval(function() {
        let time_start = getTime()
        slideX += step;
        let stop = false;
        if (slideX>=0) {
          slideX=0;
          stop = true;
        }
        g.drawImage(g2img, slideX, R.y);
        if (stop) {
          clearInterval(animInterval);
          animInterval=undefined;
          print(time_array)
          print(time_array.reduce((a, b) => a + b, 0) / time_array.length)
        }
        time_array.push(Math.round((getTime() - time_start)*1000))
      }, 20);
    };
    
    Bangle.loadWidgets();
    Bangle.drawWidgets();
    let R = Bangle.appRect;
    // R.h = 50
    let g2 = Graphics.createArrayBuffer(R.w * 2, R.h, 4, {msb:true});
    let g2img = { width:g2.getWidth(), height:g2.getHeight(), bpp:4, buffer:g2.buffer, palette: palette };
    g2.setBgColor(colormap[g.theme.bg]);
    g2.setColor(colormap[g.theme.fg]);
    
    g.setBgColor("#F00").clearRect(R);
    g.setColor("#0F0").setFont('Vector:15').­setFontAlign(0, 0).drawString("OLD SCREEN", R.w/2, R.h/2);
    let screengrab = g.asImage();
    g2.drawImage(screengrab, R.w - R.x, -R.y);
    g2.clearRect(R);
    g2.setFont('Vector:15').setFontAlign(0, 0).drawString("NEW SCREEN", R.w/2, R.h/2);
    
    animate(4);
    
  • Which bit is slow for you? The drawImage in the animate function?

    It's not helping that you have an image double the size of the screen - and it'll be iterating through every pixel (even the ones that are offscreen) I think.

    You could have just a single fullscreen graphics (for the new screen) and could then use g.scroll to scroll the existing screen contents off, and then draw the new graphics on? That could help by a factor of 2 or so I guess.

    It seems slightly counter-intuitive, but you could also use http://www.espruino.com/Reference#l_Grap­hics_drawImages to draw just the single image, but just in the small area that's new.

    ... so for instance if you scroll by 8 pixels, you do g.scroll(-8,0) and then g.drawImages([{x:R.x-8, y:0, ...}], {x:R.w-8,y:0,width:8,height:R.h})

    It's slightly slower than drawImage but it allows you to render just an area of the image, which could help in this case

  • Thanks for your suggestions Gordon, I tried them all, but in the end redrawing the entire screen was too slow to look good. Your g.scroll and g.drawImages solution was the best, but moved the widgets as well which wasn't ideal.
    I've abandoned the idea of a general function to wipe to a different screen, and opened https://github.com/espruino/BangleApps/p­ull/2547 with the much simpler solution of just clearing the screen and redrawing the text for this clock. I'll have to come up with some more imaginative animations for my other clocks!

  • Your g.scroll and g.drawImages solution was the best, but moved the widgets as well which wasn't ideal.

    Ahh - you can use g.setClipRect to fix that :)

    Oh well, glad you got something workable anyway

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

BPP and createArrayBuffer changing colours

Posted by Avatar for Sir_Indy @Sir_Indy

Actions