Graphics.drawImages: add 'compose' and 'palette' operators to allow one image to be composed on another (for instance XOR on background)

This commit is contained in:
Gordon Williams 2024-05-10 15:47:29 +01:00
parent ac4cb54c1b
commit e390e5c46d
4 changed files with 152 additions and 35 deletions

View File

@ -43,6 +43,7 @@
Graphics: fix endianness of palette when used with `g.asImage`
Fix issue using String.replace on flash-based (read only) strings
Bangle.js2: Stop Bangle.setLCDBrightness turning the LCD backlight on
Graphics.drawImages: add 'compose' and 'palette' operators to allow one image to be composed on another (for instance XOR on background)
2v21 : nRF52: free up 800b more flash by removing vector table padding
Throw Exception when a Promise tries to resolve with another Promise (#2450)

View File

@ -109,6 +109,21 @@ void _jswrap_graphics_freeImageInfo(GfxDrawImageInfo *info) {
jsvUnLock(info->buffer);
}
/** Parse the palette data from an image. Return 'palette' so it can be unlocked if needed. info->palettePtr=0 on failure */
JsVar *_jswrap_graphics_parseImage_palette(GfxDrawImageInfo *info, JsVar *palette) {
info->palettePtr = 0;
if (jsvIsArrayBuffer(palette) && palette->varData.arraybuffer.type==ARRAYBUFFERVIEW_UINT16) {
size_t l = 0;
info->palettePtr = (uint16_t *)jsvGetDataPointer(palette, &l);
if (l==2 || l==4 || l==8 || l==16 || l==256) {
info->paletteMask = (uint32_t)(l-1);
}
}
if (!info->palettePtr)
jsExceptionHere(JSET_ERROR, "Palette specified, but must be a flat Uint16Array of 2,4,8,16,256 elements");
return palette;
}
/** Parse an image into GfxDrawImageInfo. See drawImage for image format docs. Returns true on success.
* if 'image' is a string or ArrayBuffer, imageOffset is the offset within that (usually 0)
*/
@ -144,21 +159,8 @@ bool _jswrap_graphics_parseImage(JsGraphics *gfx, JsVar *image, size_t imageOffs
#ifndef SAVE_ON_FLASH_EXTREME
v = jsvObjectGetChildIfExists(image, "palette");
if (v) {
if (jsvIsArrayBuffer(v) && v->varData.arraybuffer.type==ARRAYBUFFERVIEW_UINT16) {
size_t l = 0;
info->palettePtr = (uint16_t *)jsvGetDataPointer(v, &l);
jsvUnLock(v);
if (l==2 || l==4 || l==8 || l==16 || l==256) {
info->paletteMask = (uint32_t)(l-1);
} else {
info->palettePtr = 0;
}
} else
jsvUnLock(v);
if (!info->palettePtr) {
jsExceptionHere(JSET_ERROR, "Palette specified, but must be a flat Uint16Array of 2,4,8,16,256 elements");
return false;
}
jsvUnLock(_jswrap_graphics_parseImage_palette(info, v));
if (!info->palettePtr) return false;
}
#endif
@ -3595,17 +3597,19 @@ like Bangle.js. Maximum layer count right now is 4.
layers = [ {
{x : int, // x start position
y : int, // y start position
image : string/object,
image : string/object/Graphics,
scale : float, // scale factor, default 1
rotate : float, // angle in radians
center : bool // center on x,y? default is top left
repeat : should this image be repeated (tiled?)
nobounds : bool // if true, the bounds of the image are not used to work out the default area to draw
palette : new Uint16Array(2/4/8/16/256) // (2v22+) a color palette to use with the image (overrides the image's palette)
compose : ""/"add"/"or"/"xor" // (2v22+) if set, the operation used when combining with the previous layer
}
]
options = { // the area to render. Defaults to rendering just enough to cover what's requested
x,y,
width,height
options = {
x,y, : int // the area to render. Defaults to rendering just enough to cover what's requested
width,height : int
}
```
*/
@ -3647,6 +3651,24 @@ JsVar *jswrap_graphics_drawImages(JsVar *parent, JsVar *layersVar, JsVar *option
if (layers[i].x2>x+width) width=layers[i].x2-x;
if (layers[i].y2>y+height) height=layers[i].y2-y;
}
// extra palette supplied
JsVar *v = jsvObjectGetChildIfExists(layer,"palette");
if (v) {
jsvUnLock(_jswrap_graphics_parseImage_palette(&layers[i].img, v));
if (!layers[i].img.palettePtr) ok = false;
}
// compose operation
JsVar *opVar = jsvObjectGetChildIfExists(layer,"compose");
layers[i].compose = GFXDILC_REPLACE;
if (!opVar || !jsvGetStringLength(opVar)) layers[i].compose = GFXDILC_REPLACE;
else if (jsvIsStringEqual(opVar,"add")) layers[i].compose = GFXDILC_ADD;
else if (jsvIsStringEqual(opVar,"or")) layers[i].compose = GFXDILC_OR;
else if (jsvIsStringEqual(opVar,"xor")) layers[i].compose = GFXDILC_XOR;
else {
jsExceptionHere(JSET_ERROR,"Unknown op type %q", opVar);
ok = false;
}
jsvUnLock(opVar);
} else ok = false;
jsvUnLock(image);
} else ok = false;
@ -3654,19 +3676,21 @@ JsVar *jswrap_graphics_drawImages(JsVar *parent, JsVar *layersVar, JsVar *option
}
jsvConfigObject configs[] = {
{"x", JSV_INTEGER, &x},
{"y", JSV_INTEGER, &y},
{"width", JSV_INTEGER, &width},
{"height", JSV_INTEGER, &height}
{"x", JSV_INTEGER, &x},
{"y", JSV_INTEGER, &y},
{"width", JSV_INTEGER, &width},
{"height", JSV_INTEGER, &height}
};
if (!jsvReadConfigObject(options, configs, sizeof(configs) / sizeof(jsvConfigObject)))
ok = false;
int x2 = x+width-1, y2 = y+height-1;
graphicsSetModifiedAndClip(&gfx, &x, &y, &x2, &y2,false);
JsGraphicsSetPixelFn setPixel = graphicsGetSetPixelFn(&gfx);
// If all good, start rendering!
if (ok) {
uint32_t maxCol = ((1UL<<gfx.data.bpp)-1); // max color for gfx instance
for (i=0;i<layerCount;i++) {
jsvStringIteratorNew(&layers[i].it, layers[i].img.buffer, (size_t)layers[i].img.bitmapOffset);
_jswrap_drawImageLayerSetStart(&layers[i], x, y);
@ -3676,23 +3700,32 @@ JsVar *jswrap_graphics_drawImages(JsVar *parent, JsVar *layersVar, JsVar *option
for (i=0;i<layerCount;i++)
_jswrap_drawImageLayerStartX(&layers[i]);
for (int xi = x; xi <= x2 ; xi++) {
// scan backwards until we hit a 'solid' pixel
// if first layer isn't *replace* then we
bool solid = false;
uint32_t colData = 0;
for (i=layerCount-1;i>=0;i--) {
if (_jswrap_drawImageLayerGetPixel(&layers[i], &colData)) {
for (i=0;i<layerCount;i++) {
uint32_t layerCol;
if (_jswrap_drawImageLayerGetPixel(&layers[i], &layerCol)) {
solid = true;
break;
if (i==0 && layers[0].compose!=GFXDILC_REPLACE)
colData = graphicsGetPixel(&gfx, xi, yi);
switch (layers[i].compose) {
case GFXDILC_REPLACE: colData = layerCol; break;
case GFXDILC_ADD: colData += layerCol;
if (colData>maxCol)
colData = maxCol;
break;
case GFXDILC_OR: colData |= layerCol; break;
case GFXDILC_XOR: colData ^= layerCol; break;
}
}
// next in layers!
_jswrap_drawImageLayerNextX(&layers[i]);
_jswrap_drawImageLayerNextXRepeat(&layers[i]);
}
// if nontransparent, draw it!
if (solid)
setPixel(&gfx, xi, yi, colData);
// next in layers!
for (i=0;i<layerCount;i++) {
_jswrap_drawImageLayerNextX(&layers[i]);
_jswrap_drawImageLayerNextXRepeat(&layers[i]);
}
}
for (i=0;i<layerCount;i++)
_jswrap_drawImageLayerNextY(&layers[i]);

View File

@ -120,8 +120,15 @@ void _jswrap_graphics_freeImageInfo(GfxDrawImageInfo *info);
*/
bool _jswrap_graphics_parseImage(JsGraphics *gfx, JsVar *image, size_t imageOffset, GfxDrawImageInfo *info);
/// When using drawImages, how should images be composed
typedef enum {
GFXDILC_REPLACE, //< Normal, replace unless transparent
GFXDILC_ADD, //< add to what's there
GFXDILC_OR, //< binary OR to what's there
GFXDILC_XOR //< binary XOR with what's there
} GfxDrawImageLayerCompose;
/// This is for rotating and scaling layers
/// This is for rotating and scaling layers with drawImages
typedef struct {
int x1,y1,x2,y2; //x2/y2 is exclusive
double rotate; // radians
@ -135,6 +142,7 @@ typedef struct {
int sx,sy; //< iterator X increment
int px,py; //< y iterator position
int qx,qy; //< x iterator position
GfxDrawImageLayerCompose compose; //< How should this layer be composed onto previous layers?
} GfxDrawImageLayer;
bool _jswrap_drawImageLayerGetPixel(GfxDrawImageLayer *l, uint32_t *result);

View File

@ -6,7 +6,7 @@ g.dump = _=>{
for (var y=0;y<g.getHeight();y++) {
s+="\n";
for (var x=0;x<g.getWidth();x++)
s+=".#"[b[n++]?1:0];
s+=".oO#"[b[n++]&3];
}
return s;
}
@ -31,7 +31,7 @@ function testLine(IBPP) {
g.clear(1);
g.drawImages([{image:cgimg,x:0,y:0,scale:1}],{});
console.log("bpp:",IBPP);
console.log("bpp:",IBPP);
SHOULD_BE(`
#...............
.#..............
@ -57,4 +57,79 @@ testLine(4);
testLine(8);
testLine(16);
// test combining images (OR=almost the same apart from line ~12)
g.clear(1).setColor(1);
var im = atob("BwgBqgP////AVQ==");
g.drawImages([
{image:im,x:0,y:0,scale:2},
{image:im,x:5,y:5,scale:1, compose:"or"}
],{});
SHOULD_BE(`
oo..oo..oo..oo..
oo..oo..oo..oo..
................
................
oooooooooooooo..
oooooooooooooo..
oooooooooooooo..
oooooooooooooo..
oooooooooooooo..
oooooooooooooo..
oooooooooooooo..
oooooooooooooo..
.....o.o.o.o....
................
oo..oo..oo..oo..
oo..oo..oo..oo..`);
// test combining images (ADD)
g.clear(1).setColor(1);
var im = atob("BwgBqgP////AVQ==");
g.drawImages([
{image:im,x:0,y:0,scale:2},
{image:im,x:5,y:5,scale:1, compose:"add"}
],{});
SHOULD_BE(`
oo..oo..oo..oo..
oo..oo..oo..oo..
................
................
oooooooooooooo..
oooooOoOoOoOoo..
oooooooooooooo..
oooooOOOOOOOoo..
oooooOOOOOOOoo..
oooooOOOOOOOoo..
oooooOOOOOOOoo..
oooooooooooooo..
.....o.o.o.o....
................
oo..oo..oo..oo..
oo..oo..oo..oo..`);
// test combining images (OR with palette)
g.clear(1).setColor(1);
var im = atob("BwgBqgP////AVQ==");
g.drawImages([
{image:im,x:0,y:0,scale:2},
{image:im,x:5,y:5,scale:1, compose:"or", palette:new Uint16Array([0,2])}
],{});
SHOULD_BE(`
oo..oo..oo..oo..
oo..oo..oo..oo..
................
................
oooooooooooooo..
ooooo#o#o#o#oo..
oooooooooooooo..
ooooo#######oo..
ooooo#######oo..
ooooo#######oo..
ooooo#######oo..
oooooooooooooo..
.....O.O.O.O....
................
oo..oo..oo..oo..
oo..oo..oo..oo..`);
result = ok;