Espruino/libs/graphics/jswrap_graphics.c
2023-09-27 14:57:47 +01:00

4498 lines
157 KiB
C

/*
* This file is part of Espruino, a JavaScript interpreter for Microcontrollers
*
* Copyright (C) 2013 Gordon Williams <gw@pur3.co.uk>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* ----------------------------------------------------------------------------
* This file is designed to be parsed during the build process
*
* Contains JavaScript Graphics Draw Functions
* ----------------------------------------------------------------------------
*/
#include "jswrap_graphics.h"
#include "jswrap_math.h" // for jswrap_math_cos/sin
#include "jsutils.h"
#include "jsinteractive.h"
#include "lcd_arraybuffer.h"
#include "lcd_js.h"
#ifdef USE_LCD_SDL
#include "lcd_sdl.h"
#endif
#ifdef USE_LCD_FSMC
#include "lcd_fsmc.h"
#endif
#ifdef USE_LCD_ST7789_8BIT
#include "lcd_st7789_8bit.h"
#endif
#include "jswrap_functions.h" // for asURL
#include "jswrap_object.h" // for getFonts
#ifdef DICKENS
#include "jsflash.h" // for saveScreenshot
#include "lcd_spilcd.h"
#define ESPR_LINE_FONTS
#endif
#include "bitmap_font_4x6.h"
#include "bitmap_font_6x8.h"
#include "vector_font.h"
#ifdef ESPR_PBF_FONTS
#include "pbf_font.h"
#endif
#ifdef ESPR_LINE_FONTS
#include "line_font.h"
#endif
#ifdef GRAPHICS_PALETTED_IMAGES
#if defined(ESPR_GRAPHICS_12BIT)
#define PALETTE_BPP 12
// 8 color RGB - [0,0,0,0,0,0,0,0].map((x,i)=>((i&4)?0xF00:0)|((i&2)?0xF0:0)|((i&1)?0xF:0))
const uint16_t PALETTE_3BIT[16] = { 0, 15, 240, 255, 3840, 3855, 4080, 4095 };
// 16 color 12 bit MAC OS palette
const uint16_t PALETTE_4BIT[16] = { 0x0,0x444,0x888,0xbbb,0x963,0x630,0x60,0xa0,0x9f,0xc,0x309,0xf09,0xd00,0xf60,0xff0,0xfff };
// 256 color 12 bit Web-safe palette
const uint16_t PALETTE_8BIT[256] = { 0x0,0x3,0x6,0x9,0xc,0xf,0x30,0x33,0x36,0x39,0x3c,0x3f,0x60,0x63,0x66,0x69,0x6c,0x6f,0x90,0x93,0x96,0x99,0x9c,0x9f,0xc0,0xc3,0xc6,0xc9,0xcc,0xcf,0xf0,0xf3,0xf6,0xf9,0xfc,0xff,0x300,0x303,0x306,0x309,0x30c,0x30f,0x330,0x333,0x336,0x339,0x33c,0x33f,0x360,0x363,0x366,0x369,0x36c,0x36f,0x390,0x393,0x396,0x399,0x39c,0x39f,0x3c0,0x3c3,0x3c6,0x3c9,0x3cc,0x3cf,0x3f0,0x3f3,0x3f6,0x3f9,0x3fc,0x3ff,0x600,0x603,0x606,0x609,0x60c,0x60f,0x630,0x633,0x636,0x639,0x63c,0x63f,0x660,0x663,0x666,0x669,0x66c,0x66f,0x690,0x693,0x696,0x699,0x69c,0x69f,0x6c0,0x6c3,0x6c6,0x6c9,0x6cc,0x6cf,0x6f0,0x6f3,0x6f6,0x6f9,0x6fc,0x6ff,0x900,0x903,0x906,0x909,0x90c,0x90f,0x930,0x933,0x936,0x939,0x93c,0x93f,0x960,0x963,0x966,0x969,0x96c,0x96f,0x990,0x993,0x996,0x999,0x99c,0x99f,0x9c0,0x9c3,0x9c6,0x9c9,0x9cc,0x9cf,0x9f0,0x9f3,0x9f6,0x9f9,0x9fc,0x9ff,0xc00,0xc03,0xc06,0xc09,0xc0c,0xc0f,0xc30,0xc33,0xc36,0xc39,0xc3c,0xc3f,0xc60,0xc63,0xc66,0xc69,0xc6c,0xc6f,0xc90,0xc93,0xc96,0xc99,0xc9c,0xc9f,0xcc0,0xcc3,0xcc6,0xcc9,0xccc,0xccf,0xcf0,0xcf3,0xcf6,0xcf9,0xcfc,0xcff,0xf00,0xf03,0xf06,0xf09,0xf0c,0xf0f,0xf30,0xf33,0xf36,0xf39,0xf3c,0xf3f,0xf60,0xf63,0xf66,0xf69,0xf6c,0xf6f,0xf90,0xf93,0xf96,0xf99,0xf9c,0xf9f,0xfc0,0xfc3,0xfc6,0xfc9,0xfcc,0xfcf,0xff0,0xff3,0xff6,0xff9,0xffc,0xfff,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xfff };
#else // default to 16 bit
#define PALETTE_BPP 16
// 8 color RGB - [0,0,0,0,0,0,0,0].map((x,i)=>((i&4)?0xF800:0)|((i&2)?0x7E0:0)|((i&1)?0x1F:0))
const uint16_t PALETTE_3BIT[16] = { 0, 31, 2016, 2047, 63488, 63519, 65504, 65535 };
// 16 color MAC OS palette
#if LCD_BPP==3 // for 3bpp LCDs we use a more saturated palette that maps better to the less vibrant LCD
const uint16_t PALETTE_4BIT[16] = { 0x0,0x4228,0x8c51,0xbdd7,0xcc40,0x8a20,0x320,0x7e0,0x7ff,0x1f,0x8811,0xf81f,0xf800,0xfc40,0xffe0,0xffff };
#else // otherwise we use the standard Mac pallete
const uint16_t PALETTE_4BIT[16] = { 0x0,0x4228,0x8c51,0xbdd7,0x9b26,0x6180,0x320,0x540,0x4df,0x19,0x3013,0xf813,0xd800,0xfb20,0xffe0,0xffff };
#endif
// 256 color 16 bit Web-safe palette
const uint16_t PALETTE_8BIT[256] = {
0x0,0x6,0xc,0x13,0x19,0x1f,0x180,0x186,0x18c,0x193,0x199,0x19f,0x320,0x326,0x32c,0x333,0x339,0x33f,0x4c0,
0x4c6,0x4cc,0x4d3,0x4d9,0x4df,0x660,0x666,0x66c,0x673,0x679,0x67f,0x7e0,0x7e6,0x7ec,0x7f3,0x7f9,0x7ff,
0x3000,0x3006,0x300c,0x3013,0x3019,0x301f,0x3180,0x3186,0x318c,0x3193,0x3199,0x319f,0x3320,0x3326,0x332c,
0x3333,0x3339,0x333f,0x34c0,0x34c6,0x34cc,0x34d3,0x34d9,0x34df,0x3660,0x3666,0x366c,0x3673,0x3679,0x367f,
0x37e0,0x37e6,0x37ec,0x37f3,0x37f9,0x37ff,0x6000,0x6006,0x600c,0x6013,0x6019,0x601f,0x6180,0x6186,0x618c,
0x6193,0x6199,0x619f,0x6320,0x6326,0x632c,0x6333,0x6339,0x633f,0x64c0,0x64c6,0x64cc,0x64d3,0x64d9,0x64df,
0x6660,0x6666,0x666c,0x6673,0x6679,0x667f,0x67e0,0x67e6,0x67ec,0x67f3,0x67f9,0x67ff,0x9800,0x9806,0x980c,
0x9813,0x9819,0x981f,0x9980,0x9986,0x998c,0x9993,0x9999,0x999f,0x9b20,0x9b26,0x9b2c,0x9b33,0x9b39,0x9b3f,
0x9cc0,0x9cc6,0x9ccc,0x9cd3,0x9cd9,0x9cdf,0x9e60,0x9e66,0x9e6c,0x9e73,0x9e79,0x9e7f,0x9fe0,0x9fe6,0x9fec,
0x9ff3,0x9ff9,0x9fff,0xc800,0xc806,0xc80c,0xc813,0xc819,0xc81f,0xc980,0xc986,0xc98c,0xc993,0xc999,0xc99f,
0xcb20,0xcb26,0xcb2c,0xcb33,0xcb39,0xcb3f,0xccc0,0xccc6,0xcccc,0xccd3,0xccd9,0xccdf,0xce60,0xce66,0xce6c,
0xce73,0xce79,0xce7f,0xcfe0,0xcfe6,0xcfec,0xcff3,0xcff9,0xcfff,0xf800,0xf806,0xf80c,0xf813,0xf819,0xf81f,
0xf980,0xf986,0xf98c,0xf993,0xf999,0xf99f,0xfb20,0xfb26,0xfb2c,0xfb33,0xfb39,0xfb3f,0xfcc0,0xfcc6,0xfccc,
0xfcd3,0xfcd9,0xfcdf,0xfe60,0xfe66,0xfe6c,0xfe73,0xfe79,0xfe7f,0xffe0,0xffe6,0xffec,0xfff3,0xfff9,0xffff,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xffff
};
// convert 16 bit to 8 bit with
// (c=>[(c&0xF800)>>8,(c&0x7E0)>>3,(c&0x1F)<<3])(0x0661)
// 16 bit to hex with
// (c=>"#"+[(c&0xF800)>>8,(c&0x7E0)>>3,(c&0x1F)<<3].map(c=>((c+8)>>4).toString(16)).join(""))(0x0661)
#endif
// map Mac to web-safe palette. Must be uint16_t because drawImage uses uint16_t* for palette
const uint16_t PALETTE_4BIT_TO_8BIT[16] = { 0, 43, 129, 172, 121, 78, 12, 18, 23, 4, 39, 183, 144, 192, 210, 215 };
// g2=Graphics.createArrayBuffer(8,8,8);print((new Uint8Array(8)).map((n,i)=>g2.toColor((i&4)>>2,(i&2)>>1,i&1)).join(","));
// map Mac to web-safe palette. Must be uint16_t because drawImage uses uint16_t* for palette
const uint16_t PALETTE_3BIT_TO_8BIT[8] = { 0,5,30,35,180,185,210,215 };
// g2=Graphics.createArrayBuffer(8,8,4);print((new Uint8Array(8)).map((n,i)=>g2.toColor((i&4)>>2,(i&2)>>1,i&1)).join(","));
// map Mac to web-safe palette. Must be uint16_t because drawImage uses uint16_t* for palette
const uint16_t PALETTE_3BIT_TO_4BIT[8] = { 0,9,7,8,12,11,14,15 };
#endif
// ==========================================================================================
void _jswrap_graphics_freeImageInfo(GfxDrawImageInfo *info) {
jsvUnLock(info->buffer);
}
/** 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)
*/
bool _jswrap_graphics_parseImage(JsGraphics *gfx, JsVar *image, unsigned int imageOffset, GfxDrawImageInfo *info) {
memset(info, 0, sizeof(GfxDrawImageInfo));
if (jsvIsObject(image)) {
#ifndef SAVE_ON_FLASH
if (jsvIsInstanceOf(image,"Graphics")) { // handle a Graphics instance
JsGraphics ig;
if (!graphicsGetFromVar(&ig, image)) return false;
if (ig.data.type!=JSGRAPHICSTYPE_ARRAYBUFFER) return false; // if not arraybuffer Graphics, bail out
info->width = ig.data.width;
info->height = ig.data.height;
info->bpp = ig.data.bpp;
} else
#endif
{ // Normal image object
info->width = (int)jsvGetIntegerAndUnLock(jsvObjectGetChildIfExists(image, "width"));
info->height = (int)jsvGetIntegerAndUnLock(jsvObjectGetChildIfExists(image, "height"));
info->bpp = (int)jsvGetIntegerAndUnLock(jsvObjectGetChildIfExists(image, "bpp"));
if (info->bpp<=0) info->bpp=1;
}
// Get the buffer for image data - this is the same in Graphics and Image objects
JsVar *buf = jsvObjectGetChildIfExists(image, "buffer");
info->buffer = jsvGetArrayBufferBackingString(buf, &info->bitmapOffset);
jsvUnLock(buf);
info->bitmapOffset += imageOffset;
JsVar *v;
v = jsvObjectGetChildIfExists(image, "transparent");
info->isTransparent = v!=0;
info->transparentCol = (unsigned int)jsvGetIntegerAndUnLock(v);
#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;
}
}
#endif
} else if (jsvIsString(image) || jsvIsArrayBuffer(image)) {
if (jsvIsArrayBuffer(image)) {
info->buffer = jsvGetArrayBufferBackingString(image, &info->bitmapOffset);
} else {
info->buffer = jsvLockAgain(image);
}
JsvStringIterator it;
jsvStringIteratorNewUTF8(&it, info->buffer, imageOffset);
info->width = jsvStringIteratorGetUTF8CharAndNext(&it);
info->height = jsvStringIteratorGetUTF8CharAndNext(&it);
info->bpp = jsvStringIteratorGetUTF8CharAndNext(&it);
info->bitmapOffset += imageOffset;
if (info->bpp & 128) {
info->bpp = info->bpp&127;
info->isTransparent = true;
info->transparentCol = jsvStringIteratorGetUTF8CharAndNext(&it);
info->headerLength = 4;
} else {
info->headerLength = 3;
}
info->bitmapOffset += info->headerLength;
if (info->bpp & 64) { // included palette data
info->bpp = info->bpp&63;
#ifndef SAVE_ON_FLASH_EXTREME
if (info->bpp > 8) {
jsExceptionHere(JSET_ERROR, "Can't have palette on >8 bit images");
_jswrap_graphics_freeImageInfo(info);
jsvStringIteratorFree(&it);
return false;
}
unsigned int paletteEntries = 1<<info->bpp;
info->paletteMask = (uint32_t)paletteEntries-1;
if (paletteEntries*2 <= (int)sizeof(info->_simplePalette)) {
// if it'll fit, put the palette data in _simplePalette
uint32_t n = info->bitmapOffset;
for (unsigned int i=0;i<paletteEntries;i++) {
info->_simplePalette[i] =
((unsigned char)jsvStringIteratorGetUTF8CharAndNext(&it)) |
((unsigned char)jsvStringIteratorGetUTF8CharAndNext(&it))<<8;
n+=2;
}
info->palettePtr = info->_simplePalette;
} else if (info->bpp<=8) { // otherwise if data is memory-mapped, use direct
unsigned int imgStart = info->bitmapOffset + paletteEntries*2;
size_t dataLen = 0;
char *dataPtr = jsvGetDataPointer(info->buffer, &dataLen);
if (info->bpp<=8 && dataPtr && imgStart<dataLen) {
info->paletteMask = (uint32_t)(paletteEntries-1);
info->palettePtr = (uint16_t*)&dataPtr[info->bitmapOffset+info->headerLength];
}
}
// could allocate a flat string and copy data in here
if (!info->palettePtr) {
jsExceptionHere(JSET_ERROR, "Unable to get pointer to palette. Image in flash?");
_jswrap_graphics_freeImageInfo(info);
jsvStringIteratorFree(&it);
return false;
}
// modify image start
info->headerLength += (unsigned short)(paletteEntries*2);
info->bitmapOffset += (unsigned short)(paletteEntries*2);
#else
jsExceptionHere(JSET_ERROR, "Image Palette not supported on this build");
#endif
}
jsvStringIteratorFree(&it);
} else {
jsExceptionHere(JSET_ERROR, "Expecting first argument to be an object or a String");
return 0;
}
if (!info->isTransparent)
info->transparentCol = 0xFFFFFFFF;
if (info->palettePtr==0) {
if (info->bpp==1) {
info->_simplePalette[0] = (uint16_t)gfx->data.bgColor;
info->_simplePalette[1] = (uint16_t)gfx->data.fgColor;
info->palettePtr = info->_simplePalette;
info->paletteMask = 1;
#ifdef GRAPHICS_PALETTED_IMAGES
} else if (info->bpp==2) { // Blend from bg to fg
info->_simplePalette[0] = (uint16_t)gfx->data.bgColor;
info->_simplePalette[1] = (uint16_t)graphicsBlendGfxColor(gfx, 85);
info->_simplePalette[2] = (uint16_t)graphicsBlendGfxColor(gfx, 171);
info->_simplePalette[3] = (uint16_t)gfx->data.fgColor;
info->palettePtr = info->_simplePalette;
info->paletteMask = 3;
} else if (gfx->data.bpp==PALETTE_BPP && info->bpp==3) { // palette is 16 bits, so don't use it for other things
info->palettePtr = PALETTE_3BIT;
info->paletteMask = 7;
} else if (gfx->data.bpp==PALETTE_BPP && info->bpp==4) { // palette is 16 bits, so don't use it for other things
info->palettePtr = PALETTE_4BIT;
info->paletteMask = 15;
} else if (gfx->data.bpp==PALETTE_BPP && info->bpp==8) { // palette is 16 bits, so don't use it for other things
info->palettePtr = PALETTE_8BIT;
info->paletteMask = 255;
} else if (gfx->data.bpp==8 && info->bpp==4) {
info->palettePtr = PALETTE_4BIT_TO_8BIT;
info->paletteMask = 15;
} else if (gfx->data.bpp==8 && info->bpp==3) {
info->palettePtr = PALETTE_3BIT_TO_8BIT;
info->paletteMask = 7;
} else if (gfx->data.bpp==4 && info->bpp==3) {
info->palettePtr = PALETTE_3BIT_TO_4BIT;
info->paletteMask = 7;
#endif
}
}
if ((!jsvIsString(info->buffer)) ||
info->width<=0 ||
info->height<=0 ||
info->bpp>32) {
jsExceptionHere(JSET_ERROR, "Expecting first argument to a valid Image");
_jswrap_graphics_freeImageInfo(info);
return false;
}
info->bitMask = (unsigned int)((1L<<info->bpp)-1L);
info->pixelsPerByteMask = (unsigned int)((info->bpp<8)?(8/info->bpp)-1:0);
info->stride = (info->width*info->bpp + 7)>>3;
info->bitmapLength = (unsigned short)((info->width*info->height*info->bpp + 7)>>3);
return true;
}
bool _jswrap_drawImageLayerGetPixel(GfxDrawImageLayer *l, unsigned int *result) {
int qx = l->qx+127;
int qy = l->qy+127;
if (qx>=0 && qy>=0 && qx<l->mx && qy<l->my) {
unsigned int colData = 0;
int imagex = qx>>8;
int imagey = qy>>8;
// TODO: getter callback for speed?
#ifndef SAVE_ON_FLASH
if (l->img.bpp==8) { // fast path for 8 bits
jsvStringIteratorGoto(&l->it, l->img.buffer, (size_t)((int)l->img.bitmapOffset+imagex+(imagey*l->img.stride)));
colData = (unsigned char)jsvStringIteratorGetChar(&l->it);
} else
#endif
{
int pixelOffset = (imagex+(imagey*l->img.width));
int bitOffset = pixelOffset*l->img.bpp;
jsvStringIteratorGoto(&l->it, l->img.buffer, (size_t)((int)l->img.bitmapOffset+(bitOffset>>3)));
bitOffset &= 7; // so now it's bits within a byte
// get first byte
colData = (unsigned char)jsvStringIteratorGetChar(&l->it);
// it may not fit within a byte, so if so grab more data
int b;
for (b=8-(bitOffset+l->img.bpp);b<0;b+=8) {
jsvStringIteratorNext(&l->it);
colData = (colData<<8) | (unsigned char)jsvStringIteratorGetChar(&l->it);
}
// finally shift down to the right size
colData = (colData>>b) & l->img.bitMask;
}
if (l->img.transparentCol!=colData) {
if (l->img.palettePtr) colData = l->img.palettePtr[colData&l->img.paletteMask];
*result = colData;
return true;
}
}
return false;
}
NO_INLINE void _jswrap_drawImageLayerInit(GfxDrawImageLayer *l) {
// image max
l->mx = l->img.width<<8;
l->my = l->img.height<<8;
// step values for blitting rotated image
double vcos = jswrap_math_cos(l->rotate);
double vsin = jswrap_math_sin(l->rotate);
l->sx = (int)((vcos/l->scale)*256 + 0.5);
l->sy = (int)((vsin/l->scale)*256 + 0.5);
// work out actual image width and height
int iw = (int)(0.5 + l->scale*(l->img.width*fabs(vcos) + l->img.height*fabs(vsin)));
int ih = (int)(0.5 + l->scale*(l->img.width*fabs(vsin) + l->img.height*fabs(vcos)));
// if rotating, offset our start position from center
if (l->center) {
l->x1 -= iw/2;
l->y1 -= ih/2;
}
l->x2 = l->x1 + iw;
l->y2 = l->y1 + ih;
// work out start position in the image
int centerx = l->img.width*128;
int centery = l->img.height*128;
l->px = centerx - (1 + (l->sx*iw) + (l->sy*ih)) / 2;
l->py = centery - (1 + (l->sx*ih) - (l->sy*iw)) / 2;
// handle repetition
if (l->repeat) {
// for the range we're in, it's quicker/easier than modulo
while (l->px < 0) l->px += l->mx;
while (l->px >= l->mx) l->px -= l->mx;
while (l->py < 0) l->py += l->my;
while (l->py >= l->my) l->py -= l->my;
}
}
NO_INLINE void _jswrap_drawImageLayerSetStart(GfxDrawImageLayer *l, int x, int y) {
int dx = x - l->x1;
int dy = y - l->y1;
l->px += l->sx*dx + l->sy*dy;
l->py += l->sx*dy - l->sy*dx;
}
NO_INLINE void _jswrap_drawImageLayerStartX(GfxDrawImageLayer *l) {
l->qx = l->px;
l->qy = l->py;
}
ALWAYS_INLINE void _jswrap_drawImageLayerNextX(GfxDrawImageLayer *l) {
l->qx += l->sx;
l->qy -= l->sy;
}
// Handle repeats
ALWAYS_INLINE void _jswrap_drawImageLayerNextXRepeat(GfxDrawImageLayer *l) {
if (l->repeat) {
// for the range we're in, it's quicker/easier than modulo
if (l->qx < 0) l->qx += l->mx;
if (l->qx >= l->mx) l->qx -= l->mx;
if (l->qy < 0) l->qy += l->my;
if (l->qy >= l->my) l->qy -= l->my;
}
}
NO_INLINE void _jswrap_drawImageLayerNextY(GfxDrawImageLayer *l) {
l->px += l->sy;
l->py += l->sx;
if (l->repeat) {
// for the range we're in, it's quicker/easier than modulo
if (l->px < 0) l->px += l->mx;
if (l->px >= l->mx) l->px -= l->mx;
if (l->py < 0) l->py += l->my;
if (l->py >= l->my) l->py -= l->my;
}
}
/* Draw an image 1:1 at xPos,yPos. If parseFullImage=true we ensure
we leave the StringIterator pointing right at the end of the image. If not
we can optimise if the image is clipped/offscreen. */
NO_INLINE void _jswrap_drawImageSimple(JsGraphics *gfx, int xPos, int yPos, GfxDrawImageInfo *img, JsvStringIterator *it, bool parseFullImage) {
int bits=0;
uint32_t colData=0;
int x1 = xPos, y1 = yPos, x2 = xPos+img->width-1, y2 = yPos+img->height-1;
if (!jsvStringIteratorHasChar(it)) return; // no data
#ifndef SAVE_ON_FLASH
graphicsSetModifiedAndClip(gfx,&x1,&y1,&x2,&y2); // ensure we clip Y
/* force a skip forward as many bytes as we need. Ideally we would use
jsvStringIteratorGotoUTF8 but we don't have the UTF8 index or
source string here. This is still better than trying to render every pixel! */
if (y2<y1 || x2<x1) { // offscreen - skip everything and exit
if (parseFullImage) {
bits = -img->bpp*img->width*img->height;
while (bits < 0) {
jsvStringIteratorNextUTF8(it);
bits += 8;
}
}
return;
} else // onscreen. y1!=yPos if clipped - ensure we skip enough bytes
bits = -(y1-yPos)*img->bpp*img->width;
#endif
JsGraphicsSetPixelFn setPixel = graphicsGetSetPixelUnclippedFn(gfx, xPos, y1, xPos+img->width-1, y2);
for (int y=y1;y<=y2;y++) {
for (int x=xPos;x<xPos+img->width;x++) {
// Get the data we need...
while (bits < img->bpp) {
colData = (colData<<8) | ((unsigned char)jsvStringIteratorGetUTF8CharAndNext(it));
bits += 8;
}
// extract just the bits we want
unsigned int col = (colData>>(bits-img->bpp))&img->bitMask;
bits -= img->bpp;
// Try and write pixel!
if (img->transparentCol!=col) {
if (img->palettePtr) col = img->palettePtr[col&img->paletteMask];
setPixel(gfx, x, y, col);
}
}
}
#ifndef SAVE_ON_FLASH
if (parseFullImage) {
/* If we didn't render the last bit of the image, and the caller needs
the StringIterator to point to the end of the image, skip forward */
bits += img->bpp-(yPos+img->height-(1+y2))*img->bpp*img->width;
while (bits < 0) {
jsvStringIteratorNextUTF8(it);
bits += 8;
}
}
#endif
}
// ==========================================================================================
/*JSON{
"type" : "class",
"class" : "Graphics",
"typescript" : "Graphics<IsBuffer extends boolean = boolean>"
}
This class provides Graphics operations that can be applied to a surface.
Use Graphics.createXXX to create a graphics object that renders in the way you
want. See [the Graphics page](https://www.espruino.com/Graphics) for more
information.
**Note:** On boards that contain an LCD, there is a built-in `g` object of
type `Graphics`. For instance to draw a line you'd type:
```g.drawLine(0,0,100,100)```
*/
/*JSON{
"type" : "method",
"class" : "Graphics",
"params" : [ ["all","bool","[optional] (only on some devices) If `true` then copy all pixels, not just those that have changed."] ],
"name" : "flip"
}
On instances of graphics that drive a display with an offscreen buffer, calling
this function will copy the contents of the offscreen buffer to the screen.
Call this when you have drawn something to Graphics and you want it shown on the
screen.
If a display does not have an offscreen buffer, it may not have a `g.flip()`
method.
On Bangle.js 1, there are different graphics modes chosen with
`Bangle.setLCDMode()`. The default mode is unbuffered and in this mode
`g.flip()` does not affect the screen contents.
On some devices, this command will attempt to only update the areas of the
screen that have changed in order to increase speed. If you have accessed the
`Graphics.buffer` directly then you may need to use `Graphics.flip(true)` to
force a full update of the screen.
*/
/*JSON{
"type" : "property",
"class" : "Graphics",
"name" : "buffer",
"return" : ["JsVar","An ArrayBuffer (or not defined on Graphics instances not created with `Graphics.createArrayBuffer`)"],
"typescript" : "buffer: IsBuffer extends true ? ArrayBuffer : undefined"
}
On Graphics instances with an offscreen buffer, this is an `ArrayBuffer` that
provides access to the underlying pixel data.
```
g=Graphics.createArrayBuffer(8,8,8)
g.drawLine(0,0,7,7)
print(new Uint8Array(g.buffer))
new Uint8Array([
255, 0, 0, 0, 0, 0, 0, 0,
0, 255, 0, 0, 0, 0, 0, 0,
0, 0, 255, 0, 0, 0, 0, 0,
0, 0, 0, 255, 0, 0, 0, 0,
0, 0, 0, 0, 255, 0, 0, 0,
0, 0, 0, 0, 0, 255, 0, 0,
0, 0, 0, 0, 0, 0, 255, 0,
0, 0, 0, 0, 0, 0, 0, 255])
```
*/
/*JSON{
"type" : "idle",
"generate" : "jswrap_graphics_idle"
}*/
bool jswrap_graphics_idle() {
graphicsIdle();
return false;
}
/*JSON{
"type" : "init",
"generate" : "jswrap_graphics_init",
"sortorder" : -100
}*/
void jswrap_graphics_init() {
// sortorder is first because we don't want subsequent
// _init to a) not have GFX and b) not get their theme
// settings overwritten
#ifdef USE_LCD_FSMC
JsVar *parent = jspNewObject("g", "Graphics");
if (parent) {
JsVar *parentObj = jsvSkipName(parent);
jsvObjectSetChild(execInfo.hiddenRoot, JS_GRAPHICS_VAR, parentObj);
JsGraphics gfx;
gfx.data.type = JSGRAPHICSTYPE_FSMC;
graphicsStructInit(&gfx,320,240,16);
gfx.graphicsVar = parentObj;
lcdInit_FSMC(&gfx);
lcdSetCallbacks_FSMC(&gfx);
graphicsSplash(&gfx);
graphicsSetVarInitial(&gfx);
jsvUnLock2(parentObj, parent);
}
#endif
#ifdef GRAPHICS_THEME
/// Global color scheme colours
graphicsTheme.fg = (JsGraphicsThemeColor)-1;
graphicsTheme.bg = (JsGraphicsThemeColor)0;
graphicsTheme.fg2 = (JsGraphicsThemeColor)-1;
graphicsTheme.bg2 = (JsGraphicsThemeColor)0;
graphicsTheme.fgH = (JsGraphicsThemeColor)-1;
graphicsTheme.bgH = (JsGraphicsThemeColor)0;
graphicsTheme.dark = true;
#endif
}
/*JSON{
"type" : "staticmethod",
"class" : "Graphics",
"name" : "getInstance",
"generate" : "jswrap_graphics_getInstance",
"return" : ["JsVar","An instance of `Graphics` or undefined"],
"typescript" : "getInstance(): Graphics | undefined"
}
On devices like Pixl.js or HYSTM boards that contain a built-in display this
will return an instance of the graphics class that can be used to access that
display.
Internally, this is stored as a member called `gfx` inside the 'hiddenRoot'.
*/
JsVar *jswrap_graphics_getInstance() {
return jsvObjectGetChildIfExists(execInfo.hiddenRoot, JS_GRAPHICS_VAR);
}
static bool isValidBPP(int bpp) {
return bpp==1 || bpp==2 || bpp==4 || bpp==8 || bpp==16 || bpp==24 || bpp==32; // currently one colour can't ever be spread across multiple bytes
}
/*JSON{
"type" : "staticmethod",
"class" : "Graphics",
"name" : "createArrayBuffer",
"generate" : "jswrap_graphics_createArrayBuffer",
"params" : [
["width","int32","Pixels wide"],
["height","int32","Pixels high"],
["bpp","int32","Number of bits per pixel"],
["options","JsVar",[
"An object of other options. `{ zigzag : true/false(default), vertical_byte : true/false(default), msb : true/false(default), color_order: 'rgb'(default),'bgr',etc }`",
"`zigzag` = whether to alternate the direction of scanlines for rows",
"`vertical_byte` = whether to align bits in a byte vertically or not",
"`msb` = when bits<8, store pixels most significant bit first, when bits>8, store most significant byte first",
"`interleavex` = Pixels 0,2,4,etc are from the top half of the image, 1,3,5,etc from the bottom half. Used for P3 LED panels.",
"`color_order` = re-orders the colour values that are supplied via setColor"
]]
],
"return" : ["JsVar","The new Graphics object"],
"return_object" : "Graphics",
"typescript" : "createArrayBuffer(width: number, height: number, bpp: number, options?: { zigzag?: boolean, vertical_byte?: boolean, msb?: boolean, color_order?: \"rgb\" | \"rbg\" | \"brg\" | \"bgr\" | \"grb\" | \"gbr\" }): Graphics<true>;"
}
Create a Graphics object that renders to an Array Buffer. This will have a field
called 'buffer' that can get used to get at the buffer itself
*/
JsVar *jswrap_graphics_createArrayBuffer(int width, int height, int bpp, JsVar *options) {
if (width<=0 || height<=0 || width>32767 || height>32767) {
jsExceptionHere(JSET_ERROR, "Invalid Size");
return 0;
}
if (!isValidBPP(bpp)) {
jsExceptionHere(JSET_ERROR, "Invalid BPP");
return 0;
}
JsVar *parent = jspNewObject(0, "Graphics");
if (!parent) return 0; // low memory
JsGraphics gfx;
gfx.data.type = JSGRAPHICSTYPE_ARRAYBUFFER;
graphicsStructInit(&gfx,width,height,bpp);
gfx.data.flags = JSGRAPHICSFLAGS_NONE;
gfx.graphicsVar = parent;
if (jsvIsObject(options)) {
if (jsvGetBoolAndUnLock(jsvObjectGetChildIfExists(options, "zigzag")))
gfx.data.flags = (JsGraphicsFlags)(gfx.data.flags | JSGRAPHICSFLAGS_ARRAYBUFFER_ZIGZAG);
if (jsvGetBoolAndUnLock(jsvObjectGetChildIfExists(options, "msb")))
gfx.data.flags = (JsGraphicsFlags)(gfx.data.flags | JSGRAPHICSFLAGS_ARRAYBUFFER_MSB);
if (jsvGetBoolAndUnLock(jsvObjectGetChildIfExists(options, "interleavex")))
gfx.data.flags = (JsGraphicsFlags)(gfx.data.flags | JSGRAPHICSFLAGS_ARRAYBUFFER_INTERLEAVEX);
if (jsvGetBoolAndUnLock(jsvObjectGetChildIfExists(options, "vertical_byte"))) {
if (gfx.data.bpp==1)
gfx.data.flags = (JsGraphicsFlags)(gfx.data.flags | JSGRAPHICSFLAGS_ARRAYBUFFER_VERTICAL_BYTE);
else {
jsExceptionHere(JSET_ERROR, "vertical_byte only works for 1bpp ArrayBuffers\n");
return 0;
}
if (gfx.data.height&7) {
jsExceptionHere(JSET_ERROR, "height must be a multiple of 8 when using vertical_byte\n");
return 0;
}
}
JsVar *colorv = jsvObjectGetChildIfExists(options, "color_order");
if (colorv) {
if (jsvIsStringEqual(colorv, "rgb")) ; // The default
else if (!jsvIsStringEqual(colorv, "brg"))
gfx.data.flags = (JsGraphicsFlags)(gfx.data.flags | JSGRAPHICSFLAGS_COLOR_BRG);
else if (!jsvIsStringEqual(colorv, "bgr"))
gfx.data.flags = (JsGraphicsFlags)(gfx.data.flags | JSGRAPHICSFLAGS_COLOR_BGR);
else if (!jsvIsStringEqual(colorv, "gbr"))
gfx.data.flags = (JsGraphicsFlags)(gfx.data.flags | JSGRAPHICSFLAGS_COLOR_GBR);
else if (!jsvIsStringEqual(colorv, "grb"))
gfx.data.flags = (JsGraphicsFlags)(gfx.data.flags | JSGRAPHICSFLAGS_COLOR_GRB);
else if (!jsvIsStringEqual(colorv, "rbg"))
gfx.data.flags = (JsGraphicsFlags)(gfx.data.flags | JSGRAPHICSFLAGS_COLOR_RBG);
else
jsWarn("color_order must be 3 characters");
jsvUnLock(colorv);
}
}
lcdInit_ArrayBuffer(&gfx);
graphicsSetVarInitial(&gfx);
return parent;
}
/*JSON{
"type" : "staticmethod",
"class" : "Graphics",
"name" : "createCallback",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_createCallback",
"params" : [
["width","int32","Pixels wide"],
["height","int32","Pixels high"],
["bpp","int32","Number of bits per pixel"],
["callback","JsVar","A function of the form ```function(x,y,col)``` that is called whenever a pixel needs to be drawn, or an object with: ```{setPixel:function(x,y,col),fillRect:function(x1,y1,x2,y2,col)}```. All arguments are already bounds checked."]
],
"return" : ["JsVar","The new Graphics object"],
"return_object" : "Graphics",
"typescript" : "createCallback(width: number, height: number, bpp: number, callback: ((x: number, y: number, col: number) => void) | { setPixel: (x: number, y: number, col: number) => void; fillRect: (x1: number, y1: number, x2: number, y2: number, col: number) => void }): Graphics<false>;"
}
Create a Graphics object that renders by calling a JavaScript callback function
to draw pixels
*/
JsVar *jswrap_graphics_createCallback(int width, int height, int bpp, JsVar *callback) {
if (width<=0 || height<=0 || width>32767 || height>32767) {
jsExceptionHere(JSET_ERROR, "Invalid Size");
return 0;
}
if (!isValidBPP(bpp)) {
jsExceptionHere(JSET_ERROR, "Invalid BPP");
return 0;
}
JsVar *callbackSetPixel = 0;
JsVar *callbackFillRect = 0;
if (jsvIsObject(callback)) {
jsvUnLock(callbackSetPixel);
callbackSetPixel = jsvObjectGetChildIfExists(callback, "setPixel");
callbackFillRect = jsvObjectGetChildIfExists(callback, "fillRect");
} else
callbackSetPixel = jsvLockAgain(callback);
if (!jsvIsFunction(callbackSetPixel)) {
jsExceptionHere(JSET_ERROR, "Expecting Callback Function or an Object but got %t", callbackSetPixel);
jsvUnLock2(callbackSetPixel, callbackFillRect);
return 0;
}
if (!jsvIsUndefined(callbackFillRect) && !jsvIsFunction(callbackFillRect)) {
jsExceptionHere(JSET_ERROR, "Expecting Callback Function or an Object but got %t", callbackFillRect);
jsvUnLock2(callbackSetPixel, callbackFillRect);
return 0;
}
JsVar *parent = jspNewObject(0, "Graphics");
if (!parent) return 0; // low memory
JsGraphics gfx;
gfx.data.type = JSGRAPHICSTYPE_JS;
graphicsStructInit(&gfx,width,height,bpp);
gfx.graphicsVar = parent;
lcdInit_JS(&gfx, callbackSetPixel, callbackFillRect);
graphicsSetVarInitial(&gfx);
jsvUnLock2(callbackSetPixel, callbackFillRect);
return parent;
}
#ifdef USE_LCD_SDL
/*JSON{
"type" : "staticmethod",
"class" : "Graphics",
"name" : "createSDL",
"ifdef" : "USE_LCD_SDL",
"generate" : "jswrap_graphics_createSDL",
"params" : [
["width","int32","Pixels wide"],
["height","int32","Pixels high"],
["bpp","int32","Bits per pixel (8,16,24 or 32 supported)"]
],
"return" : ["JsVar","The new Graphics object"],
"return_object" : "Graphics"
}
Create a Graphics object that renders to SDL window (Linux-based devices only)
*/
JsVar *jswrap_graphics_createSDL(int width, int height, int bpp) {
if (width<=0 || height<=0 || width>32767 || height>32767) {
jsExceptionHere(JSET_ERROR, "Invalid Size");
return 0;
}
JsVar *parent = jspNewObject(0, "Graphics");
if (!parent) return 0; // low memory
JsGraphics gfx;
gfx.data.type = JSGRAPHICSTYPE_SDL;
graphicsStructInit(&gfx,width,height,bpp);
gfx.graphicsVar = parent;
lcdInit_SDL(&gfx);
graphicsSetVarInitial(&gfx);
return parent;
}
#endif
/*TYPESCRIPT
type ImageObject = {
width: number;
height: number;
bpp?: number;
buffer: ArrayBuffer | string;
transparent?: number;
palette?: Uint16Array;
};
type Image = string | ImageObject | ArrayBuffer | Graphics<true>;
*/
/*JSON{
"type" : "staticmethod",
"class" : "Graphics",
"name" : "createImage",
"#if" : "!defined(SAVE_ON_FLASH) && !defined(ESPRUINOBOARD)",
"generate" : "jswrap_graphics_createImage",
"params" : [
["str","JsVar","A String containing a newline-separated image - space/. is 0, anything else is 1"]
],
"return" : ["JsVar","An Image object that can be used with `Graphics.drawImage`"],
"typescript" : "createImage(str: string): ImageObject;"
}
Create a simple Black and White image for use with `Graphics.drawImage`.
Use as follows:
```
var img = Graphics.createImage(`
XXXXXXXXX
X X
X X X
X X X
X X
XXXXXXXXX
`);
g.drawImage(img, x,y);
var img = Graphics.createImage(`
.....
.XXX.
.X.X.
.XXX.
.....
`);
g.drawImage(img, x,y);
```
If the characters at the beginning and end of the string are newlines, they will
be ignored. Spaces are treated as `0`, and any other character is a `1`
*/
JsVar *jswrap_graphics_createImage(JsVar *data) {
if (!jsvIsString(data)) {
jsExceptionHere(JSET_TYPEERROR, "Expecting a String");
return 0;
}
int x=0,y=0;
int width=0, height=0;
size_t startCharacter = 0;
JsvStringIterator it;
// First iterate and work out width and height
jsvStringIteratorNew(&it,data,0);
char ch;
while (jsvStringIteratorHasChar(&it)) {
ch = jsvStringIteratorGetCharAndNext(&it);
if (ch=='\n') {
if (x==0 && y==0 && !startCharacter) {
startCharacter = 1; // ignore first character
} else {
x=0;
y++;
if (y+1>height) height=y+1;
}
} else {
x++;
if (x>width) width=x;
}
}
if (height && ch=='\n') height--; // if the last char was a newline, ignore it
jsvStringIteratorFree(&it);
// Sorted - now create the object, set it up and create the buffer
JsVar *img = jsvNewObject();
if (!img) return 0;
jsvObjectSetChildAndUnLock(img,"width",jsvNewFromInteger(width));
jsvObjectSetChildAndUnLock(img,"height",jsvNewFromInteger(height));
// bpp is 1, no need to set it
int len = (width*height+7)>>3;
JsVar *buffer = jsvNewStringOfLength((unsigned)len, NULL);
if (!buffer) { // not enough memory
jsvUnLock(img);
return 0;
}
// Now set the characters!
x=0;
y=0;
jsvStringIteratorNew(&it,data,startCharacter);
while (jsvStringIteratorHasChar(&it)) {
char ch = jsvStringIteratorGetCharAndNext(&it);
if (ch=='\n') {
x=0;
y++;
} else {
if (ch!=' ' && ch!='.') {
/* a pixel to set. This'll be slowish for non-flat strings,
* but this is here for relatively small bitmaps
* anyway so it's not a big deal */
size_t idx = (size_t)(y*width + x);
jsvSetCharInString(buffer, idx>>3, (char)(128>>(idx&7)), true/*OR*/);
}
x++;
}
}
jsvStringIteratorFree(&it);
jsvObjectSetChildAndUnLock(img, "buffer", buffer);
return img;
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "getWidth",
"generate_full" : "jswrap_graphics_getWidthOrHeight(parent, false)",
"return" : ["int","The width of this Graphics instance"]
}
The width of this Graphics instance
*/
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "getHeight",
"generate_full" : "jswrap_graphics_getWidthOrHeight(parent, true)",
"return" : ["int","The height of this Graphics instance"]
}
The height of this Graphics instance
*/
int jswrap_graphics_getWidthOrHeight(JsVar *parent, bool height) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
return height ? graphicsGetHeight(&gfx) : graphicsGetWidth(&gfx);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "getBPP",
"generate" : "jswrap_graphics_getBPP",
"return" : ["int","The bits per pixel of this Graphics instance"]
}
The number of bits per pixel of this Graphics instance
**Note:** Bangle.js 2 behaves a little differently here. The display is 3 bit,
so `getBPP` returns 3 and `asBMP`/`asImage`/etc return 3 bit images. However in
order to allow dithering, the colors returned by `Graphics.getColor` and
`Graphics.theme` are actually 16 bits.
*/
int jswrap_graphics_getBPP(JsVar *parent) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
#ifdef LCD_CONTROLLER_LPM013M126
if (gfx.data.type==JSGRAPHICSTYPE_MEMLCD)
return 3;
#endif
return gfx.data.bpp;
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "reset",
"generate" : "jswrap_graphics_reset",
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Reset the state of Graphics to the defaults (e.g. Color, Font, etc) that would
have been used when Graphics was initialised.
*/
JsVar *jswrap_graphics_reset(JsVar *parent) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
#ifndef SAVE_ON_FLASH
// If we had a custom font, remove it.
jsvUnLock(jswrap_graphics_setFontSizeX(parent, 1+JSGRAPHICS_FONTSIZE_4X6, false));
#endif
// properly reset state
graphicsStructResetState(&gfx);
graphicsSetVar(&gfx); // gfx data changed because modified area
// reset font, which will unreference any custom fonts stored inside the instance
return jswrap_graphics_setFontSizeX(parent, 1+JSGRAPHICS_FONTSIZE_4X6, false);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "clear",
"generate" : "jswrap_graphics_clear",
"params" : [
["reset","bool","[optional] If `true`, resets the state of Graphics to the default (eg. Color, Font, etc) as if calling `Graphics.reset`"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Clear the LCD with the Background Color
*/
JsVar *jswrap_graphics_clear(JsVar *parent, bool resetState) {
if (resetState) jsvUnLock(jswrap_graphics_reset(parent));
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
graphicsClear(&gfx);
graphicsSetVar(&gfx); // gfx data changed because modified area
return jsvLockAgain(parent);
}
void _jswrap_graphics_getRect(JsVar *opt, int *x1, int *y1, int *x2, int *y2, int *r) {
*r = 0;
if (jsvIsObject(opt)) {
int w = -1,h = -1;
jsvConfigObject configs[] = {
{"x", JSV_INTEGER, x1},
{"y", JSV_INTEGER, y1},
{"x1", JSV_INTEGER, x1},
{"y1", JSV_INTEGER, y1},
{"x2", JSV_INTEGER, x2},
{"y2", JSV_INTEGER, y2},
{"w", JSV_INTEGER, &w},
{"h", JSV_INTEGER, &h},
{"r", JSV_INTEGER, r}
};
jsvReadConfigObject(opt, configs, sizeof(configs) / sizeof(jsvConfigObject));
if (w>=0) *x2 = *x1 + w;
if (h>=0) *y2 = *y1 + h;
} else
*x1 = jsvGetInteger(opt);
}
JsVar *_jswrap_graphics_fillRect_col(JsVar *parent, JsVar *opt, int y1, int x2, int y2, bool isFgCol) {
int x1, r;
_jswrap_graphics_getRect(opt, &x1, &y1, &x2, &y2, &r);
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
uint32_t col = isFgCol ? gfx.data.fgColor : gfx.data.bgColor;
#ifndef SAVE_ON_FLASH
if (r>0) {
// rounded rects
graphicsToDeviceCoordinates(&gfx, &x1, &y1);
graphicsToDeviceCoordinates(&gfx, &x2, &y2);
int x,y;
if (x1 > x2) { x = x1; x1 = x2; x2 = x; }
if (y1 > y2) { y = y1; y1 = y2; y2 = y; }
// clip if radius too big
x = (x2-x1)/2;
y = (y2-y1)/2;
if (x<r) r=x;
if (y<r) r=y;
// rect in middle
int cx1 = x1+r, cx2 = x2-r;
int cy1 = y1+r, cy2 = y2-r;
graphicsFillRectDevice(&gfx, x1, cy1, x2, cy2, col);
// draw rounded top and bottom
int dx = 0;
int dy = r;
int r2 = r*r;
int err = r2-(2*r-1)*r2;
int e2;
bool changed = false;
do {
changed = false;
e2 = 2*err;
if (e2 < (2*dx+1)*r2) { dx++; err += (2*dx+1)*r2; changed=true; }
if (e2 > -(2*dy-1)*r2) {
// draw only just before we change Y, to avoid a bunch of overdraw
graphicsFillRectDevice(&gfx, cx1-dx,cy2+dy, cx2+dx,cy2+dy, col);
graphicsFillRectDevice(&gfx, cx1-dx,cy1-dy, cx2+dx,cy1-dy, col);
dy--; err -= (2*dy-1)*r2; changed=true;
}
} while (changed && dy >= 0);
} else
#endif
graphicsFillRect(&gfx, x1,y1,x2,y2,col);
graphicsSetVar(&gfx); // gfx data changed because modified area
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "fillRect",
"generate" : "jswrap_graphics_fillRect",
"params" : [
["x1","JsVar","The left X coordinate OR an object containing `{x,y,x2,y2}` or `{x,y,w,h}`"],
["y1","int32","The top Y coordinate"],
["x2","int32","The right X coordinate"],
["y2","int32","The bottom Y coordinate"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : [
"fillRect(x1: number, y1: number, x2: number, y2: number): Graphics;",
"fillRect(rect: { x: number, y: number, x2: number, y2: number } | { x: number, y: number, w: number, h: number }): Graphics;"
]
}
Fill a rectangular area in the Foreground Color
On devices with enough memory, you can specify `{x,y,x2,y2,r}` as the first
argument, which allows you to draw a rounded rectangle.
*/
JsVar *jswrap_graphics_fillRect(JsVar *parent, JsVar *opt, int y1, int x2, int y2) {
return _jswrap_graphics_fillRect_col(parent,opt,y1,x2,y2,true);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "clearRect",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_clearRect",
"params" : [
["x1","JsVar","The left X coordinate OR an object containing `{x,y,x2,y2}` or `{x,y,w,h}`"],
["y1","int32","The top Y coordinate"],
["x2","int32","The right X coordinate"],
["y2","int32","The bottom Y coordinate"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : [
"clearRect(x1: number, y1: number, x2: number, y2: number): Graphics;",
"clearRect(rect: { x: number, y: number, x2: number, y2: number } | { x: number, y: number, w: number, h: number }): Graphics;"
]
}
Fill a rectangular area in the Background Color
On devices with enough memory, you can specify `{x,y,x2,y2,r}` as the first
argument, which allows you to draw a rounded rectangle.
*/
JsVar *jswrap_graphics_clearRect(JsVar *parent, JsVar *opt, int y1, int x2, int y2) {
return _jswrap_graphics_fillRect_col(parent,opt,y1,x2,y2,false);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "drawRect",
"generate" : "jswrap_graphics_drawRect",
"params" : [
["x1","JsVar","The left X coordinate OR an object containing `{x,y,x2,y2}` or `{x,y,w,h}`"],
["y1","int32","The top Y coordinate"],
["x2","int32","The right X coordinate"],
["y2","int32","The bottom Y coordinate"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : [
"drawRect(x1: number, y1: number, x2: number, y2: number): Graphics;",
"drawRect(rect: { x: number, y: number, x2: number, y2: number } | { x: number, y: number, w: number, h: number }): Graphics;"
]
}
Draw an unfilled rectangle 1px wide in the Foreground Color
*/
JsVar *jswrap_graphics_drawRect(JsVar *parent, JsVar *opt, int y1, int x2, int y2) {
int x1, r;
_jswrap_graphics_getRect(opt, &x1, &y1, &x2, &y2, &r);
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
graphicsDrawRect(&gfx, x1,y1,x2,y2);
graphicsSetVar(&gfx); // gfx data changed because modified area
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "fillCircle",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_fillCircle",
"params" : [
["x","int32","The X axis"],
["y","int32","The Y axis"],
["rad","int32","The circle radius"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Draw a filled circle in the Foreground Color
*/
JsVar *jswrap_graphics_fillCircle(JsVar *parent, int x, int y, int rad) {
return jswrap_graphics_fillEllipse(parent, x-rad, y-rad, x+rad, y+rad);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "fillAnnulus",
"#if" : "defined(DICKENS)",
"generate" : "jswrap_graphics_fillAnnulus",
"params" : [
["x","int32","The X axis"],
["y","int32","The Y axis"],
["r1","int32","The annulus inner radius"],
["r2","int32","The annulus outer radius"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Draw a filled annulus in the Foreground Color
*/
// JsVar *jswrap_graphics_fillAnnulus(JsVar *parent, int x, int y, int r1, int r2, unsigned short quadrants) { // Too many arguments!
JsVar *jswrap_graphics_fillAnnulus(JsVar *parent, int x, int y, int r1, int r2) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
unsigned short quadrants = 0x0F; // Just do all quadrants for now
graphicsFillAnnulus(&gfx, x,y,r1,r2,quadrants);
graphicsSetVar(&gfx); // gfx data changed because modified area
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "drawCircle",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_drawCircle",
"params" : [
["x","int32","The X axis"],
["y","int32","The Y axis"],
["rad","int32","The circle radius"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Draw an unfilled circle 1px wide in the Foreground Color
*/
JsVar *jswrap_graphics_drawCircle(JsVar *parent, int x, int y, int rad) {
return jswrap_graphics_drawEllipse(parent, x-rad, y-rad, x+rad, y+rad);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "drawCircleAA",
"ifdef" : "GRAPHICS_ANTIALIAS",
"generate" : "jswrap_graphics_drawCircleAA",
"params" : [
["x","int32","Centre x-coordinate"],
["y","int32","Centre y-coordinate"],
["r","int32","Radius"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Draw a circle, centred at (x,y) with radius r in the current foreground color
*/
#ifdef GRAPHICS_ANTIALIAS
JsVar *jswrap_graphics_drawCircleAA(JsVar *parent, int x, int y, int r) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
graphicsDrawCircleAA(&gfx, x,y,r);
graphicsSetVar(&gfx); // gfx data changed because modified area
return jsvLockAgain(parent);
}
#endif
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "fillEllipse",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_fillEllipse",
"params" : [
["x1","int32","The left X coordinate"],
["y1","int32","The top Y coordinate"],
["x2","int32","The right X coordinate"],
["y2","int32","The bottom Y coordinate"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Draw a filled ellipse in the Foreground Color
*/
JsVar *jswrap_graphics_fillEllipse(JsVar *parent, int x, int y, int x2, int y2) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
graphicsFillEllipse(&gfx, x,y,x2,y2);
graphicsSetVar(&gfx); // gfx data changed because modified area
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "drawEllipse",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_drawEllipse",
"params" : [
["x1","int32","The left X coordinate"],
["y1","int32","The top Y coordinate"],
["x2","int32","The right X coordinate"],
["y2","int32","The bottom Y coordinate"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Draw an ellipse in the Foreground Color
*/
JsVar *jswrap_graphics_drawEllipse(JsVar *parent, int x, int y, int x2, int y2) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
graphicsDrawEllipse(&gfx, x,y,x2,y2);
graphicsSetVar(&gfx); // gfx data changed because modified area
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "getPixel",
"generate" : "jswrap_graphics_getPixel",
"params" : [
["x","int32","The left"],
["y","int32","The top"]
],
"return" : ["int32","The color"]
}
Get a pixel's color
*/
int jswrap_graphics_getPixel(JsVar *parent, int x, int y) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
return (int)graphicsGetPixel(&gfx, x, y);
}
/*TYPESCRIPT
type ColorResolvable = number | `#${string}`;
*/
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "setPixel",
"generate" : "jswrap_graphics_setPixel",
"params" : [
["x","int32","The left"],
["y","int32","The top"],
["col","JsVar","The color (if `undefined`, the foreground color is useD)"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : "setPixel(x: number, y: number, col?: ColorResolvable): Graphics;"
}
Set a pixel's color
*/
JsVar *jswrap_graphics_setPixel(JsVar *parent, int x, int y, JsVar *color) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
unsigned int col = gfx.data.fgColor;
if (!jsvIsUndefined(color))
col = jswrap_graphics_toColor(parent,color,0,0);
graphicsSetPixel(&gfx, x, y, col);
gfx.data.cursorX = (short)x;
gfx.data.cursorY = (short)y;
graphicsSetVar(&gfx); // gfx data changed because modified area
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "toColor",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_toColor",
"params" : [
["r","JsVar","Red (between 0 and 1) **OR** an integer representing the color in the current bit depth and color order **OR** a hexidecimal color string of the form `'#rrggbb' or `'#rgb'`"],
["g","JsVar","Green (between 0 and 1)"],
["b","JsVar","Blue (between 0 and 1)"]
],
"return" : ["int","The color index represented by the arguments"],
"typescript" : [
"toColor(r: number, g: number, b: number): number;",
"toColor(col: ColorResolvable): number;"
]
}
Work out the color value to be used in the current bit depth based on the arguments.
This is used internally by setColor and setBgColor
```
// 1 bit
g.toColor(1,1,1) => 1
// 16 bit
g.toColor(1,0,0) => 0xF800
```
*/
unsigned int jswrap_graphics_toColor(JsVar *parent, JsVar *r, JsVar *g, JsVar *b) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
unsigned int color = 0;
#ifdef SAVE_ON_FLASH
if (false) {
#else
JsVarFloat rf, gf, bf;
if (jsvIsString(r)) {
char buf[9];
memset(buf,0,sizeof(buf));
jsvGetString(r,buf,sizeof(buf));
if (buf[4]==0) {
rf = chtod(buf[1])/15.0;
gf = chtod(buf[2])/15.0;
bf = chtod(buf[3])/15.0;
} else {
rf = hexToByte(buf[1],buf[2])/255.0;
gf = hexToByte(buf[3],buf[4])/255.0;
bf = hexToByte(buf[5],buf[6])/255.0;
}
if (rf<0 || gf<0 || bf<0 || buf[7]!=0) {
jsExceptionHere(JSET_ERROR, "If Color is a String, it must be of the form '#rrggbb' or '#rgb'");
return 0;
}
} else {
rf = jsvGetFloat(r);
gf = jsvGetFloat(g);
bf = jsvGetFloat(b);
}
if (isfinite(rf) && isfinite(gf) && isfinite(bf)) {
int ri = (int)(rf*256);
int gi = (int)(gf*256);
int bi = (int)(bf*256);
if (ri>255) ri=255;
if (gi>255) gi=255;
if (bi>255) bi=255;
if (ri<0) ri=0;
if (gi<0) gi=0;
if (bi<0) bi=0;
// Check if we need to twiddle colors
int colorMask = gfx.data.flags & JSGRAPHICSFLAGS_COLOR_MASK;
if (colorMask) {
int tmpr, tmpg, tmpb;
tmpr = ri;
tmpg = gi;
tmpb = bi;
switch (colorMask) {
case JSGRAPHICSFLAGS_COLOR_BRG:
ri = tmpb;
gi = tmpr;
bi = tmpg;
break;
case JSGRAPHICSFLAGS_COLOR_BGR:
ri = tmpb;
bi = tmpr;
break;
case JSGRAPHICSFLAGS_COLOR_GBR:
ri = tmpg;
gi = tmpb;
bi = tmpr;
break;
case JSGRAPHICSFLAGS_COLOR_GRB:
ri = tmpg;
gi = tmpr;
break;
case JSGRAPHICSFLAGS_COLOR_RBG:
gi = tmpb;
bi = tmpg;
break;
default: break;
}
}
if (gfx.data.bpp==16) {
color = (unsigned int)((bi>>3) | (gi>>2)<<5 | (ri>>3)<<11);
#ifdef ESPR_GRAPHICS_12BIT
} else if (gfx.data.bpp==12) {
color = (unsigned int)((bi>>4) | (gi>>4)<<4 | (ri>>4)<<8);
#endif
} else if (gfx.data.bpp==32) {
color = 0xFF000000 | (unsigned int)(bi | (gi<<8) | (ri<<16));
} else if (gfx.data.bpp==24) {
color = (unsigned int)(bi | (gi<<8) | (ri<<16));
#if defined(GRAPHICS_PALETTED_IMAGES)
} else if (gfx.data.bpp==4) {
// LCD is paletted - look up in our palette to find the best match
int d = 0x7FFFFFFF;
color = 0;
for (unsigned int i=0;i<16;i++) {
int p = PALETTE_4BIT[i];
int pr = (p>>8)&0xF8;
int pg = (p>>3)&0xFC;
int pb = (p<<3)&0xF8;
pr |= pr>>5;
pg |= pb>>6;
pb |= pb>>5;
int dr = pr-ri;
int dg = pg-gi;
int db = pb-bi;
int dd = dr*dr+dg*dg+db*db;
if (dd<d) {
d=dd;
color=i;
}
}
} else if (gfx.data.bpp==8) {
// LCD is paletted - look up in our palette to find the best match
// TODO: For web palette we should be able to cheat without searching...
int d = 0x7FFFFFFF;
color = 0;
for (int i=0;i<255;i++) {
int p = PALETTE_8BIT[i];
int pr = (p>>8)&0xF8;
int pg = (p>>3)&0xFC;
int pb = (p<<3)&0xF8;
pr |= pr>>5;
pg |= pb>>6;
pb |= pb>>5;
int dr = pr-ri;
int dg = pg-gi;
int db = pb-bi;
int dd = dr*dr+dg*dg+db*db;
if (dd<d) {
d=dd;
color=(unsigned int)i;
}
}
#endif
} else
color = (unsigned int)(((ri+gi+bi)>=384) ? 0xFFFFFFFF : 0);
#endif
} else {
// just rgb
color = (unsigned int)jsvGetInteger(r);
}
return color;
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "blendColor",
"ifdef" : "GRAPHICS_ANTIALIAS",
"generate" : "jswrap_graphics_blendColor",
"params" : [
["col_a","JsVar","Color to blend from (either a single integer color value, or a string)"],
["col_b","JsVar","Color to blend to (either a single integer color value, or a string)"],
["amt","JsVar","The amount to blend. 0=col_a, 1=col_b, 0.5=halfway between (and so on)"]
],
"return" : ["int","The color index represented by the blended colors"],
"typescript" : "blendColor(col_a: ColorResolvable, col_b: ColorResolvable, amt: number): number;"
}
Blend between two colors, and return the result.
```
// dark yellow - halfway between red and green
var col = g.blendColor("#f00","#0f0", 0.5);
// Get a color 25% brighter than the theme's background colour
var col = g.blendColor(g.theme.fg,g.theme.bg, 0.75);
// then...
g.setColor(col).fillRect(10,10,100,100);
```
*/
unsigned int jswrap_graphics_blendColor(JsVar *parent, JsVar *ca, JsVar *cb, JsVar* amt) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
unsigned int a = jswrap_graphics_toColor(parent, ca, NULL, NULL);
unsigned int b = jswrap_graphics_toColor(parent, cb, NULL, NULL);
return graphicsBlendColor(&gfx, b, a, (int)(jsvGetFloat(amt)*256));
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "setColor",
"generate_full" : "jswrap_graphics_setColorX(parent, r,g,b, true)",
"params" : [
["r","JsVar","Red (between 0 and 1) **OR** an integer representing the color in the current bit depth and color order **OR** a hexidecimal color string of the form `'#012345'`"],
["g","JsVar","[optional] Green (between 0 and 1)"],
["b","JsVar","[optional] Blue (between 0 and 1)"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : [
"setColor(r: number, g: number, b: number): Graphics;",
"setColor(col: ColorResolvable): Graphics;"
]
}
Set the color to use for subsequent drawing operations.
If just `r` is specified as an integer, the numeric value will be written directly into a pixel. eg. On a 24 bit `Graphics` instance you set bright blue with either `g.setColor(0,0,1)` or `g.setColor(0x0000FF)`.
A good shortcut to ensure you get white on all platforms is to use `g.setColor(-1)`
The mapping is as follows:
* 32 bit: `r,g,b` => `0xFFrrggbb`
* 24 bit: `r,g,b` => `0xrrggbb`
* 16 bit: `r,g,b` => `0brrrrrggggggbbbbb` (RGB565)
* Other bpp: `r,g,b` => white if `r+g+b > 50%`, otherwise black (use `r` on its own as an integer)
If you specified `color_order` when creating the `Graphics` instance, `r`,`g` and `b` will be swapped as you specified.
**Note:** On devices with low flash memory, `r` **must** be an integer representing the color in the current bit depth. It cannot
be a floating point value, and `g` and `b` are ignored.
*/
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "setBgColor",
"generate_full" : "jswrap_graphics_setColorX(parent, r,g,b, false)",
"params" : [
["r","JsVar","Red (between 0 and 1) **OR** an integer representing the color in the current bit depth and color order **OR** a hexidecimal color string of the form `'#012345'`"],
["g","JsVar","Green (between 0 and 1)"],
["b","JsVar","Blue (between 0 and 1)"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : [
"setBgColor(r: number, g: number, b: number): Graphics;",
"setBgColor(col: ColorResolvable): Graphics;"
]
}
Set the background color to use for subsequent drawing operations.
See `Graphics.setColor` for more information on the mapping of `r`, `g`, and `b` to pixel values.
**Note:** On devices with low flash memory, `r` **must** be an integer representing the color in the current bit depth. It cannot
be a floating point value, and `g` and `b` are ignored.
*/
JsVar *jswrap_graphics_setColorX(JsVar *parent, JsVar *r, JsVar *g, JsVar *b, bool isForeground) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
unsigned int color = jswrap_graphics_toColor(parent,r,g,b);
if (isForeground)
gfx.data.fgColor = color;
else
gfx.data.bgColor = color;
graphicsSetVar(&gfx);
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "getColor",
"generate_full" : "jswrap_graphics_getColorX(parent, true)",
"return" : ["int","The integer value of the colour"]
}
Get the color to use for subsequent drawing operations
*/
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "getBgColor",
"generate_full" : "jswrap_graphics_getColorX(parent, false)",
"return" : ["int","The integer value of the colour"]
}
Get the background color to use for subsequent drawing operations
*/
JsVarInt jswrap_graphics_getColorX(JsVar *parent, bool isForeground) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
return (JsVarInt)((isForeground ? gfx.data.fgColor : gfx.data.bgColor) & ((1UL<<gfx.data.bpp)-1));
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "setClipRect",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_setClipRect",
"params" : [
["x1","int","Top left X coordinate"],
["y1","int","Top left Y coordinate"],
["x2","int","Bottom right X coordinate"],
["y2","int","Bottom right Y coordinate"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
This sets the 'clip rect' that subsequent drawing operations are clipped to sit
between.
These values are inclusive - e.g. `g.setClipRect(1,0,5,0)` will ensure that only
pixel rows 1,2,3,4,5 are touched on column 0.
**Note:** For maximum flexibility on Bangle.js 1, the values here are not range
checked. For normal use, X and Y should be between 0 and
`getWidth()-1`/`getHeight()-1`.
**Note:** The x/y values here are rotated, so that if `Graphics.setRotation` is
used they correspond to the coordinates given to the draw functions, *not to the
physical device pixels*.
*/
JsVar *jswrap_graphics_setClipRect(JsVar *parent, int x1, int y1, int x2, int y2) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
graphicsToDeviceCoordinates(&gfx, &x1, &y1);
graphicsToDeviceCoordinates(&gfx, &x2, &y2);
#ifndef SAVE_ON_FLASH
#ifdef USE_LCD_ST7789_8BIT
if (gfx.data.type!=JSGRAPHICSTYPE_ST7789_8BIT) {
#endif
if (x1<0) x1=0;
if (y1<0) y1=0;
if (x2<0) x2=0;
if (y2<0) y2=0;
if (x1>=gfx.data.width) x1=gfx.data.width-1;
if (y1>=gfx.data.height) y1=gfx.data.height-1;
if (x2>=gfx.data.width) x2=gfx.data.width-1;
if (y2>=gfx.data.height) y2=gfx.data.height-1;
if (x1>x2) { int t=x1;x1=x2;x2=t; };
if (y1>y2) { int t=y1;y1=y2;y2=t; };
#ifdef USE_LCD_ST7789_8BIT
}
#endif
gfx.data.clipRect.x1 = (unsigned short)x1;
gfx.data.clipRect.y1 = (unsigned short)y1;
gfx.data.clipRect.x2 = (unsigned short)x2;
gfx.data.clipRect.y2 = (unsigned short)y2;
graphicsSetVar(&gfx);
#endif
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "setFontBitmap",
"generate_full" : "jswrap_graphics_setFontSizeX(parent, 1+JSGRAPHICS_FONTSIZE_4X6, false)",
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Make subsequent calls to `drawString` use the built-in 4x6 pixel bitmapped Font
It is recommended that you use `Graphics.setFont("4x6")` for more flexibility.
*/
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "setFontVector",
"ifndef" : "SAVE_ON_FLASH",
"generate_full" : "jswrap_graphics_setFontSizeX(parent, size, true)",
"params" : [
["size","int32","The height of the font, as an integer"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Make subsequent calls to `drawString` use a Vector Font of the given height.
It is recommended that you use `Graphics.setFont("Vector", size)` for more
flexibility.
*/
JsVar *jswrap_graphics_setFontSizeX(JsVar *parent, int size, bool isVectorFont) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
#ifdef NO_VECTOR_FONT
if (isVectorFont)
jsExceptionHere(JSET_ERROR, "No vector font in this build");
#else
if (isVectorFont) {
if (size<1) size=1;
if (size>JSGRAPHICS_FONTSIZE_SCALE_MASK) size=JSGRAPHICS_FONTSIZE_SCALE_MASK;
}
#ifndef SAVE_ON_FLASH
if ((gfx.data.fontSize&JSGRAPHICS_FONTSIZE_CUSTOM_BIT) &&
!(size&JSGRAPHICS_FONTSIZE_CUSTOM_BIT)) {
jsvObjectRemoveChild(parent, JSGRAPHICS_CUSTOMFONT_BMP);
jsvObjectRemoveChild(parent, JSGRAPHICS_CUSTOMFONT_WIDTH);
jsvObjectRemoveChild(parent, JSGRAPHICS_CUSTOMFONT_HEIGHT);
jsvObjectRemoveChild(parent, JSGRAPHICS_CUSTOMFONT_FIRSTCHAR);
}
#endif
gfx.data.fontSize = (unsigned short)size;
graphicsSetVar(&gfx);
#endif
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "setFontCustom",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_setFontCustom",
"params" : [
["bitmap","JsVar","A column-first, MSB-first, 1bpp bitmap containing the font bitmap"],
["firstChar","int32","The first character in the font - usually 32 (space)"],
["width","JsVar","The width of each character in the font. Either an integer, or a string where each character represents the width"],
["height","int32","The height as an integer (max 255). Bits 8-15 represent the scale factor (eg. `2<<8` is twice the size). Bits 16-23 represent the BPP (0,1=1 bpp, 2=2 bpp, 4=4 bpp)"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : "setFontCustom(bitmap: ArrayBuffer, firstChar: number, width: number | string, height: number): Graphics;"
}
Make subsequent calls to `drawString` use a Custom Font of the given height. See
the [Fonts page](http://www.espruino.com/Fonts) for more information about
custom fonts and how to create them.
For examples of use, see the [font
modules](https://www.espruino.com/Fonts#font-modules).
**Note:** while you can specify the character code of the first character with
`firstChar`, the newline character 13 will always be treated as a newline and
not rendered.
*/
#ifndef SAVE_ON_FLASH
JsVar *jswrap_graphics_setFontCustom(JsVar *parent, JsVar *bitmap, int firstChar, JsVar *width, int height) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
if (!jsvIsString(bitmap)) {
jsExceptionHere(JSET_ERROR, "Font bitmap must be a String");
return 0;
}
if (firstChar<0 || firstChar>255) {
jsExceptionHere(JSET_ERROR, "First character out of range");
return 0;
}
if (!jsvIsString(width) && !jsvIsInt(width)) {
jsExceptionHere(JSET_ERROR, "Font width must be a String or an integer");
return 0;
}
int scale = (height>>8)&255;
if (scale<1) scale=1;
int bpp = height>>16;
if (bpp<1) bpp=1;
JsGraphicsFontSize fontType;
if (bpp==1) fontType = JSGRAPHICS_FONTSIZE_CUSTOM_1BPP;
else if (bpp==2) fontType = JSGRAPHICS_FONTSIZE_CUSTOM_2BPP;
else if (bpp==4) fontType = JSGRAPHICS_FONTSIZE_CUSTOM_4BPP;
else {
jsExceptionHere(JSET_ERROR, "Invalid BPP - 1,2,4 supported");
return 0;
}
height = height&255;
jsvObjectSetChild(parent, JSGRAPHICS_CUSTOMFONT_BMP, bitmap);
jsvObjectSetChild(parent, JSGRAPHICS_CUSTOMFONT_WIDTH, width);
jsvObjectSetChildAndUnLock(parent, JSGRAPHICS_CUSTOMFONT_HEIGHT, jsvNewFromInteger(height));
jsvObjectSetChildAndUnLock(parent, JSGRAPHICS_CUSTOMFONT_FIRSTCHAR, jsvNewFromInteger(firstChar));
gfx.data.fontSize = (unsigned short)((unsigned)scale + fontType);
graphicsSetVar(&gfx);
return jsvLockAgain(parent);
}
#endif
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "setFontPBF",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_setFontPBF",
"params" : [
["file","JsVar","The font as a PBF file"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
*/
JsVar *jswrap_graphics_setFontPBF(JsVar *parent, JsVar *file) {
#ifdef ESPR_PBF_FONTS
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
if (!jsvIsString(file)) {
jsExceptionHere(JSET_ERROR, "Font must be a String");
return 0;
}
int scale = 1;
jsvObjectSetChild(parent, JSGRAPHICS_CUSTOMFONT_BMP, file);
gfx.data.fontSize = (unsigned short)(scale | JSGRAPHICS_FONTSIZE_CUSTOM_PBF);
graphicsSetVar(&gfx);
return jsvLockAgain(parent);
#else
jsExceptionHere(JSET_ERROR, "PBF Fonts not enabled on this build");
return 0;
#endif
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "setFontAlign",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_setFontAlign",
"params" : [
["x","int32","X alignment. -1=left (default), 0=center, 1=right"],
["y","int32","Y alignment. -1=top (default), 0=center, 1=bottom"],
["rotation","int32","Rotation of the text. 0=normal, 1=90 degrees clockwise, 2=180, 3=270"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : "setFontAlign(x: -1 | 0 | 1, y?: -1 | 0 | 1, rotation?: 0 | 1 | 2 | 3): Graphics;"
}
Set the alignment for subsequent calls to `drawString`
*/
JsVar *jswrap_graphics_setFontAlign(JsVar *parent, int x, int y, int r) {
#ifndef SAVE_ON_FLASH
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
if (x<-1) x=-1;
if (x>1) x=1;
if (y<-1) y=-1;
if (y>1) y=1;
if (r<0) r=0;
if (r>3) r=3;
gfx.data.fontAlignX = x;
gfx.data.fontAlignY = y;
gfx.data.fontRotate = r;
graphicsSetVar(&gfx);
return jsvLockAgain(parent);
#else
return 0;
#endif
}
/*TYPESCRIPT
type FontName =
| "4x4"
| "4x4Numeric"
| "4x5"
| "4x5Numeric"
| "4x8Numeric"
| "5x7Numeric7Seg"
| "5x9Numeric7Seg"
| "6x8"
| "6x12"
| "7x11Numeric7Seg"
| "8x12"
| "8x16"
| "Dennis8"
| "Cherry6x10"
| "Copasectic40x58Numeric"
| "Dylex7x13"
| "HaxorNarrow7x17"
| "Sinclair"
| "Teletext10x18Mode7"
| "Teletext5x9Ascii"
| "Teletext5x9Mode7"
| "Vector";
type FontNameWithScaleFactor =
| FontName
| `${FontName}:${number}`
| `${FontName}:${number}x${number}`;
*/
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "setFont",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_setFont",
"params" : [
["name","JsVar","The name of the font to use (if undefined, the standard 4x6 font will be used)"],
["size","int","The size of the font (or undefined)"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : [
"setFont(name: FontNameWithScaleFactor): Graphics;",
"setFont(name: FontName, size: number): Graphics;"
]
}
Set the font by name. Various forms are available:
* `g.setFont("4x6")` - standard 4x6 bitmap font
* `g.setFont("Vector:12")` - vector font 12px high
* `g.setFont("4x6:2")` - 4x6 bitmap font, doubled in size
* `g.setFont("6x8:2x3")` - 6x8 bitmap font, doubled in width, tripled in height
You can also use these forms, but they are not recommended:
* `g.setFont("Vector12")` - vector font 12px high
* `g.setFont("4x6",2)` - 4x6 bitmap font, doubled in size
`g.getFont()` will return the current font as a String.
For a list of available font names, you can use `g.getFonts()`.
*/
JsVar *jswrap_graphics_setFont(JsVar *parent, JsVar *fontId, int size) {
#ifndef SAVE_ON_FLASH
if (!jsvIsString(fontId)) return 0;
bool isVector = false;
int fontSizeCharIdx = -1;
#ifndef NO_VECTOR_FONT
if (jsvIsStringEqualOrStartsWith(fontId, "Vector", true)) {
if (jsvGetStringLength(fontId)>6)
fontSizeCharIdx = 6;
isVector = true;
}
#endif
// Is font is 'FontName:2' pull the font size out of it
int colonIdx = jsvGetStringIndexOf(fontId,':');
if (colonIdx>=0)
fontSizeCharIdx = colonIdx+1;
JsVar *name;
if (fontSizeCharIdx>=0) { // "FontName:FontSize"
JsVar *sizeVar = jsvNewFromStringVar(fontId, fontSizeCharIdx, JSVAPPENDSTRINGVAR_MAXLENGTH);
int xIndex = jsvGetStringIndexOf(sizeVar,'x');
if (xIndex>=0) { // "FontName:1x3"
int sizex = jsvGetIntegerAndUnLock(jsvNewFromStringVar(sizeVar, 0, xIndex));
int sizey = jsvGetIntegerAndUnLock(jsvNewFromStringVar(sizeVar, xIndex+1, JSVAPPENDSTRINGVAR_MAXLENGTH));
if (sizex<0) sizex=0;
if (sizey<0) sizey=0;
if (sizex>63) sizex=63;
if (sizey>63) sizey=63;
size = sizex | (sizey<<JSGRAPHICS_FONTSIZE_SCALE_Y_SHIFT) | JSGRAPHICS_FONTSIZE_SCALE_X_Y;
} else { // // "FontName:12"
size = jsvGetInteger(sizeVar);
}
jsvUnLock(sizeVar);
name = jsvNewFromStringVar(fontId, 0, (fontSizeCharIdx>0)?fontSizeCharIdx-1:0);
} else {
name = jsvLockAgain(fontId);
}
if (size<1) size=1;
if (size>JSGRAPHICS_FONTSIZE_SCALE_MASK) size=JSGRAPHICS_FONTSIZE_SCALE_MASK;
unsigned short sz = 0xFFFF; // the actual data mask
if (isVector) {
sz = size;
} else if (jsvIsUndefined(name) || jsvGetStringLength(name)==0 || jsvIsStringEqual(name, "4x6"))
sz = (unsigned short)(size + JSGRAPHICS_FONTSIZE_4X6);
#ifdef USE_FONT_6X8
if (jsvIsStringEqual(name, "6x8"))
sz = (unsigned short)(size + JSGRAPHICS_FONTSIZE_6X8);
#endif
#ifndef SAVE_ON_FLASH
// if function named 'setFontXYZ' exists, run it
if (sz==0xFFFF) {
JsVar *setterName = jsvVarPrintf("setFont%v",name);
JsVar *fontSetter = jspGetVarNamedField(parent,setterName,false);
if (fontSetter) {
jsvUnLock(jspExecuteFunction(fontSetter,parent,0,NULL));
JsGraphics gfx; graphicsGetFromVar(&gfx, parent); // graphicsGetFromVar *may* fail, but all it means is size won't get set (and jswrap_graphics_setFontSizeX would fail too)
sz = (unsigned short)(size | (gfx.data.fontSize & JSGRAPHICS_FONTSIZE_FONT_MASK));
}
jsvUnLock2(fontSetter,setterName);
}
#endif
if (sz==0xFFFF) {
jsExceptionHere(JSET_ERROR, "Unknown font %j", name);
}
jsvUnLock(name);
return jswrap_graphics_setFontSizeX(parent, sz, isVector);
#else
return 0;
#endif
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "getFont",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_getFont",
"return" : ["JsVar","Get the name of the current font"],
"return_object" : "String",
"typescript" : "getFont(): FontNameWithScaleFactor | \"Custom\""
}
Get the font by name - can be saved and used with `Graphics.setFont`.
Normally this might return something like `"4x6"`, but if a scale factor is
specified, a colon and then the size is reported, like "4x6:2"
**Note:** For custom fonts, `Custom` is currently reported instead of the font
name.
*/
JsVar *jswrap_graphics_getFont(JsVar *parent) {
#ifndef SAVE_ON_FLASH
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
JsGraphicsFontSize f = gfx.data.fontSize & JSGRAPHICS_FONTSIZE_FONT_MASK;
const char *name = 0;
#ifndef NO_VECTOR_FONT
if (f == JSGRAPHICS_FONTSIZE_VECTOR) {
name = "Vector";
}
#endif
if (f == JSGRAPHICS_FONTSIZE_4X6)
name = "4x6";
#ifdef USE_FONT_6X8
if (f == JSGRAPHICS_FONTSIZE_6X8)
name = "6x8";
#endif
#ifndef SAVE_ON_FLASH
if (f & JSGRAPHICS_FONTSIZE_CUSTOM_BIT) {
/*JsVar *n = jsvObjectGetChildIfExists(parent, JSGRAPHICS_CUSTOMFONT_NAME);
if (n) return n;*/
name = "Custom";
}
if (name) {
int scale = gfx.data.fontSize & JSGRAPHICS_FONTSIZE_SCALE_MASK;
if (scale & JSGRAPHICS_FONTSIZE_SCALE_X_Y)
return jsvVarPrintf("%s:%dx%d",name, scale&JSGRAPHICS_FONTSIZE_SCALE_X_MASK, (scale & JSGRAPHICS_FONTSIZE_SCALE_Y_MASK)>>JSGRAPHICS_FONTSIZE_SCALE_Y_SHIFT);
else if (scale>1) return jsvVarPrintf("%s:%d",name, scale);
else return jsvNewFromString(name);
}
#endif
return jsvNewFromInteger(gfx.data.fontSize);
#else
return 0;
#endif
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "getFonts",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_getFonts",
"return" : ["JsVar","And array of font names"],
"return_object" : "Array",
"typescript" : "getFonts(): FontName[];"
}
Return an array of all fonts currently in the Graphics library.
**Note:** Vector fonts are specified as `Vector#` where `#` is the font height.
As there are effectively infinite fonts, just `Vector` is included in the list.
*/
void jswrap_graphics_getFonts_callback(void *cbdata, JsVar *key) {
JsVar *result = (JsVar*)cbdata;
if (jsvGetStringLength(key)>7 &&
jsvIsStringEqualOrStartsWith(key, "setFont", true)) {
if (jsvIsStringEqual(key,"setFontBitmap") ||
jsvIsStringEqual(key,"setFontAlign") ||
jsvIsStringEqual(key,"setFontCustom")) return; // throw away non-font functions
JsVar *n = jsvNewFromStringVar(key, 7, JSVAPPENDSTRINGVAR_MAXLENGTH);
jsvArrayPush(result, n);
jsvUnLock(n);
}
}
JsVar *jswrap_graphics_getFonts(JsVar *parent) {
#ifndef SAVE_ON_FLASH
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
JsVar *arr = jsvNewEmptyArray();
if (!arr) return 0;
jsvArrayPushAndUnLock(arr, jsvNewFromString("4x6"));
#ifdef USE_FONT_6X8
jsvArrayPushAndUnLock(arr, jsvNewFromString("6x8"));
#endif
// vector font is added by below..
// scan for any functions 'setFont*' and add those names
jswrap_object_keys_or_property_names_cb(parent, JSWOKPF_INCLUDE_NON_ENUMERABLE|JSWOKPF_INCLUDE_PROTOTYPE, jswrap_graphics_getFonts_callback, arr);
return arr;
#else
return 0;
#endif
}
typedef struct {
JsGraphicsFontSize font;
unsigned short scale;
unsigned short scalex, scaley;
unsigned char customFirstChar;
JsVar *widths; // Array of widths (for old-style fonts) or just an int for fixed width
JsVar *bitmap; // Bitmap/font data
#ifdef ESPR_PBF_FONTS
PbfFontLoaderInfo pbfInfo;
#endif
} JsGraphicsFontInfo;
static void _jswrap_graphics_getFontInfo(JsGraphics *gfx, JsGraphicsFontInfo *info) {
info->font = gfx->data.fontSize & JSGRAPHICS_FONTSIZE_FONT_MASK;
info->scale = gfx->data.fontSize & JSGRAPHICS_FONTSIZE_SCALE_MASK;
info->scalex = info->scale;
info->scaley = info->scale;
if (info->scale & JSGRAPHICS_FONTSIZE_SCALE_X_Y) {
info->scalex = info->scale & JSGRAPHICS_FONTSIZE_SCALE_X_MASK;
info->scaley = (info->scale & JSGRAPHICS_FONTSIZE_SCALE_Y_MASK) >> JSGRAPHICS_FONTSIZE_SCALE_Y_SHIFT;
}
#ifndef SAVE_ON_FLASH
if (info->font & JSGRAPHICS_FONTSIZE_CUSTOM_BIT) {
info->customFirstChar = (unsigned char)jsvGetIntegerAndUnLock(jsvObjectGetChildIfExists(gfx->graphicsVar, JSGRAPHICS_CUSTOMFONT_FIRSTCHAR));
info->widths = jsvObjectGetChildIfExists(gfx->graphicsVar, JSGRAPHICS_CUSTOMFONT_WIDTH);
info->bitmap = jsvObjectGetChildIfExists(gfx->graphicsVar, JSGRAPHICS_CUSTOMFONT_BMP);
#ifdef ESPR_PBF_FONTS
if ((info->font & JSGRAPHICS_FONTSIZE_FONT_MASK) == JSGRAPHICS_FONTSIZE_CUSTOM_PBF)
jspbfFontNew(&info->pbfInfo, info->bitmap);
#endif
} else
#endif
info->customFirstChar = 0;
}
static void _jswrap_graphics_freeFontInfo(JsGraphicsFontInfo *info) {
#ifndef SAVE_ON_FLASH
if (info->font & JSGRAPHICS_FONTSIZE_CUSTOM_BIT) {
jsvUnLock(info->widths);
jsvUnLock(info->bitmap);
#ifdef ESPR_PBF_FONTS
if ((info->font & JSGRAPHICS_FONTSIZE_FONT_MASK) == JSGRAPHICS_FONTSIZE_CUSTOM_PBF)
jspbfFontFree(&info->pbfInfo);
#endif
}
#endif
}
static int _jswrap_graphics_getCharWidth(JsGraphics *gfx, JsGraphicsFontInfo *info, int ch) {
if ((info->font == JSGRAPHICS_FONTSIZE_VECTOR) && (ch<256)) {
#ifndef NO_VECTOR_FONT
return (int)graphicsVectorCharWidth(info->scalex, (char)ch);
#endif
} else if ((info->font == JSGRAPHICS_FONTSIZE_4X6) && (ch<256)) {
return 4*info->scalex;
#ifdef USE_FONT_6X8
} else if ((info->font == JSGRAPHICS_FONTSIZE_6X8) && (ch<256)) {
return 6*info->scalex;
#endif
#ifndef SAVE_ON_FLASH
} else if (info->font & JSGRAPHICS_FONTSIZE_CUSTOM_BIT) {
#ifdef ESPR_PBF_FONTS
if ((info->font & JSGRAPHICS_FONTSIZE_FONT_MASK)==JSGRAPHICS_FONTSIZE_CUSTOM_PBF) {
PbfFontLoaderGlyph result;
if (jspbfFontFindGlyph(&info->pbfInfo, ch, &result))
return result.advance;
else
return 0;
}
#endif
int w = 0;
if (ch<256) {
if (jsvIsString(info->widths)) {
if (ch>=info->customFirstChar)
w = info->scalex*(unsigned char)jsvGetCharInString(info->widths, (size_t)(ch-info->customFirstChar));
} else
w = info->scalex*(int)jsvGetInteger(info->widths);
}
return w;
#endif
}
return 0;
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "getFontHeight",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_getFontHeight",
"return" : ["int","The height in pixels of the current font"]
}
Return the height in pixels of the current font
*/
static int _jswrap_graphics_getFontHeightInternal(JsGraphics *gfx, JsGraphicsFontInfo *info) {
if (info->font == JSGRAPHICS_FONTSIZE_VECTOR) {
return info->scaley;
} else if (info->font == JSGRAPHICS_FONTSIZE_4X6) {
return 6*info->scaley;
#ifdef USE_FONT_6X8
} else if (info->font == JSGRAPHICS_FONTSIZE_6X8) {
return 8*info->scaley;
#endif
#ifndef SAVE_ON_FLASH
} else if (info->font & JSGRAPHICS_FONTSIZE_CUSTOM_BIT) {
#ifdef ESPR_PBF_FONTS
if ((info->font & JSGRAPHICS_FONTSIZE_FONT_MASK)==JSGRAPHICS_FONTSIZE_CUSTOM_PBF)
return info->pbfInfo.lineHeight;
#endif
return info->scaley*(int)jsvGetIntegerAndUnLock(jsvObjectGetChildIfExists(gfx->graphicsVar, JSGRAPHICS_CUSTOMFONT_HEIGHT));
#endif
}
return 0;
}
int jswrap_graphics_getFontHeight(JsVar *parent) {
#ifndef SAVE_ON_FLASH
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
JsGraphicsFontInfo info;
_jswrap_graphics_getFontInfo(&gfx, &info);
int h = _jswrap_graphics_getFontHeightInternal(&gfx, &info);
_jswrap_graphics_freeFontInfo(&info);
return h;
#else
return 0;
#endif
}
/** Work out the width and height of a bit of text. If 'lineStartIndex' is -1 the whole string is used
* otherwise *just* the line of text starting at that char index is used
*/
void _jswrap_graphics_stringMetrics(JsGraphics *gfx, JsVar *var, int lineStartIndex, int *stringWidth, int *stringHeight) {
JsGraphicsFontInfo info;
_jswrap_graphics_getFontInfo(gfx, &info);
int fontHeight = _jswrap_graphics_getFontHeightInternal(gfx, &info);
JsVar *str = jsvAsString(var);
JsvStringIterator it;
jsvStringIteratorNewUTF8(&it, str, (lineStartIndex<0)?0:lineStartIndex);
int width = 0;
int height = fontHeight;
int maxWidth = 0;
while (jsvStringIteratorHasChar(&it)) {
int ch = jsvStringIteratorGetUTF8CharAndNext(&it);
if (ch=='\n') {
if (width>maxWidth) maxWidth=width;
width = 0;
height += fontHeight;
if (lineStartIndex>=0) break; // only do one line
}
#ifndef SAVE_ON_FLASH
if (ch==0) { // If images are described in-line in the string, render them
GfxDrawImageInfo img;
size_t idx = jsvConvertToUTF8Index(str, jsvStringIteratorGetIndex(&it));
if (_jswrap_graphics_parseImage(gfx, str, idx, &img)) {
jsvStringIteratorGotoUTF8(&it, str, idx+img.headerLength+img.bitmapLength);
_jswrap_graphics_freeImageInfo(&img);
// string iterator now points to the next char after image
width += img.width;
}
continue;
}
#endif
width += _jswrap_graphics_getCharWidth(gfx, &info, ch);
}
jsvStringIteratorFree(&it);
jsvUnLock(str);
if (stringWidth) *stringWidth = width>maxWidth ? width : maxWidth;
if (stringHeight) *stringHeight = height;
_jswrap_graphics_freeFontInfo(&info);
}
JsVarInt _jswrap_graphics_stringWidth(JsGraphics *gfx, JsVar *var, int lineStartIndex) {
int w,h;
_jswrap_graphics_stringMetrics(gfx, var, lineStartIndex, &w, &h);
return w;
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "stringWidth",
"generate" : "jswrap_graphics_stringWidth",
"params" : [
["str","JsVar","The string"]
],
"return" : ["int","The length of the string in pixels"],
"typescript" : "stringWidth(str: string): number;"
}
Return the size in pixels of a string of text in the current font
*/
JsVarInt jswrap_graphics_stringWidth(JsVar *parent, JsVar *var) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
return _jswrap_graphics_stringWidth(&gfx, var, -1);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "stringMetrics",
"generate" : "jswrap_graphics_stringMetrics",
"params" : [
["str","JsVar","The string"]
],
"return" : ["JsVar","An object containing `{width,height}` of the string"],
"typescript" : "stringMetrics(str: string): { width: number, height: number };"
}
Return the width and height in pixels of a string of text in the current font
*/
JsVar* jswrap_graphics_stringMetrics(JsVar *parent, JsVar *var) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
int w,h;
JsVar *o = jsvNewObject();
if (o) {
_jswrap_graphics_stringMetrics(&gfx, var, -1, &w, &h);
jsvObjectSetChildAndUnLock(o, "width", jsvNewFromInteger(w));
jsvObjectSetChildAndUnLock(o, "height", jsvNewFromInteger(h));
}
return o;
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "wrapString",
"generate" : "jswrap_graphics_wrapString",
"params" : [
["str","JsVar","The string"],
["maxWidth","int","The width in pixels"]
],
"return" : ["JsVar","An array of lines that are all less than `maxWidth`"],
"typescript" : "wrapString(str: string, maxWidth: number): string[];"
}
Wrap a string to the given pixel width using the current font, and return the
lines as an array.
To render within the screen's width you can do:
```
g.drawString(g.wrapString(text, g.getWidth()).join("\n")),
```
*/
JsVar *jswrap_graphics_wrapString(JsVar *parent, JsVar *str, int maxWidth) {
if (!str) return jsvNewEmptyArray();
if (maxWidth<=0) return 0;
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
JsGraphicsFontInfo info;
_jswrap_graphics_getFontInfo(&gfx, &info);
str = jsvAsString(str);
JsVar *lines = jsvNewEmptyArray();
JsVar *currentLine = jsvNewFromEmptyString();
#ifdef ESPR_UNICODE_SUPPORT
if (jsvIsUTF8String(str))
currentLine = jsvNewUTF8StringAndUnLock(currentLine);
#endif
int spaceWidth = _jswrap_graphics_getCharWidth(&gfx, &info, ' ');
int wordWidth = 0;
int lineWidth = 0;
int wordStartIdx = 0;
int wordIdxAtMaxWidth = 0; // index just before the word width>maxWidth
int wordWidthAtMaxWidth = 0; // index just before the word width>maxWidth
bool endOfText = false;
bool wasNewLine = false;
JsvStringIterator it;
jsvStringIteratorNewUTF8(&it, str, 0);
while (jsvStringIteratorHasChar(&it) || endOfText) {
int ch = jsvStringIteratorGetUTF8CharAndNext(&it);
if (endOfText || ch=='\n' || ch==' ') { // newline or space
int currentPos = (int)jsvStringIteratorGetIndex(&it);
if ((lineWidth + spaceWidth + wordWidth <= maxWidth) &&
!wasNewLine) {
// all on one line
if (lineWidth) {
jsvAppendString(currentLine, " ");
lineWidth += spaceWidth;
}
jsvAppendStringVar(currentLine, str, wordStartIdx, currentPos-(wordStartIdx+1));
lineWidth += wordWidth;
} else { // doesn't fit one one line - move to new line
lineWidth = wordWidth;
if (jsvGetStringLength(currentLine) || wasNewLine) {
jsvArrayPush(lines, currentLine);
}
jsvUnLock(currentLine);
if (wordIdxAtMaxWidth) {
// word is too long to fit on a line
currentLine = jsvNewFromStringVar(str, wordStartIdx, wordIdxAtMaxWidth-(wordStartIdx+1));
// jsvNewFromStringVar will create a unicode string is str was a unicode string
jsvArrayPushAndUnLock(lines, currentLine);
wordStartIdx = wordIdxAtMaxWidth-1;
lineWidth -= wordWidthAtMaxWidth;
}
currentLine = jsvNewFromStringVar(str, wordStartIdx, currentPos-(wordStartIdx+1));
// jsvNewFromStringVar will create a unicode string is str was a unicode string
}
wordWidth = 0;
wordIdxAtMaxWidth = 0;
wordStartIdx = currentPos;
wasNewLine = ch=='\n';
if (endOfText) break;
continue;
}
#ifndef SAVE_ON_FLASH
if (ch==0) { // If images are described in-line in the string, render them
GfxDrawImageInfo img;
size_t idx = jsvConvertToUTF8Index(str, jsvStringIteratorGetIndex(&it));
if (_jswrap_graphics_parseImage(&gfx, str, idx, &img)) {
jsvStringIteratorGotoUTF8(&it, str, idx+img.headerLength+img.bitmapLength);
_jswrap_graphics_freeImageInfo(&img);
// string iterator now points to the next char after image
wordWidth += img.width;
if (!jsvStringIteratorHasChar(&it)) endOfText=true;
}
continue;
}
#endif
int w = _jswrap_graphics_getCharWidth(&gfx, &info, ch);
if (wordWidth <= maxWidth && wordWidth+w>maxWidth) {
wordIdxAtMaxWidth = (int)jsvStringIteratorGetIndex(&it);
wordWidthAtMaxWidth = wordWidth;
}
wordWidth += w;
if (!jsvStringIteratorHasChar(&it)) endOfText=true;
}
jsvStringIteratorFree(&it);
// deal with final line
if (jsvGetStringLength(currentLine)) {
jsvArrayPush(lines, currentLine);
}
jsvUnLock2(str,currentLine);
_jswrap_graphics_freeFontInfo(&info);
return lines;
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "drawString",
"generate" : "jswrap_graphics_drawString",
"params" : [
["str","JsVar","The string"],
["x","int32","The X position of the leftmost pixel"],
["y","int32","The Y position of the topmost pixel"],
["solid","bool","For bitmap fonts, should empty pixels be filled with the background color?"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : "drawString(str: string, x: number, y: number, solid?: boolean): Graphics;"
}
Draw a string of text in the current font.
```
g.drawString("Hello World", 10, 10);
```
Images may also be embedded inside strings (e.g. to render Emoji or characters
not in the current font). To do this, just add `0` then the image string ([about
Images](http://www.espruino.com/Graphics#images-bitmaps)) For example:
```
g.drawString("Hi \0\7\5\1\x82 D\x17\xC0");
// draws:
// # # # # #
// # # #
// ### ## #
// # # # # #
// # # ### #####
```
*/
JsVar *jswrap_graphics_drawString(JsVar *parent, JsVar *var, int x, int y, bool solidBackground) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
JsGraphicsFontInfo info;
_jswrap_graphics_getFontInfo(&gfx, &info);
int fontHeight = _jswrap_graphics_getFontHeightInternal(&gfx, &info);
#ifndef SAVE_ON_FLASH
int customBPP = 1;
if (info.font & JSGRAPHICS_FONTSIZE_CUSTOM_BIT) {
if ((info.font&JSGRAPHICS_FONTSIZE_FONT_MASK)==JSGRAPHICS_FONTSIZE_CUSTOM_2BPP) customBPP = 2;
if ((info.font&JSGRAPHICS_FONTSIZE_FONT_MASK)==JSGRAPHICS_FONTSIZE_CUSTOM_4BPP) customBPP = 4;
}
#endif
#ifndef SAVE_ON_FLASH
// Handle text rotation
JsGraphicsFlags oldFlags = gfx.data.flags;
if (gfx.data.fontRotate==1) {
gfx.data.flags ^= JSGRAPHICSFLAGS_SWAP_XY | JSGRAPHICSFLAGS_INVERT_X;
int t = gfx.data.width - (x+1);
x = y;
y = t;
} else if (gfx.data.fontRotate==2) {
gfx.data.flags ^= JSGRAPHICSFLAGS_INVERT_X | JSGRAPHICSFLAGS_INVERT_Y;
x = gfx.data.width - (x+1);
y = gfx.data.height - (y+1);
} else if (gfx.data.fontRotate==3) {
gfx.data.flags ^= JSGRAPHICSFLAGS_SWAP_XY | JSGRAPHICSFLAGS_INVERT_Y;
int t = gfx.data.height - (y+1);
y = x;
x = t;
}
JsVar *str = jsvAsString(var);
// Handle font alignment
int startx = x;
if (gfx.data.fontAlignX<2) // 0=center, 1=right, 2=undefined, 3=left
x = startx - (_jswrap_graphics_stringWidth(&gfx, str, 0) * (gfx.data.fontAlignX+1)/2);
if (gfx.data.fontAlignY<2) { // 0=center, 1=bottom, 2=undefined, 3=top
int stringWidth=0, stringHeight=0; // width/height of entire string
_jswrap_graphics_stringMetrics(&gfx, str, -1, &stringWidth, &stringHeight);
y -= stringHeight * (gfx.data.fontAlignY+1)/2;
}
// figure out clip rectangles
int minX = gfx.data.clipRect.x1, minY = gfx.data.clipRect.y1;
int maxX = gfx.data.clipRect.x2, maxY = gfx.data.clipRect.y2;
deviceToGraphicsCoordinates(&gfx, &minX, &minY);
deviceToGraphicsCoordinates(&gfx, &maxX, &maxY);
if (maxX < minX) {
int t = minX; minX = maxX; maxX = t;
}
if (maxY < minY) {
int t = minY; minY = maxY; maxY = t;
}
#else
int minX = 0;
int minY = 0;
int maxX = graphicsGetWidth(&gfx) - 1;
int maxY = graphicsGetHeight(&gfx) - 1;
int startx = x;
JsVar *str = jsvAsString(var);
#endif
JsvStringIterator it;
jsvStringIteratorNewUTF8(&it, str, 0);
while (jsvStringIteratorHasChar(&it)) {
int ch = jsvStringIteratorGetUTF8CharAndNext(&it);
if (ch=='\n') {
x = startx;
#ifndef SAVE_ON_FLASH
// alignment for non-left aligned multi-line strings
if (gfx.data.fontAlignX<2) // 0=center, 1=right, 2=undefined, 3=left
x = startx - (_jswrap_graphics_stringWidth(&gfx, str, jsvStringIteratorGetIndex(&it)) * (gfx.data.fontAlignX+1)/2);
#endif
y += fontHeight;
continue;
}
#ifndef SAVE_ON_FLASH
if (ch==0) { // If images are described in-line in the string, render them
GfxDrawImageInfo img;
size_t idx = jsvConvertToUTF8Index(str, jsvStringIteratorGetIndex(&it));
if (_jswrap_graphics_parseImage(&gfx, str, idx, &img)) {
jsvStringIteratorGotoUTF8(&it, str, idx+img.headerLength);
_jswrap_drawImageSimple(&gfx, x, y+(fontHeight-img.height)/2, &img, &it, true/*string iterator now points to the next char after image*/);
_jswrap_graphics_freeImageInfo(&img);
x += img.width;
}
continue;
}
#endif
if ((info.font == JSGRAPHICS_FONTSIZE_VECTOR) && (ch<256)) {
#ifndef NO_VECTOR_FONT
int w = (int)graphicsVectorCharWidth(info.scalex, (char)ch);
// TODO: potentially we could do this in x16 accuracy so vector chars rendered together better
if (x>minX-w && x<maxX && y>minY-fontHeight && y<=maxY) {
if (solidBackground)
graphicsFillRect(&gfx,x,y,x+w-1,y+fontHeight-1, gfx.data.bgColor);
graphicsGetVectorChar((graphicsPolyCallback)graphicsFillPoly, &gfx, x, y, info.scalex, info.scaley, (char)ch);
}
x+=w;
#endif
} else if ((info.font == JSGRAPHICS_FONTSIZE_4X6) && (ch<256)) {
if (x>minX-4*info.scalex && x<maxX && y>minY-fontHeight && y<=maxY)
graphicsDrawChar4x6(&gfx, x, y, (char)ch, info.scalex, info.scaley, solidBackground);
x+=4*info.scalex;
#ifdef USE_FONT_6X8
} else if (info.font == JSGRAPHICS_FONTSIZE_6X8) {
if (x>minX-6*info.scalex && x<maxX && y>minY-fontHeight && y<=maxY)
graphicsDrawChar6x8(&gfx, x, y, (char)ch, info.scalex, info.scaley, solidBackground);
x+=6*info.scalex;
#endif
#ifndef SAVE_ON_FLASH
#ifdef ESPR_PBF_FONTS
} else if ((info.font & JSGRAPHICS_FONTSIZE_FONT_MASK)==JSGRAPHICS_FONTSIZE_CUSTOM_PBF) {
PbfFontLoaderGlyph glyph;
if (jspbfFontFindGlyph(&info.pbfInfo, ch, &glyph)) {
jspbfFontRenderGlyph(&info.pbfInfo, &glyph, &gfx,
x+glyph.x*info.scalex, y+glyph.y*info.scaley,
solidBackground, info.scalex, info.scaley);
x+=glyph.advance*info.scalex;
}
#endif
} else if ((info.font & JSGRAPHICS_FONTSIZE_CUSTOM_BIT) && (ch<256)) {
int customBPPRange = (1<<customBPP)-1;
// get char width and offset in string
int width = 0, bmpOffset = 0;
if (jsvIsString(info.widths)) {
if (ch>=info.customFirstChar) {
JsvStringIterator wit;
jsvStringIteratorNew(&wit, info.widths, 0);
while (jsvStringIteratorHasChar(&wit) && (int)jsvStringIteratorGetIndex(&wit)<(ch-info.customFirstChar)) {
bmpOffset += (unsigned char)jsvStringIteratorGetCharAndNext(&wit);
}
width = (unsigned char)jsvStringIteratorGetChar(&wit);
jsvStringIteratorFree(&wit);
}
} else {
width = (int)jsvGetInteger(info.widths);
bmpOffset = width*(ch-info.customFirstChar);
}
if (ch>=info.customFirstChar && (x>minX-width*info.scalex) && (x<maxX) && (y>minY-fontHeight) && y<=maxY) {
int ch = fontHeight/info.scaley;
bmpOffset *= ch * customBPP;
// now render character
JsvStringIterator cit;
jsvStringIteratorNew(&cit, info.bitmap, (size_t)(bmpOffset>>3));
bmpOffset &= 7;
int cx,cy;
int citdata = jsvStringIteratorGetChar(&cit);
citdata <<= customBPP*bmpOffset;
for (cx=0;cx<width;cx++) {
for (cy=0;cy<ch;cy++) {
int col = ((citdata&255)>>(8-customBPP));
if (solidBackground || col)
graphicsFillRect(&gfx,
(x + cx*info.scalex),
(y + cy*info.scaley),
(x + cx*info.scalex + info.scalex-1),
(y + cy*info.scaley + info.scaley-1),
graphicsBlendGfxColor(&gfx, (256*col)/customBPPRange));
bmpOffset += customBPP;
citdata <<= customBPP;
if (bmpOffset>=8) {
bmpOffset=0;
jsvStringIteratorNext(&cit);
citdata = jsvStringIteratorGetChar(&cit);
}
}
}
jsvStringIteratorFree(&cit);
}
x += width*info.scalex;
#endif
}
if (jspIsInterrupted()) break;
}
jsvStringIteratorFree(&it);
jsvUnLock(str);
#ifndef SAVE_ON_FLASH
gfx.data.flags = oldFlags; // restore flags because of text rotation
graphicsSetVar(&gfx); // gfx data changed because modified area
#endif
_jswrap_graphics_freeFontInfo(&info);
return jsvLockAgain(parent);
}
/// Convenience function for using drawString from C code
void jswrap_graphics_drawCString(JsGraphics *gfx, int x, int y, char *str) {
JsVar *s = jsvNewFromString(str);
jsvUnLock2(jswrap_graphics_drawString(gfx->graphicsVar, s, x, y, false),s);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "getVectorFontPolys",
"#if" : "!defined(SAVE_ON_FLASH) || !defined(NO_VECTOR_FONT)",
"generate" : "jswrap_graphics_getVectorFontPolys",
"params" : [
["str","JsVar","The string"],
["options","JsVar","[optional] `{x,y,w,h}` (see below)"]
],
"return" : ["JsVar","An array of Uint8Arrays for vector font polygons"],
"return_object" : "Array"
}
Return the current string as a series of polygons (using the current vector font). `options` is as follows:
* `x` - X offset of font (default 0)
* `y` - Y offset of font (default 0)
* `w` - Width of font (default 256) - the actual width will likely be less than this as most characters are non-square
* `h` - Height of font (default 256) - the actual height will likely be less than this as most characters don't fully fill the font box
```
g.getVectorFontPolys("Hi", {x:-80,y:-128});
```
*/
void _jswrap_graphics_getVectorFontPolys_cb(void *data, int points, short *vertices) {
JsVar *arr = (JsVar *)data;
if (!arr) return;
// scale back down
for (int i=0;i<points*2;i+=2) {
vertices[i ] = (short)(vertices[i ]+8)>>4;
vertices[i+1] = (short)(vertices[i+1]+8)>>4;
}
// create 16 bit array with the data
JsVar *v = jsvNewTypedArray(ARRAYBUFFERVIEW_INT16, points*2);
if (v) {
JsVar *d = jsvGetArrayBufferBackingString(v, NULL);
if (d) {
jsvSetString(d, (char*)vertices, points*4);
jsvUnLock(d);
}
jsvArrayPushAndUnLock(arr, v);
}
}
JsVar *jswrap_graphics_getVectorFontPolys(JsGraphics *gfx, JsVar *str, JsVar *options) {
#ifndef NO_VECTOR_FONT
int x = 0, y = 0, scalex = 256, scaley = 256;
jsvConfigObject configs[] = {
{"x", JSV_INTEGER, &x},
{"y", JSV_INTEGER, &y},
{"w", JSV_INTEGER, &scalex},
{"h", JSV_INTEGER, &scaley},
};
jsvReadConfigObject(options, configs, sizeof(configs) / sizeof(jsvConfigObject));
str = jsvAsString(str);
JsVar *arr = jsvNewEmptyArray();
JsvStringIterator it;
jsvStringIteratorNew(&it, str, 0);
while (jsvStringIteratorHasChar(&it)) {
char ch = jsvStringIteratorGetCharAndNext(&it);
x += graphicsGetVectorChar(_jswrap_graphics_getVectorFontPolys_cb, arr, x, y, scalex, scaley, ch);
}
jsvStringIteratorFree(&it);
jsvUnLock(str);
return arr;
#else
return 0;
#endif
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "drawLineString",
"#if" : "defined(DICKENS)",
"generate" : "jswrap_graphics_drawLineString",
"params" : [
["str","JsVar","The string"],
["x","int32","The X position of the start of the text string"],
["y","int32","The Y position of the middle of the text string"],
["options","JsVar","Options for drawing this font (see below)"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Draw a string of text as a fixed-width line font
`options` contains:
* `size`: font size in pixels (char width is half font size) - default 16
* `rotate`: Initial rotation in radians - default 0
* `twist`: Subsequent rotation per character in radians - default 0
*/
#ifdef ESPR_LINE_FONTS
JsVar *jswrap_graphics_drawLineString(JsVar *parent, JsVar *var, int x, int y, JsVar *options) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
int fontSize = 16;
double rotate = 0, twist = 0;
jsvConfigObject configs[] = {
{"size", JSV_INTEGER, &fontSize},
{"rotate", JSV_FLOAT, &rotate},
{"twist", JSV_FLOAT, &twist}
};
if (!jsvReadConfigObject(options, configs, sizeof(configs) / sizeof(jsvConfigObject))) {
jsExceptionHere(JSET_ERROR, "Invalid options");
return 0;
}
fontSize *= 16;
x = x*16 - 8;
y = y*16 - 8;
int startx = x;
JsVar *str = jsvAsString(var);
JsvStringIterator it;
jsvStringIteratorNew(&it, str, 0);
while (jsvStringIteratorHasChar(&it)) {
char ch = jsvStringIteratorGetCharAndNext(&it);
if (ch=='\n') {
x = startx;
y += fontSize;
continue;
}
int xdx = (int)(0.5 + fontSize*cos(rotate));
int xdy = (int)(0.5 + fontSize*sin(rotate));
graphicsDrawLineChar(&gfx, x, y, xdx, xdy, ch);
x += xdx * 1 / 2;
y += xdy * 1 / 2;
rotate += twist;
if (jspIsInterrupted()) break;
}
jsvStringIteratorFree(&it);
jsvUnLock(str);
graphicsSetVar(&gfx); // gfx data changed because modified area
return jsvLockAgain(parent);
}
#endif
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "drawLine",
"generate" : "jswrap_graphics_drawLine",
"params" : [
["x1","int32","The left"],
["y1","int32","The top"],
["x2","int32","The right"],
["y2","int32","The bottom"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Draw a line between x1,y1 and x2,y2 in the current foreground color
*/
JsVar *jswrap_graphics_drawLine(JsVar *parent, int x1, int y1, int x2, int y2) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
graphicsDrawLine(&gfx, x1,y1,x2,y2);
graphicsSetVar(&gfx); // gfx data changed because modified area
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "drawLineAA",
"ifdef" : "GRAPHICS_ANTIALIAS",
"generate" : "jswrap_graphics_drawLineAA",
"params" : [
["x1","float","The left"],
["y1","float","The top"],
["x2","float","The right"],
["y2","float","The bottom"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Draw a line between x1,y1 and x2,y2 in the current foreground color
*/
#ifdef GRAPHICS_ANTIALIAS
JsVar *jswrap_graphics_drawLineAA(JsVar *parent, double x1, double y1, double x2, double y2) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
graphicsDrawLineAA(&gfx,
(int)(x1*16+0.5),
(int)(y1*16+0.5),
(int)(x2*16+0.5),
(int)(y2*16+0.5));
graphicsSetVar(&gfx); // gfx data changed because modified area
return jsvLockAgain(parent);
}
#endif
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "lineTo",
"generate" : "jswrap_graphics_lineTo",
"params" : [
["x","int32","X value"],
["y","int32","Y value"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Draw a line from the last position of `lineTo` or `moveTo` to this position
*/
JsVar *jswrap_graphics_lineTo(JsVar *parent, int x, int y) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
graphicsDrawLine(&gfx, gfx.data.cursorX, gfx.data.cursorY, x, y);
gfx.data.cursorX = (short)x;
gfx.data.cursorY = (short)y;
graphicsSetVar(&gfx);
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "moveTo",
"generate" : "jswrap_graphics_moveTo",
"params" : [
["x","int32","X value"],
["y","int32","Y value"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Move the cursor to a position - see lineTo
*/
JsVar *jswrap_graphics_moveTo(JsVar *parent, int x, int y) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
gfx.data.cursorX = (short)x;
gfx.data.cursorY = (short)y;
graphicsSetVar(&gfx);
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "drawPoly",
"ifndef" : "SAVE_ON_FLASH",
"generate_full" : "jswrap_graphics_drawPoly_X(parent, poly, closed, false)",
"params" : [
["poly","JsVar","An array of vertices, of the form ```[x1,y1,x2,y2,x3,y3,etc]```"],
["closed","bool","Draw another line between the last element of the array and the first"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : "drawPoly(poly: number[], closed?: boolean): Graphics;"
}
Draw a polyline (lines between each of the points in `poly`) in the current
foreground color
**Note:** there is a limit of 64 points (128 XY elements) for polygons
*/
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "drawPolyAA",
"ifdef" : "GRAPHICS_ANTIALIAS",
"generate_full" : "jswrap_graphics_drawPoly_X(parent, poly, closed, true)",
"params" : [
["poly","JsVar","An array of vertices, of the form ```[x1,y1,x2,y2,x3,y3,etc]```"],
["closed","bool","Draw another line between the last element of the array and the first"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : "drawPolyAA(poly: number[], closed?: boolean): Graphics;"
}
Draw an **antialiased** polyline (lines between each of the points in `poly`) in
the current foreground color
**Note:** there is a limit of 64 points (128 XY elements) for polygons
*/
JsVar *jswrap_graphics_drawPoly_X(JsVar *parent, JsVar *poly, bool closed, bool antiAlias) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
if (!jsvIsIterable(poly)) return 0;
int scale;
void (*drawFn)(JsGraphics *gfx, int x1, int y1, int x2, int y2);
#ifdef GRAPHICS_ANTIALIAS
if (antiAlias) {
scale = 16;
drawFn = graphicsDrawLineAA;
} else
#endif
{
scale = 1;
drawFn = graphicsDrawLine;
}
int lx,ly;
int startx, starty;
int idx = 0;
JsvIterator it;
jsvIteratorNew(&it, poly, JSIF_EVERY_ARRAY_ELEMENT);
while (jsvIteratorHasElement(&it)) {
int x,y;
x = (int)((jsvIteratorGetFloatValue(&it)*scale)+0.5);
jsvIteratorNext(&it);
if (!jsvIteratorHasElement(&it)) break;
y = (int)((jsvIteratorGetFloatValue(&it)*scale)+0.5);
jsvIteratorNext(&it);
if (idx==0) { // save xy positions of first point
startx = x;
starty = y;
} else {
// only start drawing between the first 2 points
drawFn(&gfx, lx, ly, x, y);
}
lx = x;
ly = y;
idx++;
}
jsvIteratorFree(&it);
gfx.data.cursorX = (short)(lx/scale);
gfx.data.cursorY = (short)(ly/scale);
// if closed, draw between first and last points
if (closed)
drawFn(&gfx, lx, ly, startx, starty);
graphicsSetVar(&gfx); // gfx data changed because modified area
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "fillPoly",
"ifndef" : "SAVE_ON_FLASH",
"generate_full" : "jswrap_graphics_fillPoly_X(parent, poly, false);",
"params" : [
["poly","JsVar","An array of vertices, of the form ```[x1,y1,x2,y2,x3,y3,etc]```"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : "fillPoly(poly: number[]): Graphics;"
}
Draw a filled polygon in the current foreground color.
```
g.fillPoly([
16, 0,
31, 31,
26, 31,
16, 12,
6, 28,
0, 27 ]);
```
This fills from the top left hand side of the polygon (low X, low Y) *down to
but not including* the bottom right. When placed together polygons will align
perfectly without overdraw - but this will not fill the same pixels as
`drawPoly` (drawing a line around the edge of the polygon).
**Note:** there is a limit of 64 points (128 XY elements) for polygons
*/
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "fillPolyAA",
"ifdef" : "GRAPHICS_ANTIALIAS",
"generate_full" : "jswrap_graphics_fillPoly_X(parent, poly, true);",
"params" : [
["poly","JsVar","An array of vertices, of the form ```[x1,y1,x2,y2,x3,y3,etc]```"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : "fillPolyAA(poly: number[]): Graphics;"
}
Draw a filled polygon in the current foreground color.
```
g.fillPolyAA([
16, 0,
31, 31,
26, 31,
16, 12,
6, 28,
0, 27 ]);
```
This fills from the top left hand side of the polygon (low X, low Y) *down to
but not including* the bottom right. When placed together polygons will align
perfectly without overdraw - but this will not fill the same pixels as
`drawPoly` (drawing a line around the edge of the polygon).
**Note:** there is a limit of 64 points (128 XY elements) for polygons
*/
JsVar *jswrap_graphics_fillPoly_X(JsVar *parent, JsVar *poly, bool antiAlias) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
if (!jsvIsIterable(poly)) return 0;
const int maxVerts = 128;
short verts[maxVerts];
int idx = 0;
JsvIterator it;
jsvIteratorNew(&it, poly, JSIF_EVERY_ARRAY_ELEMENT);
while (jsvIteratorHasElement(&it) && idx<maxVerts) {
int v = (int)(0.5 + jsvIteratorGetFloatValue(&it)*16);
if (v<-32768) v=-32768;
if (v>32767) v=32767;
verts[idx++] = (short)v;
jsvIteratorNext(&it);
}
if (jsvIteratorHasElement(&it))
jsExceptionHere(JSET_ERROR, "Maximum number of points (%d) exceeded for fillPoly", maxVerts/2);
jsvIteratorFree(&it);
#ifdef GRAPHICS_ANTIALIAS
// For antialiased fillPoly the easiest solution is just to draw AA lines
// around the edge first, then fill solidly
if (antiAlias) {
int lx = verts[idx-2];
int ly = verts[idx-1];
for (int i=0;i<idx;i+=2) {
int vx = verts[i];
int vy = verts[i+1];
graphicsDrawLineAA(&gfx, vx,vy, lx,ly);
lx = vx;
ly = vy;
}
}
#endif
graphicsFillPoly(&gfx, idx/2, verts);
graphicsSetVar(&gfx); // gfx data changed because modified area
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "setRotation",
"generate" : "jswrap_graphics_setRotation",
"params" : [
["rotation","int32","The clockwise rotation. 0 for no rotation, 1 for 90 degrees, 2 for 180, 3 for 270"],
["reflect","bool","Whether to reflect the image"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : "setRotation(rotation: 0 | 1 | 2 | 3, reflect?: boolean): Graphics;"
}
Set the current rotation of the graphics device.
*/
JsVar *jswrap_graphics_setRotation(JsVar *parent, int rotation, bool reflect) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
// clear flags
gfx.data.flags &= (JsGraphicsFlags)~(JSGRAPHICSFLAGS_SWAP_XY | JSGRAPHICSFLAGS_INVERT_X | JSGRAPHICSFLAGS_INVERT_Y);
// set flags
switch (rotation) {
case 0:
break;
case 1:
gfx.data.flags |= JSGRAPHICSFLAGS_SWAP_XY | JSGRAPHICSFLAGS_INVERT_X;
break;
case 2:
gfx.data.flags |= JSGRAPHICSFLAGS_INVERT_X | JSGRAPHICSFLAGS_INVERT_Y;
break;
case 3:
gfx.data.flags |= JSGRAPHICSFLAGS_SWAP_XY | JSGRAPHICSFLAGS_INVERT_Y;
break;
}
if (reflect) {
if (gfx.data.flags & JSGRAPHICSFLAGS_SWAP_XY)
gfx.data.flags ^= JSGRAPHICSFLAGS_INVERT_Y;
else
gfx.data.flags ^= JSGRAPHICSFLAGS_INVERT_X;
}
graphicsSetVar(&gfx);
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "imageMetrics",
"generate" : "jswrap_graphics_imageMetrics",
"params" : [
["str","JsVar","The string"]
],
"return" : ["JsVar","An object containing `{width,height,bpp,transparent}` for the image"],
"typescript" : "imageMetrics(img: Image): { width: number, height: number, bpp: number, transparent: number, frames?: ArrayBuffer[] } | undefined;"
}
Return the width and height in pixels of an image (either Graphics, Image
Object, Image String or ArrayBuffer). Returns `undefined` if image couldn't be
decoded.
`frames` is also included is the image contains more information than you'd
expect for a single bitmap. In this case the bitmap might be an animation with
multiple frames
*/
JsVar *jswrap_graphics_imageMetrics(JsVar *parent, JsVar *var) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
GfxDrawImageInfo img;
if (!_jswrap_graphics_parseImage(&gfx, var, 0, &img))
return 0;
int bufferLen = jsvGetLength(img.buffer) - img.bitmapOffset;
_jswrap_graphics_freeImageInfo(&img);
JsVar *o = jsvNewObject();
if (o) {
jsvObjectSetChildAndUnLock(o, "width", jsvNewFromInteger(img.width));
jsvObjectSetChildAndUnLock(o, "height", jsvNewFromInteger(img.height));
jsvObjectSetChildAndUnLock(o, "bpp", jsvNewFromInteger(img.bpp));
jsvObjectSetChildAndUnLock(o, "transparent", jsvNewFromBool(img.isTransparent));
int frames = bufferLen / img.bitmapLength;
if (frames>1) jsvObjectSetChildAndUnLock(o, "frames", jsvNewFromInteger(frames));
}
return o;
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "drawImage",
"generate" : "jswrap_graphics_drawImage",
"params" : [
["image","JsVar","An image to draw, either a String or an Object (see below)"],
["x","int32","The X offset to draw the image"],
["y","int32","The Y offset to draw the image"],
["options","JsVar","options for scaling,rotation,etc (see below)"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : "drawImage(image: Image, x: number, y: number, options?: { rotate?: number, scale?: number, frame?: number }): Graphics;"
}
Image can be:
* An object with the following fields `{ width : int, height : int, bpp :
optional int, buffer : ArrayBuffer/String, transparent: optional int,
palette : optional Uint16Array(2/4/16) }`. bpp = bits per pixel (default is
1), transparent (if defined) is the colour that will be treated as
transparent, and palette is a color palette that each pixel will be looked up
in first
* A String where the the first few bytes are:
`width,height,bpp,[transparent,]image_bytes...`. If a transparent colour is
specified the top bit of `bpp` should be set.
* An ArrayBuffer Graphics object (if `bpp<8`, `msb:true` must be set) - this is
disabled on devices without much flash memory available. If a Graphics object
is supplied, it can also contain transparent/palette fields as if it were
an image.
Draw an image at the specified position.
* If the image is 1 bit, the graphics foreground/background colours will be
used.
* If `img.palette` is a Uint16Array or 2/4/16 elements, color data will be
looked from the supplied palette
* On Bangle.js, 2 bit images blend from background(0) to foreground(1) colours
* On Bangle.js, 4 bit images use the Apple Mac 16 color palette
* On Bangle.js, 8 bit images use the Web Safe 216 color palette
* Otherwise color data will be copied as-is. Bitmaps are rendered MSB-first
If `options` is supplied, `drawImage` will allow images to be rendered at any
scale or angle. If `options.rotate` is set it will center images at `x,y`.
`options` must be an object of the form:
```
{
rotate : float, // the amount to rotate the image in radians (default 0)
scale : float, // the amount to scale the image up (default 1)
frame : int // if specified and the image has frames of data
// after the initial frame, draw one of those frames from the image
filter : bool // (2v19+) when set, if scale<0.75 perform 2x2 supersampling to smoothly downscale the image
}
```
For example:
```
// In the top left of the screen
g.drawImage(img,0,0);
// In the top left of the screen, twice as big
g.drawImage(img,0,0,{scale:2});
// In the center of the screen, twice as big, 45 degrees
g.drawImage(img, g.getWidth()/2, g.getHeight()/2,
{scale:2, rotate:Math.PI/4});
```
*/
JsVar *jswrap_graphics_drawImage(JsVar *parent, JsVar *image, int xPos, int yPos, JsVar *options) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
GfxDrawImageInfo img;
if (!_jswrap_graphics_parseImage(&gfx, image, 0, &img))
return 0;
double scale = 1, rotate = 0;
bool centerImage = false;
#ifndef SAVE_ON_FLASH
bool filter = false;
#endif
if (jsvIsObject(options)) {
// support for multi-frame rendering
int frame = jsvGetIntegerAndUnLock(jsvObjectGetChildIfExists(options,"frame"));
if (frame>0)
img.bitmapOffset += img.bitmapLength * frame;
// rotate, scale
scale = jsvGetFloatAndUnLock(jsvObjectGetChildIfExists(options,"scale"));
if (!isfinite(scale) || scale<=0) scale=1;
rotate = jsvGetFloatAndUnLock(jsvObjectGetChildIfExists(options,"rotate"));
centerImage = isfinite(rotate);
if (!centerImage) rotate = 0;
#ifndef SAVE_ON_FLASH
filter = jsvGetBoolAndUnLock(jsvObjectGetChildIfExists(options,"filter"));
#endif
}
int x=0, y=0;
unsigned int colData = 0;
JsvStringIterator it;
jsvStringIteratorNew(&it, img.buffer, (size_t)img.bitmapOffset);
#ifdef USE_LCD_ST7789_8BIT // can we blit directly to the display?
bool isST7789 =
gfx.data.type==JSGRAPHICSTYPE_ST7789_8BIT && // it's the display
(gfx.data.flags & JSGRAPHICSFLAGS_MAPPEDXY)==0 && // no messing with coordinates
gfx.data.bpp==16 && // normal BPP
(img.bpp==8 || img.bpp==1) && // image bpp is handled by fast path
!img.isTransparent; // not transparent
#endif
if (scale==1 && rotate==0 && !centerImage) {
// Standard 1:1 blitting
#ifdef USE_LCD_ST7789_8BIT // can we blit directly to the display?
if (isST7789 &&
xPos>=gfx.data.clipRect.x1 && yPos>=gfx.data.clipRect.y1 && // check it's all on-screen
(xPos+img.width)<=gfx.data.clipRect.x2+1 && (yPos+img.height)<=gfx.data.clipRect.y2+1) {
if (img.bpp==1) lcdST7789_blit1Bit(xPos, yPos, img.width, img.height, 1, &it, img.palettePtr);
else if (img.bpp==8) lcdST7789_blit8Bit(xPos, yPos, img.width, img.height, 1, &it, img.palettePtr);
} else {
#else
{
#endif
_jswrap_drawImageSimple(&gfx, xPos, yPos, &img, &it, false/*don't care about string iterator now*/);
}
} else {
#ifndef GRAPHICS_DRAWIMAGE_ROTATED
jsExceptionHere(JSET_ERROR,"Image scale/rotate not implemented on this device");
#else
// fancy rotation/scaling
#ifdef GRAPHICS_FAST_PATHS
bool fastPath =
(!centerImage) && // not rotating
(scale-floor(scale))==0 && // integer scale
(gfx.data.flags & JSGRAPHICSFLAGS_MAPPEDXY)==0; // no messing with coordinates
if (fastPath) { // fast path for non-rotated, integer scale
int s = (int)scale;
// Scaled blitting
/* Output as scanlines rather than small fillrects so
* that on direct-coupled displays we can optimise away
* coordinate setting
*/
#ifdef USE_LCD_ST7789_8BIT // can we blit directly to the display?
if (isST7789 &&
s>=1 &&
xPos>=gfx.data.clipRect.x1 && yPos>=gfx.data.clipRect.y1 && // check it's all on-screen
(xPos+img.width*s)<=gfx.data.clipRect.x2+1 && (yPos+img.height*s)<=gfx.data.clipRect.y2+1) {
if (img.bpp==1) lcdST7789_blit1Bit(xPos, yPos, img.width, img.height, s, &it, img.palettePtr);
else lcdST7789_blit8Bit(xPos, yPos, img.width, img.height, s, &it, img.palettePtr);
} else
#endif
{
int bits=0;
int yp = yPos;
for (y=0;y<img.height;y++) {
// Store current pos as we need to rewind
size_t lastIt = jsvStringIteratorGetIndex(&it);
int lastBits = bits;
unsigned int lastColData = colData;
// do a new iteration for each line we're scaling
for (int iy=0;iy<s;iy++) {
if (iy) { // rewind for all but the first line of scaling
jsvStringIteratorGoto(&it, img.buffer, lastIt);
bits = lastBits;
colData = lastColData;
}
// iterate over x
int xp = xPos;
for (x=0;x<img.width;x++) {
// Get the data we need...
while (bits < img.bpp) {
colData = (colData<<8) | ((unsigned char)jsvStringIteratorGetCharAndNext(&it));
bits += 8;
}
// extract just the bits we want
unsigned int col = (colData>>(bits-img.bpp))&img.bitMask;
bits -= img.bpp;
// Try and write pixel!
if (img.transparentCol!=col && yp>=gfx.data.clipRect.y1 && yp<=gfx.data.clipRect.y2) {
if (img.palettePtr) col = img.palettePtr[col&img.paletteMask];
for (int ix=0;ix<s;ix++) {
if (xp>=gfx.data.clipRect.x1 && xp<=gfx.data.clipRect.x2)
gfx.setPixel(&gfx, xp, yp, col);
xp++;
}
} else xp += s;
}
yp++;
}
}
// update modified area since we went direct
int x1=xPos, y1=yPos, x2=xPos+s*img.width, y2=yPos+s*img.height;
graphicsSetModifiedAndClip(&gfx,&x1,&y1,&x2,&y2);
}
} else { // handle rotation, and default to center the image
#else
if (true) {
#endif // GRAPHICS_FAST_PATHS
GfxDrawImageLayer l;
l.x1 = xPos;
l.y1 = yPos;
l.img = img;
l.it = it;
l.rotate = rotate;
l.scale = scale;
l.center = centerImage;
l.repeat = false;
_jswrap_drawImageLayerInit(&l);
int x1=l.x1, y1=l.y1, x2=l.x2-1, y2=l.y2-1;
graphicsSetModifiedAndClip(&gfx, &x1, &y1, &x2, &y2);
_jswrap_drawImageLayerSetStart(&l, x1, y1);
JsGraphicsSetPixelFn setPixel = graphicsGetSetPixelFn(&gfx);
#ifndef SAVE_ON_FLASH
if (filter && scale<0.75) { // 2x2 antialiasing
int sx = (int)(l.sx * scale); // use scale rather than 0.5, so if scaling dithered it still works nicely
int sy = (int)(l.sy * scale);
int s2x = l.sx - sx; // sx+s2x = l.sx
int s2y = l.sy - sy;
GfxDrawImageLayer l2;
memcpy(&l2, &l, sizeof(l));
jsvStringIteratorNew(&l2.it, l2.img.buffer, 0);
l2.px += sy;
l2.py += sx;
_jswrap_drawImageLayerNextY(&l2);
// scan across image
for (y = y1; y <= y2; y++) {
_jswrap_drawImageLayerStartX(&l);
_jswrap_drawImageLayerStartX(&l2);
for (x = x1; x <= x2 ; x++) {
uint32_t ca,cb,cc,cd;
bool nonTransparent = true;
nonTransparent &= _jswrap_drawImageLayerGetPixel(&l, &ca);
l.qx += sx;
l.qy -= sy;
nonTransparent &= _jswrap_drawImageLayerGetPixel(&l, &cb);
l.qx += s2x;
l.qy -= s2y;
nonTransparent &= _jswrap_drawImageLayerGetPixel(&l2, &cc);
l2.qx += sx;
l2.qy -= sy;
nonTransparent &= _jswrap_drawImageLayerGetPixel(&l2, &cd);
l2.qx += s2x;
l2.qy -= s2y;
if (true) {
ca = graphicsBlendColor(&gfx, ca, cb, 128);
cc = graphicsBlendColor(&gfx, cc, cd, 128);
colData = graphicsBlendColor(&gfx, ca, cc, 128);
setPixel(&gfx, x, y, colData);
}
}
_jswrap_drawImageLayerNextY(&l);
_jswrap_drawImageLayerNextY(&l2);
}
jsvStringIteratorFree(&l2.it);
} else
#endif
{
// scan across image
for (y = y1; y <= y2; y++) {
_jswrap_drawImageLayerStartX(&l);
for (x = x1; x <= x2 ; x++) {
if (_jswrap_drawImageLayerGetPixel(&l, &colData)) {
setPixel(&gfx, x, y, colData);
}
_jswrap_drawImageLayerNextX(&l);
}
_jswrap_drawImageLayerNextY(&l);
}
}
it = l.it; // make sure it gets freed properly
}
#endif // GRAPHICS_DRAWIMAGE_ROTATED
}
jsvStringIteratorFree(&it);
_jswrap_graphics_freeImageInfo(&img);
graphicsSetVar(&gfx); // gfx data changed because modified area
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "drawImages",
"#if" : "defined(BANGLEJS) || defined(LINUX)",
"generate" : "jswrap_graphics_drawImages",
"params" : [
["layers","JsVar","An array of objects {x,y,image,scale,rotate,center} (up to 3)"],
["options","JsVar","options for rendering - see below"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : "drawImages(layers: { x: number, y: number, image: Image, scale?: number, rotate?: number, center?: boolean, repeat?: boolean, nobounds?: boolean }[], options?: { x: number, y: number, width: number, height: number }): Graphics;"
}
Draws multiple images *at once* - which avoids flicker on unbuffered systems
like Bangle.js. Maximum layer count right now is 4.
```
layers = [ {
{x : int, // x start position
y : int, // y start position
image : string/object,
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
}
]
options = { // the area to render. Defaults to rendering just enough to cover what's requested
x,y,
width,height
}
```
*/
JsVar *jswrap_graphics_drawImages(JsVar *parent, JsVar *layersVar, JsVar *options) {
const int MAXIMAGES = 4;
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
GfxDrawImageLayer layers[MAXIMAGES];
int i,layerCount;
if (!(jsvIsArray(layersVar) && (layerCount=jsvGetArrayLength(layersVar))<=MAXIMAGES)) {
jsExceptionHere(JSET_TYPEERROR,"Expecting array for first argument with <%d entries", MAXIMAGES);
return 0;
}
// default bounds (=nothing)
int x=10000,y=10000;
int width=10000;
int height=10000;
// now run through all layers getting stuff ready and checking
bool ok = true;
for (i=0;i<layerCount;i++) {
JsVar *layer = jsvGetArrayItem(layersVar, i);
if (jsvIsObject(layer)) {
JsVar *image = jsvObjectGetChildIfExists(layer,"image");
if (_jswrap_graphics_parseImage(&gfx, image, 0, &layers[i].img)) {
layers[i].x1 = jsvGetIntegerAndUnLock(jsvObjectGetChildIfExists(layer,"x"));
layers[i].y1 = jsvGetIntegerAndUnLock(jsvObjectGetChildIfExists(layer,"y"));
// rotate, scale
layers[i].scale = jsvGetFloatAndUnLock(jsvObjectGetChildIfExists(layer,"scale"));
if (!isfinite(layers[i].scale) || layers[i].scale<=0)
layers[i].scale=1;
layers[i].rotate = jsvGetFloatAndUnLock(jsvObjectGetChildIfExists(layer,"rotate"));
if (!isfinite(layers[i].rotate)) layers[i].rotate=0;
layers[i].center = jsvGetBoolAndUnLock(jsvObjectGetChildIfExists(layer,"center"));
layers[i].repeat = jsvGetBoolAndUnLock(jsvObjectGetChildIfExists(layer,"repeat"));
_jswrap_drawImageLayerInit(&layers[i]);
// add the calculated bounds to our default bounds
if (!jsvGetBoolAndUnLock(jsvObjectGetChildIfExists(layer,"nobounds"))) {
if (layers[i].x1<x) x=layers[i].x1;
if (layers[i].y1<y) y=layers[i].y1;
if (layers[i].x2>x+width) width=layers[i].x2-x;
if (layers[i].y2>y+height) height=layers[i].y2-y;
}
} else ok = false;
jsvUnLock(image);
} else ok = false;
jsvUnLock(layer);
}
jsvConfigObject configs[] = {
{"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);
JsGraphicsSetPixelFn setPixel = graphicsGetSetPixelFn(&gfx);
// If all good, start rendering!
if (ok) {
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);
}
// scan across image
for (int yi = y; yi <= y2; yi++) {
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
bool solid = false;
unsigned int colData = 0;
for (i=layerCount-1;i>=0;i--) {
if (_jswrap_drawImageLayerGetPixel(&layers[i], &colData)) {
solid = true;
break;
}
}
// 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]);
}
for (i=0;i<layerCount;i++)
jsvStringIteratorFree(&layers[i].it);
}
// tidy up
for (i=0;i<layerCount;i++) {
jsvUnLock(layers[i].img.buffer);
}
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "asImage",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_asImage",
"params" : [
["type","JsVar","The type of image to return. Either `object`/undefined to return an image object, or `string` to return an image string"]
],
"return" : ["JsVar","An Image that can be used with `Graphics.drawImage`"],
"typescript" : [
"asImage(type?: \"object\"): ImageObject;",
"asImage(type: \"string\"): string;"
]
}
Return this Graphics object as an Image that can be used with
`Graphics.drawImage`. Check out [the Graphics reference
page](http://www.espruino.com/Graphics#images-bitmaps) for more information on
images.
Will return undefined if data can't be allocated for the image.
The image data itself will be referenced rather than copied if:
* An image `object` was requested (not `string`)
* The Graphics instance was created with `Graphics.createArrayBuffer`
* Is 8 bpp *OR* the `{msb:true}` option was given
* No other format options (zigzag/etc) were given
Otherwise data will be copied, which takes up more space and may be quite slow.
If the `Graphics` object contains `transparent` or `pelette` fields,
[as you might find in an image](http://www.espruino.com/Graphics#images-bitmaps),
those will be included in the generated image too.
*/
JsVar *jswrap_graphics_asImage(JsVar *parent, JsVar *imgType) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
bool isObject;
if (jsvIsUndefined(imgType) || jsvIsStringEqual(imgType,"object"))
isObject = true;
else if (jsvIsStringEqual(imgType,"string")) {
isObject = false;
} else {
jsExceptionHere(JSET_ERROR, "Unknown image type %j", imgType);
return 0;
}
int w = jswrap_graphics_getWidthOrHeight(parent,false);
int h = jswrap_graphics_getWidthOrHeight(parent,true);
int bpp = gfx.data.bpp;
int colorCount = (1<<bpp);
#ifdef LCD_CONTROLLER_LPM013M126
// memory LCD reports bit depth as 16 so it can do dithering
// but when we get an image we only want it the real bit depth (3!)
if (gfx.data.type==JSGRAPHICSTYPE_MEMLCD) bpp=3;
#endif
int len = (w*h*bpp+7)>>3;
int transparent = -1;
JsVar *v = jsvObjectGetChildIfExists(parent, "transparent");
if (v) transparent = jsvGetIntegerAndUnLock(v);
JsVar *palette = jsvObjectGetChildIfExists(parent, "palette");
if (!(jsvIsArray(palette) || jsvIsArrayBuffer(palette))) {
jsvUnLock(palette);
palette = 0;
}
JsVar *img = 0;
if (isObject) {
img = jsvNewObject();
if (!img) return 0;
jsvObjectSetChildAndUnLock(img,"width",jsvNewFromInteger(w));
jsvObjectSetChildAndUnLock(img,"height",jsvNewFromInteger(h));
if (bpp!=1) jsvObjectSetChildAndUnLock(img,"bpp",jsvNewFromInteger(bpp));
/* IF we have an arraybuffer of the right form then
we can return the original buffer directly */
if (gfx.data.type == JSGRAPHICSTYPE_ARRAYBUFFER &&
(bpp==8 || // 8 bit data is fine
((gfx.data.flags & JSGRAPHICSFLAGS_ARRAYBUFFER_MSB) && // must be MSB first
!(gfx.data.flags & JSGRAPHICSFLAGS_NONLINEAR)))) { // must be in-order
jsvObjectSetChildAndUnLock(img,"buffer",jsvObjectGetChildIfExists(gfx.graphicsVar, "buffer"));
return img;
}
if (transparent>=0)
jsvObjectSetChildAndUnLock(img,"transparent",jsvNewFromInteger(transparent));
if (palette) jsvObjectSetChild(img,"palette",palette);
} else {
len += 3 + ((transparent>=0)?1:0) + (palette?(colorCount*2):0); // for the header!
}
JsVar *buffer = jsvNewStringOfLength((unsigned)len, NULL);
if (!buffer) { // not enough memory
jsvUnLock(img);
return 0;
}
int x=0, y=0;
unsigned int pixelBits = 0;
unsigned int pixelBitCnt = 0;
JsvStringIterator it;
jsvStringIteratorNew(&it, buffer, 0);
if (!isObject) { // if not an object, add the header
// info on image format at http://www.espruino.com/Graphics#images-bitmaps
jsvStringIteratorSetCharAndNext(&it, (char)(uint8_t)w);
jsvStringIteratorSetCharAndNext(&it, (char)(uint8_t)h);
jsvStringIteratorSetCharAndNext(&it, (char)(uint8_t)(bpp | ((transparent>=0)?128:0)) | (palette?64:0));
if (transparent>=0)
jsvStringIteratorSetCharAndNext(&it, (char)(uint8_t)transparent);
if (palette) {
JsvIterator pit;
int colorIdx = 0;
jsvIteratorNew(&pit, palette, JSIF_EVERY_ARRAY_ELEMENT);
while (jsvIteratorHasElement(&pit)) {
int colorValue = jsvIteratorGetIntegerValue(&pit);
if (colorIdx<colorCount) {
jsvStringIteratorSetCharAndNext(&it, (char)(uint8_t)(colorValue>>8));
jsvStringIteratorSetCharAndNext(&it, (char)(uint8_t)colorValue);
}
jsvIteratorNext(&pit);
colorIdx++;
}
jsvIteratorFree(&pit);
while (colorIdx<colorCount) {
jsvStringIteratorSetCharAndNext(&it, 0);
jsvStringIteratorSetCharAndNext(&it, 0);
colorIdx++;
}
}
}
while (jsvStringIteratorHasChar(&it)) {
unsigned int pixel = graphicsGetPixel(&gfx, x, y);
#ifdef LCD_CONTROLLER_LPM013M126
if (gfx.data.type==JSGRAPHICSTYPE_MEMLCD)
pixel = GRAPHICS_COL_16_TO_3(pixel);
#endif
pixelBits = (pixelBits<<bpp) | pixel;
pixelBitCnt += (unsigned)bpp;
x++;
if (x>=w) {
x=0;
y++;
}
while (pixelBitCnt>=8) {
jsvStringIteratorSetCharAndNext(&it, (char)(pixelBits>>(pixelBitCnt-8)));
pixelBitCnt -= 8;
}
}
jsvStringIteratorFree(&it);
jsvUnLock(palette);
if (isObject) {
jsvObjectSetChildAndUnLock(img,"buffer",buffer);
return img;
} else
return buffer;
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "getModified",
"ifndef" : "SAVE_ON_FLASH",
"generate" : "jswrap_graphics_getModified",
"params" : [
["reset","bool","Whether to reset the modified area or not"]
],
"return" : ["JsVar","An object {x1,y1,x2,y2} containing the modified area, or undefined if not modified"],
"typescript" : "getModified(reset?: boolean): { x1: number, y1: number, x2: number, y2: number };"
}
Return the area of the Graphics canvas that has been modified, and optionally
clear the modified area to 0.
For instance if `g.setPixel(10,20)` was called, this would return `{x1:10,
y1:20, x2:10, y2:20}`
*/
JsVar *jswrap_graphics_getModified(JsVar *parent, bool reset) {
#ifndef NO_MODIFIED_AREA
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
JsVar *obj = 0;
if (gfx.data.modMinX <= gfx.data.modMaxX) { // do we have a rect?
obj = jsvNewObject();
if (obj) {
jsvObjectSetChildAndUnLock(obj, "x1", jsvNewFromInteger(gfx.data.modMinX));
jsvObjectSetChildAndUnLock(obj, "y1", jsvNewFromInteger(gfx.data.modMinY));
jsvObjectSetChildAndUnLock(obj, "x2", jsvNewFromInteger(gfx.data.modMaxX));
jsvObjectSetChildAndUnLock(obj, "y2", jsvNewFromInteger(gfx.data.modMaxY));
}
}
if (reset) {
gfx.data.modMaxX = -32768;
gfx.data.modMaxY = -32768;
gfx.data.modMinX = 32767;
gfx.data.modMinY = 32767;
graphicsSetVar(&gfx);
}
return obj;
#else
return 0;
#endif
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "scroll",
"#if" : "!defined(SAVE_ON_FLASH) && !defined(ESPRUINOBOARD)",
"generate" : "jswrap_graphics_scroll",
"params" : [
["x","int32","X direction. >0 = to right"],
["y","int32","Y direction. >0 = down"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Scroll the contents of this graphics in a certain direction. The remaining area
is filled with the background color.
Note: This uses repeated pixel reads and writes, so will not work on platforms
that don't support pixel reads.
*/
JsVar *jswrap_graphics_scroll(JsVar *parent, int xdir, int ydir) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
graphicsScroll(&gfx, xdir, ydir);
// update modified area
graphicsSetVar(&gfx);
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "blit",
"#if" : "!defined(SAVE_ON_FLASH) && !defined(ESPRUINOBOARD)",
"generate" : "jswrap_graphics_blit",
"params" : [
["options","JsVar","options - see below"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : "blit(options: { x1: number, y1: number, x2: number, y2: number, w: number, h: number, setModified?: boolean }): Graphics;"
}
Blit one area of the screen (x1,y1 w,h) to another (x2,y2 w,h)
```
g.blit({
x1:0, y1:0,
w:32, h:32,
x2:100, y2:100,
setModified : true // should we set the modified area?
});
```
Note: This uses repeated pixel reads and writes, so will not work on platforms
that don't support pixel reads.
*/
JsVar *jswrap_graphics_blit(JsVar *parent, JsVar *options) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
int sw = gfx.data.width;
int sh = gfx.data.height;
int x1=0,y1=0,w=0,h=0,x2=0,y2=0;
bool setModified = false;
jsvConfigObject configs[] = {
{"x1", JSV_INTEGER, &x1},
{"y1", JSV_INTEGER, &y1},
{"w", JSV_INTEGER, &w},
{"h", JSV_INTEGER, &h},
{"x2", JSV_INTEGER, &x2},
{"y2", JSV_INTEGER, &y2},
{"setModified", JSV_BOOLEAN, &setModified}};
if (!jsvIsObject(options) ||
!jsvReadConfigObject(options, configs, sizeof(configs) / sizeof(jsvConfigObject))) {
jsExceptionHere(JSET_ERROR, "Invalid options");
return 0;
}
int ex;
// clip source positions
if (x1<0) {
x2 -= x1;
w += x1;
x1 = 0;
}
if (y1<0) {
y2 -= y1;
h += y1;
y1 = 0;
}
ex = (x1+w) - sw;
if (ex > 0) w -= ex;
ex = (y1+h) - sh;
if (ex > 0) h -= ex;
// clip destination positions
if (x2<0) {
x1 -= x2;
w += x2;
x2 = 0;
}
if (y2<0) {
y1 -= y2;
h += y2;
y2 = 0;
}
ex = (x2+w) - sw;
if (ex > 0) w -= ex;
ex = (y2+h) - sh;
if (ex > 0) h -= ex;
if (w>0 || h>0) {
gfx.blit(&gfx, x1,y1,w,h,x2,y2);
if (setModified) {
graphicsSetModified(&gfx, x2,y2,x2+w,y2+h);
graphicsSetVar(&gfx);
}
}
return jsvLockAgain(parent);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "asBMP",
"#if" : "!defined(SAVE_ON_FLASH) && !defined(ESPRUINOBOARD)",
"generate" : "jswrap_graphics_asBMP",
"return" : ["JsVar","A String representing the Graphics as a Windows BMP file (or 'undefined' if not possible)"],
"typescript" : "asBMP(): string;"
}
Create a Windows BMP file from this Graphics instance, and return it as a
String.
*/
JsVar *jswrap_graphics_asBMP_X(JsVar *parent, bool printBase64) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
int width = graphicsGetWidth(&gfx);
int height = graphicsGetHeight(&gfx);
int realBPP = gfx.data.bpp;
#ifdef LCD_CONTROLLER_LPM013M126
// memory LCD reports bit depth as 16 so it can do dithering
// but when we get a bitmap we only want it the real bit depth (3!)
if (gfx.data.type==JSGRAPHICSTYPE_MEMLCD) realBPP=3;
#endif
int bpp = realBPP;
if (bpp>1 && bpp<4) bpp=4;
else if (bpp>4 && bpp<8) bpp=8;
bool hasPalette = bpp<=8;
int rowstride = (((width*bpp)+31) >> 5) << 2; // padded to 32 bits
// palette length (byte size is 3x this)
int paletteEntries = hasPalette?(1<<bpp):0;
int headerLen = 14 + 12 + paletteEntries*3;
int fileSize = headerLen + height*rowstride;
// if printing base64 we only need enough memory for header + one row
int imgDataLen = printBase64 ? (headerLen + rowstride) : fileSize;
JsVar *imgData = jsvNewFlatStringOfLength((unsigned)imgDataLen);
if (!imgData) return 0; // not enough memory
unsigned char *imgPtr = (unsigned char *)jsvGetFlatStringPointer(imgData);
imgPtr[0]=66; //B
imgPtr[1]=77; //M
imgPtr[2]=(unsigned char)fileSize;
imgPtr[3]=(unsigned char)(fileSize>>8); // plus 2 more bytes for size
imgPtr[10]=(unsigned char)headerLen;
// maybe we want the InfoHeader, not BITMAPCOREHEADER (http://www.ece.ualberta.ca/~elliott/ee552/studentAppNotes/2003_w/misc/bmp_file_format/bmp_file_format.htm)
// Chrome doesn't like 16 bit BMPs in this format
// BITMAPCOREHEADER
imgPtr[14]=12; // sizeof(BITMAPCOREHEADER)
imgPtr[18]=(unsigned char)width;
imgPtr[19]=(unsigned char)(width>>8);
imgPtr[20]=(unsigned char)height;
imgPtr[21]=(unsigned char)(height>>8);
imgPtr[22]=1; // color planes, should be 1
imgPtr[24]=(unsigned char)bpp; // bpp
if (hasPalette) {
// palette starts at 26
if (bpp==1) {
// first is white(?)
imgPtr[26]=255;
imgPtr[27]=255;
imgPtr[28]=255;
} else {
if (realBPP==3) {
for (int i=0;i<paletteEntries;i++) {
imgPtr[26 + (i*3)] = (i&1) ? 255 : 0;
imgPtr[27 + (i*3)] = (i&2) ? 255 : 0;
imgPtr[28 + (i*3)] = (i&4) ? 255 : 0;
}
#if defined(GRAPHICS_PALETTED_IMAGES)
} else if (realBPP==4) {
for (int i=0;i<16;i++) {
int p = PALETTE_4BIT[i];
imgPtr[26 + (i*3)] = (unsigned char)((p<<3)&0xF8);
imgPtr[27 + (i*3)] = (unsigned char)((p>>3)&0xFC);
imgPtr[28 + (i*3)] = (unsigned char)((p>>8)&0xF8);
}
} else if (realBPP==8) {
for (int i=0;i<255;i++) {
int p = PALETTE_8BIT[i];
imgPtr[26 + (i*3)] = (unsigned char)((p<<3)&0xF8);
imgPtr[27 + (i*3)] = (unsigned char)((p>>3)&0xFC);
imgPtr[28 + (i*3)] = (unsigned char)((p>>8)&0xF8);
}
#endif
} else { // otherwise default to greyscale
for (int i=0;i<(1<<realBPP);i++) {
unsigned char c = (unsigned char)(255 * i / (1<<realBPP));
imgPtr[26 + (i*3)] = c;
imgPtr[27 + (i*3)] = c;
imgPtr[28 + (i*3)] = c;
}
}
}
}
int pixelMask = (1<<bpp)-1;
int pixelsPerByte = 8 / bpp;
int idx = headerLen; //< index we're writing to our data at
for (int y=height-1;y>=0;y--) {
int yi = height-(y+1);
int bytesWritten = 0;
if (bpp<8) { // >1 pixels per byte
for (int x=0;x<width;) {
unsigned int b = 0;
for (int i=0;i<pixelsPerByte;i++) {
unsigned int pixel = graphicsGetPixel(&gfx, x++, y);
#ifdef LCD_CONTROLLER_LPM013M126
if (gfx.data.type==JSGRAPHICSTYPE_MEMLCD)
pixel = GRAPHICS_COL_16_TO_3(pixel);
#endif
b = (b<<bpp)|(pixel&pixelMask);
}
imgPtr[idx++] = (unsigned char)b;
bytesWritten++;
}
} else { // <= 1 pixel per byte
for (int x=0;x<width;x++) {
unsigned int c = graphicsGetPixel(&gfx, x, y);
if (bpp==16) // 16 bit BMP is RGB555, not RGB565
c = (c&31) | ((c>>1)&~31);
for (int j=0;j<bpp;j+=8) {
imgPtr[idx++] = (unsigned char)(c);
bytesWritten++;
c >>= 8;
}
}
}
// ensure our index matches up
if (bytesWritten<rowstride)
idx += rowstride-bytesWritten;
// if printing to console, we're going to print everything as long as we have a multiple of 3 (or we're at the end)
if (printBase64 && idx>2) {
bool isLastRow = y==0;
int count = isLastRow ? idx : (idx-(idx%3));
JsVar *view = jsvNewArrayBufferFromString(imgData, count); // create an arraybuffer - this means we can pass to btoa with zero allocations
JsVar *b64 = jswrap_btoa(view);
jsvUnLock(view);
if (b64) jsiConsolePrintf("%v", b64);
jsvUnLock(b64);
// shift everything back so we can start again
if (count < idx)
memmove(imgPtr, &imgPtr[count], idx-count);
idx -= count;
}
}
if (printBase64) {
jsiConsolePrintf("\n");
jsvUnLock(imgData);
return 0;
}
return imgData;
}
JsVar *jswrap_graphics_asBMP(JsVar *parent) {
return jswrap_graphics_asBMP_X(parent, false/*printBase64*/);
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "asURL",
"#if" : "!defined(SAVE_ON_FLASH) && !defined(ESPRUINOBOARD)",
"generate" : "jswrap_graphics_asURL",
"return" : ["JsVar","A String representing the Graphics as a URL (or 'undefined' if not possible)"],
"typescript" : "asURL(): string;"
}
Create a URL of the form `data:image/bmp;base64,...` that can be pasted into the
browser.
The Espruino Web IDE can detect this data on the console and render the image
inline automatically.
*/
JsVar *jswrap_graphics_asURL(JsVar *parent) {
JsVar *imgData = jswrap_graphics_asBMP_X(parent, false/*printBase64*/);
if (!imgData) return 0; // not enough memory
JsVar *b64 = jswrap_btoa(imgData);
jsvUnLock(imgData);
if (!b64) return 0; // not enough memory
JsVar *r = jsvVarPrintf("data:image/bmp;base64,%v",b64);
jsvUnLock(b64);
return r;
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "dump",
"#if" : "!defined(SAVE_ON_FLASH) && !defined(ESPRUINOBOARD)",
"generate" : "jswrap_graphics_dump"
}
Output this image as a bitmap URL of the form `data:image/bmp;base64,...`. The
Espruino Web IDE will detect this on the console and will render the image
inline automatically.
This is identical to `console.log(g.asURL())` - it is just a convenient function
for easy debugging and producing screenshots of what is currently in the
Graphics instance.
**Note:** This may not work on some bit depths of Graphics instances. It will
also not work for the main Graphics instance of Bangle.js 1 as the graphics on
Bangle.js 1 are stored in write-only memory.
*/
void jswrap_graphics_dump(JsVar *parent) {
jsiConsolePrintf("data:image/bmp;base64,");
jsvUnLock(jswrap_graphics_asBMP_X(parent, true/*printBase64*/));
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "saveScreenshot",
"#if" : "defined(DICKENS)",
"generate" : "jswrap_graphics_saveScreenshot",
"params" : [
["filename","JsVar","If supplied, a file to save, otherwise 'screenshot.img'"]
]
}
*/
void jswrap_graphics_saveScreenshot(JsVar *parent, JsVar *fileNameVar) {
#ifdef DICKENS
JsfFileName fileName = jsfNameFromString("screenshot.img");
if (fileNameVar) fileName = jsfNameFromVar(fileNameVar);
JsVar *v = jsvNewFromString("\xF0\xF0\x10");
jsfWriteFile(fileName, v, JSFF_NONE, 0, sizeof(lcdBuffer)+3);
jsvUnLock(v);
const int chunkSize = 16384;
for (int i=0;i<sizeof(lcdBuffer);i+=chunkSize) {
int s = chunkSize;
if (s+i > sizeof(lcdBuffer))
s=sizeof(lcdBuffer)-i;
JsVar *gfxBufferString = jsvNewNativeString(&lcdBuffer[i], s);
jsfWriteFile(fileName, gfxBufferString, JSFF_NONE, 3+i, 0);
jsvUnLock(gfxBufferString);
}
#endif
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "quadraticBezier",
"#if" : "!defined(SAVE_ON_FLASH) && !defined(ESPRUINOBOARD)",
"generate" : "jswrap_graphics_quadraticBezier",
"params" : [
["arr","JsVar","An array of three vertices, six enties in form of ```[x0,y0,x1,y1,x2,y2]```"],
["options","JsVar","number of points to calulate"]
],
"return" : ["JsVar", "Array with calculated points" ],
"typescript" : "quadraticBezier(arr: [number, number, number, number, number, number], options?: number): number[];"
}
Calculate the square area under a Bezier curve.
x0,y0: start point x1,y1: control point y2,y2: end point
Max 10 points without start point.
*/
JsVar *jswrap_graphics_quadraticBezier( JsVar *parent, JsVar *arr, JsVar *options ){
NOT_USED(parent);
JsVar *result = jsvNewEmptyArray();
if (!result) return 0;
if (jsvGetArrayLength(arr) != 6) return result;
int sn = 5;
typedef struct { int x,y; } XY;
XY xy[3];
int count = 0;
JsvIterator it;
jsvIteratorNew(&it, arr, JSIF_EVERY_ARRAY_ELEMENT);
for (int i=0;i<6;i++) {
((int*)xy)[i] = jsvIteratorGetIntegerValue(&it);
jsvIteratorNext(&it);
}
jsvIteratorFree(&it);
if (jsvIsObject(options)) count = jsvGetIntegerAndUnLock(jsvObjectGetChildIfExists(options,"count"));
const int FP_MUL = 4096;
const int FP_SHIFT = 12;
int dx = xy[0].x - xy[2].x;
if (dx<0) dx=-dx;
int dy = xy[0].y - xy[2].y;
if (dy<0) dy=-dy;
int dmin = (dx < dy) ? dx : dy;
if (dmin==0) dmin=1;
int s = FP_MUL*sn / dmin;
if ( s >= FP_MUL) s = FP_MUL/3;
if ( s < FP_MUL/10) s = FP_MUL/10;
if (count > 0) s = FP_MUL / count;
jsvArrayPush2Int(result, xy[0].x, xy[0].y);
for ( int t = s; t <= FP_MUL; t += s ) {
int t2 = (t*t) >> FP_SHIFT;
int tp2 = ((FP_MUL - t) * (FP_MUL - t)) >> FP_SHIFT;
int tpt = (2 * (FP_MUL - t) * t) >> FP_SHIFT;
jsvArrayPush2Int(result,
(xy[0].x * tp2 + xy[1].x * tpt + xy[2].x * t2 + (FP_MUL/2)) >> FP_SHIFT,
(xy[0].y * tp2 + xy[1].y * tpt + xy[2].y * t2 + (FP_MUL/2)) >> FP_SHIFT);
}
jsvArrayPush2Int(result, xy[2].x, xy[2].y);
return result;
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "transformVertices",
"#if" : "!defined(SAVE_ON_FLASH) && !defined(ESPRUINOBOARD)",
"generate" : "jswrap_graphics_transformVertices",
"params" : [
["verts","JsVar","An array of vertices, of the form ```[x1,y1,x2,y2,x3,y3,etc]```"],
["transformation","JsVar","The transformation to apply, either an Object or an Array (see below)"]
],
"return" : ["JsVar", "Array of transformed vertices" ],
"typescript" : "transformVertices(arr: number[], transformation: { x?: number, y?: number, scale?: number, rotate?: number } | [number, number, number, number, number, number]): number[];"
}
Transformation can be:
* An object of the form
```
{
x: float, // x offset (default 0)
y: float, // y offset (default 0)
scale: float, // scale factor (default 1)
rotate: float, // angle in radians (default 0)
}
```
* A six-element array of the form `[a,b,c,d,e,f]`, which represents the 2D transformation matrix
```
a c e
b d f
0 0 1
```
Apply a transformation to an array of vertices.
*/
JsVar *jswrap_graphics_transformVertices(JsVar *parent, JsVar *verts, JsVar *transformation) {
NOT_USED(parent);
JsVar *result = jsvNewEmptyArray();
if (!result) return 0;
if (!jsvIsIterable(verts)) return result;
double m[6];
if (jsvIsObject(transformation)) {
double x=0, y=0, scale=1, rotate=0;
jsvConfigObject configs[] = {
{"x", JSV_FLOAT, &x},
{"y", JSV_FLOAT, &y},
{"scale", JSV_FLOAT, &scale},
{"rotate", JSV_FLOAT, &rotate}
};
if (!jsvReadConfigObject(transformation, configs, sizeof(configs) / sizeof(jsvConfigObject)))
return result;
double cosr = 1, sinr = 0;
if (rotate) {
cosr = jswrap_math_cos(rotate);
sinr = jswrap_math_sin(rotate);
}
m[0] = cosr*scale; m[2] = -sinr*scale; m[4] = x;
m[1] = sinr*scale; m[3] = cosr*scale; m[5] = y;
} else if (jsvIsIterable(transformation) && jsvGetLength(transformation) == 6) {
JsvIterator it;
jsvIteratorNew(&it, transformation, JSIF_EVERY_ARRAY_ELEMENT);
for (int i = 0; i < 6; ++i) {
m[i] = jsvIteratorGetFloatValue(&it);
jsvIteratorNext(&it);
}
jsvIteratorFree(&it);
} else {
jsExceptionHere(JSET_TYPEERROR,"Expected either an object or an array with 6 entries for second argument");
return 0;
}
JsvIterator it;
jsvIteratorNew(&it, verts, JSIF_EVERY_ARRAY_ELEMENT);
while (jsvIteratorHasElement(&it)) {
double x = jsvIteratorGetFloatValue(&it);
jsvIteratorNext(&it);
if (!jsvIteratorHasElement(&it)) break;
double y = jsvIteratorGetFloatValue(&it);
jsvIteratorNext(&it);
jsvArrayPushAndUnLock(result, jsvNewFromFloat(m[0]*x + m[2]*y + m[4]));
jsvArrayPushAndUnLock(result, jsvNewFromFloat(m[1]*x + m[3]*y + m[5]));
}
jsvIteratorFree(&it);
return result;
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "floodFill",
"#if" : "!defined(SAVE_ON_FLASH) && !defined(ESPRUINOBOARD)",
"generate" : "jswrap_graphics_floodFill",
"params" : [
["x","int","X coordinate to start from"],
["y","int","Y coordinate to start from"],
["col","JsVar","The color to fill with (if undefined, foreground is used)"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics"
}
Flood fills the given Graphics instance out from a particular point.
**Note:** This only works on Graphics instances that support readback with `getPixel`. It
is also not capable of filling over dithered patterns (eg non-solid colours on Bangle.js 2)
*/
static bool _jswrap_graphics_floodFill_inside(JsGraphics *gfx, int x, int y, unsigned int col) {
if (x<0 || y<0 || x>=gfx->data.width || y>=gfx->data.height) return false;
return graphicsGetPixel(gfx, x, y)==col;
}
JsVar *jswrap_graphics_floodFill(JsVar *parent, int x, int y, JsVar *col) {
JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return 0;
if (gfx.getPixel == graphicsFallbackGetPixel) {
jsExceptionHere(JSET_ERROR, "Can't use floodFill on Graphics with no getPixel");
return 0;
}
unsigned int fillCol = gfx.data.fgColor;
if (col) fillCol = jswrap_graphics_toColor(parent, col, NULL, NULL);
// https://en.wikipedia.org/wiki/Flood_fill#Span_filling
unsigned int currentCol = graphicsGetPixel(&gfx, x, y);
const int QUEUE_LEN = 64;
short s[QUEUE_LEN];
int si = 0; // index in queue
#define S_ADD(x,y) if (si<QUEUE_LEN) {s[si++]=(short)x;s[si++]=(short)y;} else {jsiConsolePrintf("floodFill overflow\n");return jsvLockAgain(parent);}
S_ADD(x, y);
while (si) {
// get new area to work from...
short y = s[--si], x = s[--si], lx=x;
// scan left
while (_jswrap_graphics_floodFill_inside(&gfx, lx-1, y, currentCol))
graphicsSetPixel(&gfx, --lx, y, fillCol);
// scan right
while (_jswrap_graphics_floodFill_inside(&gfx, x, y, currentCol))
graphicsSetPixel(&gfx, x++, y, fillCol);
// scan down and add
bool span_added = false;
for (short ix=lx;ix<x;ix++) {
if (!_jswrap_graphics_floodFill_inside(&gfx, ix, y+1, currentCol))
span_added = false;
else if (!span_added) {
S_ADD(ix, y+1);
span_added = true;
}
}
// scan up and add
span_added = false;
for (short ix=lx;ix<x;ix++) {
if (!_jswrap_graphics_floodFill_inside(&gfx, ix, y-1, currentCol))
span_added = false;
else if (!span_added) {
S_ADD(ix, y-1);
span_added = true;
}
}
}
#undef S_ADD
return jsvLockAgain(parent);
}
/*TYPESCRIPT
type Theme = {
fg: number;
bg: number;
fg2: number;
bg2: number;
fgH: number;
bgH: number;
dark: boolean;
};
*/
/*JSON{
"type" : "property",
"class" : "Graphics",
"name" : "theme",
"#if" : "!defined(SAVE_ON_FLASH) && !defined(ESPRUINOBOARD)",
"generate" : "jswrap_graphics_theme",
"return" : ["JsVar","An object containing the current 'theme' (see below)"],
"typescript" : "theme: Theme;"
}
Returns an object of the form:
```
{
fg : 0xFFFF, // foreground colour
bg : 0, // background colour
fg2 : 0xFFFF, // accented foreground colour
bg2 : 0x0007, // accented background colour
fgH : 0xFFFF, // highlighted foreground colour
bgH : 0x02F7, // highlighted background colour
dark : true, // Is background dark (e.g. foreground should be a light colour)
}
```
These values can then be passed to `g.setColor`/`g.setBgColor` for example
`g.setColor(g.theme.fg2)`. When the Graphics instance is reset, the background
color is automatically set to `g.theme.bg` and foreground is set to
`g.theme.fg`.
On Bangle.js these values can be changed by writing updated values to `theme` in
`settings.js` and reloading the app - or they can be changed temporarily by
calling `Graphics.setTheme`
*/
JsVar *jswrap_graphics_theme(JsVar *parent) {
NOT_USED(parent);
#ifdef GRAPHICS_THEME
JsVar *o = jsvNewObject();
jsvObjectSetChildAndUnLock(o,"fg",jsvNewFromInteger((JsVarInt)(uint32_t)graphicsTheme.fg));
jsvObjectSetChildAndUnLock(o,"bg",jsvNewFromInteger((JsVarInt)(uint32_t)graphicsTheme.bg));
jsvObjectSetChildAndUnLock(o,"fg2",jsvNewFromInteger((JsVarInt)(uint32_t)graphicsTheme.fg2));
jsvObjectSetChildAndUnLock(o,"bg2",jsvNewFromInteger((JsVarInt)(uint32_t)graphicsTheme.bg2));
jsvObjectSetChildAndUnLock(o,"fgH",jsvNewFromInteger((JsVarInt)(uint32_t)graphicsTheme.fgH));
jsvObjectSetChildAndUnLock(o,"bgH",jsvNewFromInteger((JsVarInt)(uint32_t)graphicsTheme.bgH));
jsvObjectSetChildAndUnLock(o,"dark",jsvNewFromBool((JsVarInt)(uint32_t)graphicsTheme.dark));
return o;
#else
return 0;
#endif
}
/*JSON{
"type" : "method",
"class" : "Graphics",
"name" : "setTheme",
"#if" : "!defined(SAVE_ON_FLASH) && !defined(ESPRUINOBOARD)",
"generate" : "jswrap_graphics_setTheme",
"params" : [
["theme","JsVar","An object of the form returned by `Graphics.theme`"]
],
"return" : ["JsVar","The instance of Graphics this was called on, to allow call chaining"],
"return_object" : "Graphics",
"typescript" : "setTheme(theme: { [key in keyof Theme]?: Theme[key] extends number ? ColorResolvable : Theme[key] }): Graphics;"
}
Set the global colour scheme. On Bangle.js, this is reloaded from
`settings.json` for each new app loaded.
See `Graphics.theme` for the fields that can be provided. For instance you can
change the background to red using:
```
g.setTheme({bg:"#f00"});
```
*/
JsVar *jswrap_graphics_setTheme(JsVar *parent, JsVar *theme) {
#ifdef GRAPHICS_THEME
if (jsvIsObject(theme)) {
JsVar *v;
v = jsvObjectGetChildIfExists(theme, "fg");
if (v) {
graphicsTheme.fg = jswrap_graphics_toColor(parent, v,0,0);
jsvUnLock(v);
}
v = jsvObjectGetChildIfExists(theme, "bg");
if (v) {
graphicsTheme.bg = jswrap_graphics_toColor(parent, v,0,0);
jsvUnLock(v);
}
v = jsvObjectGetChildIfExists(theme, "fg2");
if (v) {
graphicsTheme.fg2 = jswrap_graphics_toColor(parent, v,0,0);
jsvUnLock(v);
}
v = jsvObjectGetChildIfExists(theme, "bg2");
if (v) {
graphicsTheme.bg2 = jswrap_graphics_toColor(parent, v,0,0);
jsvUnLock(v);
}
v = jsvObjectGetChildIfExists(theme, "fgH");
if (v) {
graphicsTheme.fgH = jswrap_graphics_toColor(parent, v,0,0);
jsvUnLock(v);
}
v = jsvObjectGetChildIfExists(theme, "bgH");
if (v) {
graphicsTheme.bgH = jswrap_graphics_toColor(parent, v,0,0);
jsvUnLock(v);
}
v = jsvObjectGetChildIfExists(theme, "dark");
if (v) graphicsTheme.dark = jsvGetBoolAndUnLock(v);
}
#endif
return jsvLockAgain(parent);
}