diff --git a/libs/bitmap/bitmap.cpp b/libs/bitmap/bitmap.cpp new file mode 100644 index 000000000..beab9f5e6 --- /dev/null +++ b/libs/bitmap/bitmap.cpp @@ -0,0 +1,1565 @@ +#include "pxt.h" + +typedef RefImage *Bitmap_; + +#define IMAGE_BITS 4 + +#if IMAGE_BITS == 1 +// OK +#elif IMAGE_BITS == 4 +// OK +#else +#error "Invalid IMAGE_BITS" +#endif + +#define XX(v) (int)(((int16_t)(v))) +#define YY(v) (int)(((int16_t)(((int32_t)(v)) >> 16))) + +namespace pxt { + +PXT_VTABLE(RefImage, ValType::Object) + +void RefImage::destroy(RefImage *t) {} + +void RefImage::print(RefImage *t) { + DMESG("RefImage %p size=%d x %d", t, t->width(), t->height()); +} + +int RefImage::wordHeight() { + if (bpp() == 1) + oops(20); + return ((height() * 4 + 31) >> 5); +} + +void RefImage::makeWritable() { + ++revision; + if (buffer->isReadOnly()) { + buffer = mkBuffer(data(), length()); + } +} + +uint8_t RefImage::fillMask(color c) { + return this->bpp() == 1 ? (c & 1) * 0xff : 0x11 * (c & 0xf); +} + +bool RefImage::inRange(int x, int y) { + return 0 <= x && x < width() && 0 <= y && y < height(); +} + +void RefImage::clamp(int *x, int *y) { + *x = min(max(*x, 0), width() - 1); + *y = min(max(*y, 0), height() - 1); +} + +RefImage::RefImage(BoxedBuffer *buf) : PXT_VTABLE_INIT(RefImage), buffer(buf) { + revision = 0; + if (!buf) + oops(21); +} + +static inline int byteSize(int w, int h, int bpp) { + if (bpp == 1) + return sizeof(ImageHeader) + ((h + 7) >> 3) * w; + else + return sizeof(ImageHeader) + (((h * 4 + 31) / 32) * 4) * w; +} + +Bitmap_ allocImage(const uint8_t *data, uint32_t sz) { + auto buf = mkBuffer(data, sz); + registerGCObj(buf); + Bitmap_ r = NEW_GC(RefImage, buf); + unregisterGCObj(buf); + return r; +} + +Bitmap_ mkImage(int width, int height, int bpp) { + if (width < 0 || height < 0 || width > 2000 || height > 2000) + return NULL; + if (bpp != 1 && bpp != 4) + return NULL; + uint32_t sz = byteSize(width, height, bpp); + Bitmap_ r = allocImage(NULL, sz); + auto hd = r->header(); + hd->magic = IMAGE_HEADER_MAGIC; + hd->bpp = bpp; + hd->width = width; + hd->height = height; + hd->padding = 0; + MEMDBG("mkImage: %d X %d => %p", width, height, r); + return r; +} + +bool isValidImage(Buffer buf) { + if (!buf || buf->length < 9) + return false; + + auto hd = (ImageHeader *)(buf->data); + if (hd->magic != IMAGE_HEADER_MAGIC || (hd->bpp != 1 && hd->bpp != 4)) + return false; + + int sz = byteSize(hd->width, hd->height, hd->bpp); + if (sz != (int)buf->length) + return false; + + return true; +} + +bool isLegacyImage(Buffer buf) { + if (!buf || buf->length < 5) + return false; + + if (buf->data[0] != 0xe1 && buf->data[0] != 0xe4) + return false; + + int sz = byteSize(buf->data[1], buf->data[2], buf->data[0] & 0xf) - 4; + if (sz != (int)buf->length) + return false; + + return true; +} + +} // namespace pxt + +namespace BitmapMethods { + +/** + * Get underlying buffer + */ +//% property +Buffer __buffer(Bitmap_ img) { + // only for simulator + return NULL; +} + +/** + * Get the width of the bitmap + */ +//% property +int width(Bitmap_ img) { + return img->width(); +} + +/** + * Get the height of the bitmap + */ +//% property +int height(Bitmap_ img) { + return img->height(); +} + +/** + * True if the bitmap is monochromatic (black and white) + */ +//% property +bool isMono(Bitmap_ img) { + return img->bpp() == 1; +} + +//% property +bool isStatic(Bitmap_ img) { + return img->buffer->isReadOnly(); +} + +//% property +bool revision(Bitmap_ img) { + return img->revision; +} + +/** + * Sets all pixels in the current bitmap from the other bitmap, which has to be of the same size and + * bpp. + */ +//% +void copyFrom(Bitmap_ img, Bitmap_ from) { + if (img->width() != from->width() || img->height() != from->height() || + img->bpp() != from->bpp()) + return; + img->makeWritable(); + memcpy(img->pix(), from->pix(), from->pixLength()); +} + +static void setCore(Bitmap_ img, int x, int y, int c) { + auto ptr = img->pix(x, y); + if (img->bpp() == 4) { + if (y & 1) + *ptr = (*ptr & 0x0f) | (c << 4); + else + *ptr = (*ptr & 0xf0) | (c & 0xf); + } else if (img->bpp() == 1) { + uint8_t mask = 0x01 << (y & 7); + if (c) + *ptr |= mask; + else + *ptr &= ~mask; + } +} + +static int getCore(Bitmap_ img, int x, int y) { + auto ptr = img->pix(x, y); + if (img->bpp() == 4) { + if (y & 1) + return *ptr >> 4; + else + return *ptr & 0x0f; + } else if (img->bpp() == 1) { + uint8_t mask = 0x01 << (y & 7); + return (*ptr & mask) ? 1 : 0; + } + return 0; +} + +/** + * Set pixel color + */ +//% +void setPixel(Bitmap_ img, int x, int y, int c) { + if (!img->inRange(x, y)) + return; + img->makeWritable(); + setCore(img, x, y, c); +} + +/** + * Get a pixel color + */ +//% +int getPixel(Bitmap_ img, int x, int y) { + if (!img->inRange(x, y)) + return 0; + return getCore(img, x, y); +} + +void fillRect(Bitmap_ img, int x, int y, int w, int h, int c); + +/** + * Fill entire bitmap with a given color + */ +//% +void fill(Bitmap_ img, int c) { + if (c && img->hasPadding()) { + fillRect(img, 0, 0, img->width(), img->height(), c); + return; + } + img->makeWritable(); + memset(img->pix(), img->fillMask(c), img->pixLength()); +} + +/** + * Copy row(s) of pixel from bitmap to buffer (8 bit per pixel). + */ +//% +void getRows(Bitmap_ img, int x, Buffer dst) { + if (img->bpp() != 4) + return; + + int w = img->width(); + int h = img->height(); + if (x >= w || x < 0) + return; + + uint8_t *sp = img->pix(x, 0); + uint8_t *dp = dst->data; + int n = min(dst->length, (w - x) * h) >> 1; + + while (n--) { + *dp++ = *sp & 0xf; + *dp++ = *sp >> 4; + sp++; + } +} + +/** + * Copy row(s) of pixel from buffer to bitmap. + */ +//% +void setRows(Bitmap_ img, int x, Buffer src) { + if (img->bpp() != 4) + return; + + int w = img->width(); + int h = img->height(); + if (x >= w || x < 0) + return; + + img->makeWritable(); + + uint8_t *dp = img->pix(x, 0); + uint8_t *sp = src->data; + int n = min(src->length, (w - x) * h) >> 1; + + while (n--) { + *dp++ = (sp[0] & 0xf) | (sp[1] << 4); + sp += 2; + } +} + +void fillRect(Bitmap_ img, int x, int y, int w, int h, int c) { + if (w == 0 || h == 0 || x >= img->width() || y >= img->height()) + return; + + int x2 = x + w - 1; + int y2 = y + h - 1; + + if (x2 < 0 || y2 < 0) + return; + + img->clamp(&x2, &y2); + img->clamp(&x, &y); + w = x2 - x + 1; + h = y2 - y + 1; + + if (!img->hasPadding() && x == 0 && y == 0 && w == img->width() && h == img->height()) { + fill(img, c); + return; + } + + img->makeWritable(); + + auto bh = img->byteHeight(); + uint8_t f = img->fillMask(c); + + uint8_t *p = img->pix(x, y); + while (w-- > 0) { + if (img->bpp() == 1) { + auto ptr = p; + unsigned mask = 0x01 << (y & 7); + + for (int i = 0; i < h; ++i) { + if (mask == 0x100) { + if (h - i >= 8) { + *++ptr = f; + i += 7; + continue; + } else { + mask = 0x01; + ++ptr; + } + } + if (c) + *ptr |= mask; + else + *ptr &= ~mask; + mask <<= 1; + } + + } else if (img->bpp() == 4) { + auto ptr = p; + unsigned mask = 0x0f; + if (y & 1) + mask <<= 4; + + for (int i = 0; i < h; ++i) { + if (mask == 0xf00) { + if (h - i >= 2) { + *++ptr = f; + i++; + continue; + } else { + mask = 0x0f; + ptr++; + } + } + *ptr = (*ptr & ~mask) | (f & mask); + mask <<= 4; + } + } + p += bh; + } +} + +//% +void _fillRect(Bitmap_ img, int xy, int wh, int c) { + fillRect(img, XX(xy), YY(xy), XX(wh), YY(wh), c); +} + +void mapRect(Bitmap_ img, int x, int y, int w, int h, Buffer map) { + if (w == 0 || h == 0 || x >= img->width() || y >= img->height()) + return; + + if (img->bpp() != 4 || map->length < 16) + return; + + int x2 = x + w - 1; + int y2 = y + h - 1; + + if (x2 < 0 || y2 < 0) + return; + + img->clamp(&x2, &y2); + img->clamp(&x, &y); + w = x2 - x + 1; + h = y2 - y + 1; + + img->makeWritable(); + + auto bh = img->byteHeight(); + auto m = map->data; + uint8_t *p = img->pix(x, y); + while (w-- > 0) { + auto ptr = p; + unsigned shift = y & 1; + for (int i = 0; i < h; i++) { + if (shift) { + *ptr = (m[*ptr >> 4] << 4) | (*ptr & 0x0f); + ptr++; + shift = 0; + } else { + *ptr = (m[*ptr & 0xf] & 0xf) | (*ptr & 0xf0); + shift = 1; + } + } + p += bh; + } +} + +//% +void _mapRect(Bitmap_ img, int xy, int wh, Buffer c) { + mapRect(img, XX(xy), YY(xy), XX(wh), YY(wh), c); +} + +//% argsNullable +bool equals(Bitmap_ img, Bitmap_ other) { + if (!other) { + return false; + } + auto len = img->length(); + if (len != other->length()) { + return false; + } + return 0 == memcmp(img->data(), other->data(), len); +} + +/** + * Return a copy of the current bitmap + */ +//% +Bitmap_ clone(Bitmap_ img) { + auto r = allocImage(img->data(), img->length()); + MEMDBG("mkImageClone: %d X %d => %p", img->width(), img->height(), r); + return r; +} + +/** + * Flips (mirrors) pixels horizontally in the current bitmap + */ +//% +void flipX(Bitmap_ img) { + img->makeWritable(); + + int bh = img->byteHeight(); + auto a = img->pix(); + auto b = img->pix(img->width() - 1, 0); + + uint8_t tmp[bh]; + + while (a < b) { + memcpy(tmp, a, bh); + memcpy(a, b, bh); + memcpy(b, tmp, bh); + a += bh; + b -= bh; + } +} + +/** + * Flips (mirrors) pixels vertically in the current bitmap + */ +//% +void flipY(Bitmap_ img) { + img->makeWritable(); + + // this is quite slow - for small 16x16 sprite it will take in the order of 1ms + // something faster requires quite a bit of bit tweaking, especially for mono bitmaps + for (int i = 0; i < img->width(); ++i) { + int a = 0; + int b = img->height() - 1; + while (a < b) { + int tmp = getCore(img, i, a); + setCore(img, i, a, getCore(img, i, b)); + setCore(img, i, b, tmp); + a++; + b--; + } + } +} + +/** + * Returns a transposed bitmap (with X/Y swapped) + */ +//% +Bitmap_ transposed(Bitmap_ img) { + Bitmap_ r = mkImage(img->height(), img->width(), img->bpp()); + + // this is quite slow + for (int i = 0; i < img->width(); ++i) { + for (int j = 0; j < img->height(); ++i) { + setCore(r, j, i, getCore(img, i, j)); + } + } + + return r; +} + +void drawBitmap(Bitmap_ img, Bitmap_ from, int x, int y); + +/** + * Every pixel in bitmap is moved by (dx,dy) + */ +//% +void scroll(Bitmap_ img, int dx, int dy) { + img->makeWritable(); + auto bh = img->byteHeight(); + auto w = img->width(); + if (dy != 0) { + // TODO one day we may want a more memory-efficient implementation + auto img2 = clone(img); + fill(img, 0); + drawBitmap(img, img2, dx, dy); + } else if (dx < 0) { + dx = -dx; + if (dx < w) + memmove(img->pix(), img->pix(dx, 0), (w - dx) * bh); + else + dx = w; + memset(img->pix(w - dx, 0), 0, dx * bh); + } else if (dx > 0) { + if (dx < w) + memmove(img->pix(dx, 0), img->pix(), (w - dx) * bh); + else + dx = w; + memset(img->pix(), 0, dx * bh); + } +} + +const uint8_t bitdouble[] = {0x00, 0x03, 0x0c, 0x0f, 0x30, 0x33, 0x3c, 0x3f, + 0xc0, 0xc3, 0xcc, 0xcf, 0xf0, 0xf3, 0xfc, 0xff}; +const uint8_t nibdouble[] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}; + +/** + * Stretches the bitmap horizontally by 100% + */ +//% +Bitmap_ doubledX(Bitmap_ img) { + if (img->width() > 126) + return NULL; + + Bitmap_ r = mkImage(img->width() * 2, img->height(), img->bpp()); + auto src = img->pix(); + auto dst = r->pix(); + auto w = img->width(); + auto bh = img->byteHeight(); + + for (int i = 0; i < w; ++i) { + memcpy(dst, src, bh); + dst += bh; + memcpy(dst, src, bh); + dst += bh; + + src += bh; + } + + return r; +} + +/** + * Stretches the bitmap vertically by 100% + */ +//% +Bitmap_ doubledY(Bitmap_ img) { + if (img->height() > 126) + return NULL; + + Bitmap_ r = mkImage(img->width(), img->height() * 2, img->bpp()); + auto src0 = img->pix(); + auto dst = r->pix(); + + auto w = img->width(); + auto sbh = img->byteHeight(); + auto bh = r->byteHeight(); + auto dbl = img->bpp() == 1 ? bitdouble : nibdouble; + + for (int i = 0; i < w; ++i) { + auto src = src0 + i * sbh; + for (int j = 0; j < bh; j += 2) { + *dst++ = dbl[*src & 0xf]; + if (j != bh - 1) + *dst++ = dbl[*src >> 4]; + src++; + } + } + + return r; +} + +/** + * Replaces one color in an bitmap with another + */ +//% +void replace(Bitmap_ img, int from, int to) { + if (img->bpp() != 4) + return; + to &= 0xf; + if (from == to) + return; + + img->makeWritable(); + + // avoid bleeding 'to' color into the overflow areas of the picture + if (from == 0 && img->hasPadding()) { + for (int i = 0; i < img->height(); ++i) + for (int j = 0; j < img->width(); ++j) + if (getCore(img, j, i) == from) + setCore(img, j, i, to); + return; + } + + auto ptr = img->pix(); + auto len = img->pixLength(); + while (len--) { + auto b = *ptr; + if ((b & 0xf) == from) + b = (b & 0xf0) | to; + if ((b >> 4) == from) + b = (to << 4) | (b & 0xf); + *ptr++ = b; + } +} + +/** + * Stretches the bitmap in both directions by 100% + */ +//% +Bitmap_ doubled(Bitmap_ img) { + Bitmap_ tmp = doubledX(img); + registerGCObj(tmp); + Bitmap_ r = doubledY(tmp); + unregisterGCObj(tmp); + return r; +} + +bool drawBitmapCore(Bitmap_ img, Bitmap_ from, int x, int y, int color) { + auto w = from->width(); + auto h = from->height(); + auto sh = img->height(); + auto sw = img->width(); + + if (x + w <= 0) + return false; + if (x >= sw) + return false; + if (y + h <= 0) + return false; + if (y >= sh) + return false; + + auto len = y < 0 ? min(sh, h + y) : min(sh - y, h); + auto tbp = img->bpp(); + auto fbp = from->bpp(); + auto y0 = y; + + if (color == -2 && x == 0 && y == 0 && tbp == fbp && w == sw && h == sh) { + copyFrom(img, from); + return false; + } + + // DMESG("drawIMG(%d,%d) at (%d,%d) w=%d bh=%d len=%d", + // w,h,x, y, img->width(), img->byteHeight(), len ); + + auto fromH = from->byteHeight(); + auto imgH = img->byteHeight(); + auto fromBase = from->pix(); + auto imgBase = img->pix(0, y); + +#define LOOPHD \ + for (int xx = 0; xx < w; ++xx, ++x) \ + if (0 <= x && x < sw) + + if (tbp == 4 && fbp == 4) { + auto wordH = fromH >> 2; + LOOPHD { + y = y0; + + auto fdata = (uint32_t *)fromBase + wordH * xx; + auto tdata = imgBase + imgH * x; + + // DMESG("%d,%d xx=%d/%d - %p (%p) -- %d",x,y,xx,w,tdata,img->pix(), + // (uint8_t*)fdata - from->pix()); + + auto cnt = wordH; + auto bot = min(sh, y + h); + +#define COLS(s) ((v >> (s)) & 0xf) +#define COL(s) COLS(s) + +#define STEPA(s) \ + if (COL(s) && 0 <= y && y < bot) \ + SETLOW(s); \ + y++; +#define STEPB(s) \ + if (COL(s) && 0 <= y && y < bot) \ + SETHIGH(s); \ + y++; \ + tdata++; +#define STEPAQ(s) \ + if (COL(s)) \ + SETLOW(s); +#define STEPBQ(s) \ + if (COL(s)) \ + SETHIGH(s); \ + tdata++; + +// perf: expanded version 5% faster +#define ORDER(A, B) \ + A(0); \ + B(4); \ + A(8); \ + B(12); \ + A(16); \ + B(20); \ + A(24); \ + B(28) +//#define ORDER(A,B) for (int k = 0; k < 32; k += 8) { A(k); B(4+k); } +#define LOOP(A, B, xbot) \ + while (cnt--) { \ + auto v = *fdata++; \ + if (0 <= y && y <= xbot - 8) { \ + ORDER(A##Q, B##Q); \ + y += 8; \ + } else { \ + ORDER(A, B); \ + } \ + } +#define LOOPS(xbot) \ + if (y & 1) \ + LOOP(STEPB, STEPA, xbot) \ + else \ + LOOP(STEPA, STEPB, xbot) + + if (color >= 0) { +#define SETHIGH(s) *tdata = (*tdata & 0x0f) | ((COLS(s)) << 4) +#define SETLOW(s) *tdata = (*tdata & 0xf0) | COLS(s) + LOOPS(sh) + } else if (color == -2) { +#undef COL +#define COL(s) 1 + LOOPS(bot) + } else { +#undef COL +#define COL(s) COLS(s) +#undef SETHIGH +#define SETHIGH(s) \ + if (*tdata & 0xf0) \ + return true +#undef SETLOW +#define SETLOW(s) \ + if (*tdata & 0x0f) \ + return true + LOOPS(sh) + } + } + } else if (tbp == 1 && fbp == 1) { + auto left = img->pix() - imgBase; + auto right = img->pix(0, img->height() - 1) - imgBase; + LOOPHD { + y = y0; + + auto data = fromBase + fromH * xx; + auto off = imgBase + imgH * x; + auto off0 = off + left; + auto off1 = off + right; + + int shift = (y & 7); + + int y1 = y + h + (y & 7); + int prev = 0; + + while (y < y1 - 8) { + int curr = *data++ << shift; + if (off0 <= off && off <= off1) { + uint8_t v = (curr >> 0) | (prev >> 8); + + if (color == -1) { + if (*off & v) + return true; + } else { + *off |= v; + } + } + off++; + prev = curr; + y += 8; + } + + int left = y1 - y; + if (left > 0) { + int curr = *data << shift; + if (off0 <= off && off <= off1) { + uint8_t v = ((curr >> 0) | (prev >> 8)) & (0xff >> (8 - left)); + if (color == -1) { + if (*off & v) + return true; + } else { + *off |= v; + } + } + } + } + } else if (tbp == 4 && fbp == 1) { + if (y < 0) { + fromBase = from->pix(0, -y); + imgBase = img->pix(); + } + // icon mode + LOOPHD { + auto fdata = fromBase + fromH * xx; + auto tdata = imgBase + imgH * x; + + unsigned mask = 0x01; + auto v = *fdata++; + int off = (y & 1) ? 1 : 0; + if (y < 0) { + mask <<= -y & 7; + off = 0; + } + for (int i = off; i < len + off; ++i) { + if (mask == 0x100) { + mask = 0x01; + v = *fdata++; + } + if (v & mask) { + if (i & 1) + *tdata = (*tdata & 0x0f) | (color << 4); + else + *tdata = (*tdata & 0xf0) | color; + } + mask <<= 1; + if (i & 1) + tdata++; + } + } + } + + return false; +} + +/** + * Draw given bitmap on the current bitmap + */ +//% +void drawBitmap(Bitmap_ img, Bitmap_ from, int x, int y) { + img->makeWritable(); + if (img->bpp() == 4 && from->bpp() == 4) { + drawBitmapCore(img, from, x, y, -2); + } else { + fillRect(img, x, y, from->width(), from->height(), 0); + drawBitmapCore(img, from, x, y, 0); + } +} + +/** + * Draw given bitmap with transparent background on the current bitmap + */ +//% +void drawTransparentBitmap(Bitmap_ img, Bitmap_ from, int x, int y) { + img->makeWritable(); + drawBitmapCore(img, from, x, y, 0); +} + +/** + * Check if the current bitmap "collides" with another + */ +//% +bool overlapsWith(Bitmap_ img, Bitmap_ other, int x, int y) { + return drawBitmapCore(img, other, x, y, -1); +} + +// Bitmap_ format (legacy) +// byte 0: magic 0xe4 - 4 bit color; 0xe1 is monochromatic +// byte 1: width in pixels +// byte 2: height in pixels +// byte 3: padding (should be zero) +// byte 4...N: data 4 bits per pixels, high order nibble printed first, lines aligned to 32 bit +// words byte 4...N: data 1 bit per pixels, high order bit printed first, lines aligned to byte + +Bitmap_ convertAndWrap(Buffer buf) { + if (isValidImage(buf)) + return NEW_GC(RefImage, buf); + + // What follows in this function is mostly dead code, except if people construct bitmap buffers + // by hand. Probably safe to remove in a year (middle of 2020) or so. When removing, also remove + // from sim. + if (!isLegacyImage(buf)) + return NULL; + + auto tmp = mkBuffer(NULL, buf->length + 4); + auto hd = (ImageHeader *)tmp->data; + auto src = buf->data; + hd->magic = IMAGE_HEADER_MAGIC; + hd->bpp = src[0] & 0xf; + hd->width = src[1]; + hd->height = src[2]; + hd->padding = 0; + memcpy(hd->pixels, src + 4, buf->length - 4); + + registerGCObj(tmp); + auto r = NEW_GC(RefImage, tmp); + unregisterGCObj(tmp); + return r; +} + +//% +void _drawIcon(Bitmap_ img, Buffer icon, int xy, int c) { + img->makeWritable(); + + auto iconImg = convertAndWrap(icon); + if (!iconImg || iconImg->bpp() != 1) + return; + + drawBitmapCore(img, iconImg, XX(xy), YY(xy), c); +} + +static void drawLineLow(Bitmap_ img, int x0, int y0, int x1, int y1, int c) { + int dx = x1 - x0; + int dy = y1 - y0; + int yi = 1; + if (dy < 0) { + yi = -1; + dy = -dy; + } + int D = 2 * dy - dx; + dx <<= 1; + dy <<= 1; + int y = y0; + for (int x = x0; x <= x1; ++x) { + setCore(img, x, y, c); + if (D > 0) { + y += yi; + D -= dx; + } + D += dy; + } +} + +static void drawLineHigh(Bitmap_ img, int x0, int y0, int x1, int y1, int c) { + int dx = x1 - x0; + int dy = y1 - y0; + int xi = 1; + if (dx < 0) { + xi = -1; + dx = -dx; + } + int D = 2 * dx - dy; + dx <<= 1; + dy <<= 1; + int x = x0; + for (int y = y0; y <= y1; ++y) { + setCore(img, x, y, c); + if (D > 0) { + x += xi; + D -= dy; + } + D += dx; + } +} + +void drawLine(Bitmap_ img, int x0, int y0, int x1, int y1, int c) { + if (x1 < x0) { + drawLine(img, x1, y1, x0, y0, c); + return; + } + int w = x1 - x0; + int h = y1 - y0; + + if (h == 0) { + if (w == 0) + setPixel(img, x0, y0, c); + else + fillRect(img, x0, y0, w + 1, 1, c); + return; + } + + if (w == 0) { + if (h > 0) + fillRect(img, x0, y0, 1, h + 1, c); + else + fillRect(img, x0, y1, 1, -h + 1, c); + return; + } + + if (x1 < 0 || x0 >= img->width()) + return; + if (x0 < 0) { + y0 -= (h * x0 / w); + x0 = 0; + } + if (x1 >= img->width()) { + int d = (img->width() - 1) - x1; + y1 += (h * d / w); + x1 = img->width() - 1; + } + + if (y0 < y1) { + if (y0 >= img->height() || y1 < 0) + return; + if (y0 < 0) { + x0 -= (w * y0 / h); + y0 = 0; + } + if (y1 >= img->height()) { + int d = (img->height() - 1) - y1; + x1 += (w * d / h); + y1 = img->height() - 1; + } + } else { + if (y1 >= img->height() || y0 < 0) + return; + if (y1 < 0) { + x1 -= (w * y1 / h); + y1 = 0; + } + if (y0 >= img->height()) { + int d = (img->height() - 1) - y0; + x0 += (w * d / h); + y0 = img->height() - 1; + } + } + + img->makeWritable(); + + if (h < 0) { + h = -h; + if (h < w) + drawLineLow(img, x0, y0, x1, y1, c); + else + drawLineHigh(img, x1, y1, x0, y0, c); + } else { + if (h < w) + drawLineLow(img, x0, y0, x1, y1, c); + else + drawLineHigh(img, x0, y0, x1, y1, c); + } +} + +//% +void _drawLine(Bitmap_ img, int xy, int wh, int c) { + drawLine(img, XX(xy), YY(xy), XX(wh), YY(wh), c); +} + +void blitRow(Bitmap_ img, int x, int y, Bitmap_ from, int fromX, int fromH) { + if (!img->inRange(x, 0) || !img->inRange(fromX, 0) || fromH <= 0) + return; + + if (img->bpp() != 4 || from->bpp() != 4) + return; + + int fy = 0; + int stepFY = (from->width() << 16) / fromH; + int endY = y + fromH; + if (endY > img->height()) + endY = img->height(); + if (y < 0) { + fy += -y * stepFY; + y = 0; + } + + auto dp = img->pix(x, y); + auto sp = from->pix(fromX, 0); + + while (y < endY) { + int p = fy >> 16, c; + if (p & 1) + c = sp[p >> 1] >> 4; + else + c = sp[p >> 1] & 0xf; + if (y & 1) { + *dp = (*dp & 0x0f) | (c << 4); + dp++; + } else { + *dp = (*dp & 0xf0) | (c & 0xf); + } + y++; + fy += stepFY; + } +} + +//% +void _blitRow(Bitmap_ img, int xy, Bitmap_ from, int xh) { + blitRow(img, XX(xy), YY(xy), from, XX(xh), YY(xh)); +} + +bool blit(Bitmap_ dst, Bitmap_ src, pxt::RefCollection *args) { + int xDst = pxt::toInt(args->getAt(0)); + int yDst = pxt::toInt(args->getAt(1)); + int wDst = pxt::toInt(args->getAt(2)); + int hDst = pxt::toInt(args->getAt(3)); + int xSrc = pxt::toInt(args->getAt(4)); + int ySrc = pxt::toInt(args->getAt(5)); + int wSrc = pxt::toInt(args->getAt(6)); + int hSrc = pxt::toInt(args->getAt(7)); + bool transparent = pxt::toBoolQuick(args->getAt(8)); + bool check = pxt::toBoolQuick(args->getAt(9)); + + int xSrcStep = (wSrc << 16) / wDst; + int ySrcStep = (hSrc << 16) / hDst; + + int xDstClip = abs(min(0, xDst)); + int yDstClip = abs(min(0, yDst)); + int xDstStart = xDst + xDstClip; + int yDstStart = yDst + yDstClip; + int xDstEnd = min(dst->width(), xDst + wDst); + int yDstEnd = min(dst->height(), yDst + hDst); + + int xSrcStart = max(0, (xSrc << 16) + xDstClip * xSrcStep); + int ySrcStart = max(0, (ySrc << 16) + yDstClip * ySrcStep); + int xSrcEnd = min(src->width(), xSrc + wSrc) << 16; + int ySrcEnd = min(src->height(), ySrc + hSrc) << 16; + + if (!check) + dst->makeWritable(); + + for (int yDstCur = yDstStart, ySrcCur = ySrcStart; yDstCur < yDstEnd && ySrcCur < ySrcEnd; ++yDstCur, ySrcCur += ySrcStep) { + int ySrcCurI = ySrcCur >> 16; + for (int xDstCur = xDstStart, xSrcCur = xSrcStart; xDstCur < xDstEnd && xSrcCur < xSrcEnd; ++xDstCur, xSrcCur += xSrcStep) { + int xSrcCurI = xSrcCur >> 16; + int cSrc = getCore(src, xSrcCurI, ySrcCurI); + if (check && cSrc) { + int cDst = getCore(dst, xDstCur, yDstCur); + if (cDst) { + return true; + } + continue; + } + if (!transparent || cSrc) { + setCore(dst, xDstCur, yDstCur, cSrc); + } + } + } + return false; +} + +//% +bool _blit(Bitmap_ img, Bitmap_ src, pxt::RefCollection *args) { + return blit(img, src, args); +} + +void fillCircle(Bitmap_ img, int cx, int cy, int r, int c) { + int x = r - 1; + int y = 0; + int dx = 1; + int dy = 1; + int err = dx - (r << 1); + + while (x >= y) { + fillRect(img, cx + x, cy - y, 1, 1 + (y << 1), c); + fillRect(img, cx + y, cy - x, 1, 1 + (x << 1), c); + fillRect(img, cx - x, cy - y, 1, 1 + (y << 1), c); + fillRect(img, cx - y, cy - x, 1, 1 + (x << 1), c); + if (err <= 0) { + ++y; + err += dy; + dy += 2; + } else { + --x; + dx += 2; + err += dx - (r << 1); + } + } +} + +//% +void _fillCircle(Bitmap_ img, int cxy, int r, int c) { + fillCircle(img, XX(cxy), YY(cxy), r, c); +} + +typedef struct +{ + int x, y; + int x0, y0; + int x1, y1; + int W,H; + int dx, dy; + int yi, xi; + int D; + int nextFuncIndex; +} LineGenState; // For keeping track of the state when generating Y values for a line, even when moving to the next X. + +typedef struct +{ + int min; + int max; +} ValueRange; + +void nextYRange_Low(int x, LineGenState *line, ValueRange *yRange) { + while (line->x == x && line->x <= line->x1 && line->x < line->W) { + if (0 <= line->x) { + if (line->y < yRange->min) yRange->min = line->y; + if (line->y > yRange->max) yRange->max = line->y; + } + if (line->D > 0) { + line->y += line->yi; + line->D -= line->dx; + } + line->D += line->dy; + ++line->x; + } +} + +void nextYRange_HighUp(int x, LineGenState *line, ValueRange *yRange) { + while (line->x == x && line->y >= line->y1 && line->x < line->W) { + if (0 <= line->x) { + if (line->y < yRange->min) yRange->min = line->y; + if (line->y > yRange->max) yRange->max = line->y; + } + if (line->D > 0) { + line->x += line->xi; + line->D += line->dy; + } + line->D += line->dx; + --line->y; + } +} +// This function is similar to the sub-function drawLineHigh for drawLine. However, it yields back after calculating all Y values of a given X. When the function is called again, it continues from the state where it yielded back previously. +void nextYRange_HighDown(int x, LineGenState *line, ValueRange *yRange) { + while (line->x == x && line->y <= line->y1 && line->x < line->W) { + if (0 <= line->x) { + if (line->y < yRange->min) yRange->min = line->y; + if (line->y > yRange->max) yRange->max = line->y; + } + if (line->D > 0) { + line->x += line->xi; + line->D -= line->dy; + } + line->D += line->dx; + ++line->y; + } +} + +LineGenState initYRangeGenerator(int16_t X0, int16_t Y0, int16_t X1, int16_t Y1) { + LineGenState line; + + line.x0 = X0, line.y0 = Y0, line.x1 = X1, line.y1 = Y1; + + line.dx = line.x1 - line.x0; + line.dy = line.y1 - line.y0; + line.y = line.y0; + line.x = line.x0; + + if ((line.dy < 0 ? -line.dy : line.dy) < line.dx) { + line.yi = 1; + if (line.dy < 0) { + line.yi = -1; + line.dy = -line.dy; + } + line.D = 2 * line.dy - line.dx; + line.dx <<= 1; + line.dy <<= 1; + + line.nextFuncIndex = 0; + return line; + } else { + line.xi = 1; + // if (dx < 0) {//should not hit + // PANIC(); + // } + if (line.dy < 0) { + line.D = 2 * line.dx + line.dy; + line.dx <<= 1; + line.dy <<= 1; + + line.nextFuncIndex = 1; + return line; + } else { + line.D = 2 * line.dx - line.dy; + line.dx <<= 1; + line.dy <<= 1; + + line.nextFuncIndex = 2; + return line; + } + } +} + +// core of draw vertical line for repeatly calling, eg.: fillTriangle() or fillPolygon4() +// value range/safety check not included +// prepare "img->makeWritable();" and "uint8_t f = img->fillMask(c);" outside required. +// bpp=4 support only right now +void drawVLineCore(Bitmap_ img, int x, int y, int h, uint8_t f) { + uint8_t *p = img->pix(x, y); + auto ptr = p; + unsigned mask = 0x0f; + if (y & 1) + mask <<= 4; + for (int i = 0; i < h; ++i) { + if (mask == 0xf00) { + if (h - i >= 2) { + *++ptr = f; + i++; + continue; + } else { + mask = 0x0f; + ptr++; + } + } + *ptr = (*ptr & ~mask) | (f & mask); + mask <<= 4; + } +} + +void drawVLine(Bitmap_ img, int x, int y, int h, int c) { + int H = height(img); + uint8_t f = img->fillMask(c); + if (x < 0 || x >= width(img) || y >= H || y + h - 1 < 0) + return; + if (y < 0){ + h += y; + y = 0; + } + if (y + h > H) + h = H - y; + drawVLineCore(img, x, y, h, f); +} + +void fillTriangle(Bitmap_ img, int x0, int y0, int x1, int y1, int x2, int y2, int c) { + if (x1 < x0) { + pxt::swap(x0, x1); + pxt::swap(y0, y1); + } + if (x2 < x1) { + pxt::swap(x1, x2); + pxt::swap(y1, y2); + } + if (x1 < x0) { + pxt::swap(x0, x1); + pxt::swap(y0, y1); + } + + LineGenState lines[] = { + initYRangeGenerator(x0, y0, x2, y2), + initYRangeGenerator(x0, y0, x1, y1), + initYRangeGenerator(x1, y1, x2, y2) + }; + + int W = width(img), H = height(img); + lines[0].W = lines[1].W = lines[2].W = W; + lines[0].H = lines[1].H = lines[2].H = H; + + // We have 3 different sub-functions to generate Ys of edges, each particular edge maps to one of them. + // Use function pointers to avoid judging which function to call at every X. + typedef void (*FP_NEXT)(int x, LineGenState *line, ValueRange *yRange); + FP_NEXT nextFuncList[] = { nextYRange_Low, nextYRange_HighUp, nextYRange_HighDown }; + FP_NEXT fpNext0 = nextFuncList[lines[0].nextFuncIndex]; + FP_NEXT fpNext1 = nextFuncList[lines[1].nextFuncIndex]; + FP_NEXT fpNext2 = nextFuncList[lines[2].nextFuncIndex]; + + ValueRange yRange = {H, -1}; + img->makeWritable(); + uint8_t f = img->fillMask(c); + + for (int x = lines[1].x0; x <= min(x1, W - 1); x++) { + yRange.min = H; + yRange.max = -1; + fpNext0(x, &lines[0], &yRange); + fpNext1(x, &lines[1], &yRange); + + if (x < 0 || yRange.min >= H || yRange.max < 0) + continue; + if (yRange.min < 0) + yRange.min = 0; + if (yRange.max >= H) + yRange.max = H - 1; + drawVLineCore(img, x, yRange.min, yRange.max - yRange.min + 1, f); + } + + fpNext2(lines[2].x0, &lines[2], &yRange); + + for (int x = lines[2].x0 + 1; x <= min(x2, W - 1); x++) { + yRange.min = H; + yRange.max = -1; + fpNext0(x, &lines[0], &yRange); + fpNext2(x, &lines[2], &yRange); + + if (x < 0 || yRange.min >= H || yRange.max < 0) + continue; + if (yRange.min < 0) + yRange.min = 0; + if (yRange.max >= H) + yRange.max = H - 1; + drawVLineCore(img, x, yRange.min, yRange.max - yRange.min + 1, f); + } +} + +void fillPolygon4(Bitmap_ img, int x0, int y0, int x1, int y1, int x2, int y2, int x3, int y3, int c) { + LineGenState lines[] = { + (x0 < x1) ? initYRangeGenerator(x0, y0, x1, y1) : initYRangeGenerator(x1, y1, x0, y0), + (x1 < x2) ? initYRangeGenerator(x1, y1, x2, y2) : initYRangeGenerator(x2, y2, x1, y1), + (x2 < x3) ? initYRangeGenerator(x2, y2, x3, y3) : initYRangeGenerator(x3, y3, x2, y2), + (x0 < x3) ? initYRangeGenerator(x0, y0, x3, y3) : initYRangeGenerator(x3, y3, x0, y0)}; + + int W = width(img), H = height(img); + lines[0].W = lines[1].W = lines[2].W = lines[3].W = W; + lines[0].H = lines[1].H = lines[2].H = lines[3].H = H; + + int minX = min(min(x0, x1), min(x2, x3)); + int maxX = min(max(max(x0, x1), max(x2, x3)), W - 1); + + typedef void (*FP_NEXT)(int x, LineGenState *line, ValueRange *yRange); + FP_NEXT nextFuncList[] = { nextYRange_Low, nextYRange_HighUp, nextYRange_HighDown }; + FP_NEXT fpNext0 = nextFuncList[lines[0].nextFuncIndex]; + FP_NEXT fpNext1 = nextFuncList[lines[1].nextFuncIndex]; + FP_NEXT fpNext2 = nextFuncList[lines[2].nextFuncIndex]; + FP_NEXT fpNext3 = nextFuncList[lines[3].nextFuncIndex]; + + ValueRange yRange = { H, -1 }; + img->makeWritable(); + uint8_t f = img->fillMask(c); + + for (int x = minX; x <= maxX; x++) { + yRange.min = H; + yRange.max = -1; + fpNext0(x, &lines[0], &yRange); + fpNext1(x, &lines[1], &yRange); + fpNext2(x, &lines[2], &yRange); + fpNext3(x, &lines[3], &yRange); + + if (x < 0 || yRange.min >= H || yRange.max < 0) + continue; + if (yRange.min < 0) + yRange.min = 0; + if (yRange.max >= H) + yRange.max = H - 1; + drawVLineCore(img, x, yRange.min, yRange.max - yRange.min + 1, f); + } +} + +//% +void _fillTriangle(Bitmap_ img, pxt::RefCollection *args) { + fillTriangle( + img, + pxt::toInt(args->getAt(0)), + pxt::toInt(args->getAt(1)), + pxt::toInt(args->getAt(2)), + pxt::toInt(args->getAt(3)), + pxt::toInt(args->getAt(4)), + pxt::toInt(args->getAt(5)), + pxt::toInt(args->getAt(6)) + ); +} + +// This polygon fill is similar to fillTriangle(): Scan minY and maxY of all edges at each X, and draw a vertical line between (x,minY)~(x,maxY). +// The main difference is that it sorts the endpoints of each edge, x0 < x1, to draw from left to right, but doesn't sort the edges as it's too time consuming. +// Instead, just call next(), which returns immediately if the x is not in range of the edge in horizon. +// NOTE: Unlike triangles, edges of a polygon can cross a vertical line at a given X multi time. This algorithm can fill correctly only if edges meet this condition: Any vertical line(x) cross edges at most 2 times. +// Fortunately, no matter what perspective transform is applied, a rectangle/trapezoid will still meet this condition. +// Ref: https://forum.makecode.com/t/new-3d-engine-help-filling-4-sided-polygons/18641/9 +//% +void _fillPolygon4(Bitmap_ img, pxt::RefCollection *args) { + fillPolygon4( + img, + pxt::toInt(args->getAt(0)), + pxt::toInt(args->getAt(1)), + pxt::toInt(args->getAt(2)), + pxt::toInt(args->getAt(3)), + pxt::toInt(args->getAt(4)), + pxt::toInt(args->getAt(5)), + pxt::toInt(args->getAt(6)), + pxt::toInt(args->getAt(7)), + pxt::toInt(args->getAt(8)) + ); +} + +} // namespace BitmapMethods + +namespace bitmap { +/** + * Create new bitmap with given content + */ +//% +Bitmap_ ofBuffer(Buffer buf) { + return BitmapMethods::convertAndWrap(buf); +} +} + +namespace bitmap { +/** + * Create new empty (transparent) bitmap + */ +//% +Bitmap_ create(int width, int height) { + Bitmap_ r = mkImage(width, height, IMAGE_BITS); + if (r) + memset(r->pix(), 0, r->pixLength()); + else + target_panic(PANIC_INVALID_IMAGE); + return r; +} + +/** + * Double the size of an icon + */ +//% +Buffer doubledIcon(Buffer icon) { + if (!isValidImage(icon)) + return NULL; + + auto r = NEW_GC(RefImage, icon); + registerGCObj(r); + auto t = BitmapMethods::doubled(r); + unregisterGCObj(r); + return t->buffer; +} + +} // namespace bitmap + +// This is 6.5x faster than standard on word-aligned copy +// probably should move to codal + +#ifndef __linux__ +extern "C" void *memcpy(void *dst, const void *src, size_t sz) { + void *dst0 = dst; + if (sz >= 4 && !((uintptr_t)dst & 3) && !((uintptr_t)src & 3)) { + size_t cnt = sz >> 2; + uint32_t *d = (uint32_t *)dst; + const uint32_t *s = (const uint32_t *)src; + while (cnt--) { + *d++ = *s++; + } + sz &= 3; + dst = d; + src = s; + } + + // see comment in memset() below (have not seen optimization here, but better safe than sorry) + volatile uint8_t *dd = (uint8_t *)dst; + volatile uint8_t *ss = (uint8_t *)src; + + while (sz--) { + *dd++ = *ss++; + } + + return dst0; +} + +extern "C" void *memset(void *dst, int v, size_t sz) { + void *dst0 = dst; + if (sz >= 4 && !((uintptr_t)dst & 3)) { + size_t cnt = sz >> 2; + uint32_t vv = 0x01010101 * v; + uint32_t *d = (uint32_t *)dst; + while (cnt--) { + *d++ = vv; + } + sz &= 3; + dst = d; + } + + // without volatile here, GCC may optimize the loop to memset() call which is obviously not great + volatile uint8_t *dd = (uint8_t *)dst; + + while (sz--) { + *dd++ = v; + } + + return dst0; +} +#endif diff --git a/libs/bitmap/bitmap.ts b/libs/bitmap/bitmap.ts new file mode 100644 index 000000000..92eee9923 --- /dev/null +++ b/libs/bitmap/bitmap.ts @@ -0,0 +1,292 @@ +type color = number + +namespace bitmap { + export function repeatY(count: number, image: Bitmap) { + let arr = [image] + while (--count > 0) + arr.push(image) + return concatY(arr) + } + + export function concatY(images: Bitmap[]) { + let w = 0 + let h = 0 + for (let img of images) { + w = Math.max(img.width, w) + h += img.height + } + let r = bitmap.create(w, h) + let y = 0 + for (let img of images) { + let x = (w - img.width) >> 1 + r.drawBitmap(img, x, y) + y += img.height + } + return r + } +} + + +//% snippet='bmp` `' +//% pySnippet='bmp(""" """)' +//% fixedInstances +interface Bitmap { + /** + * Draw an icon (monochromatic image) using given color + */ + //% helper=imageDrawIcon + drawIcon(icon: Buffer, x: number, y: number, c: color): void; + + /** + * Fill a rectangle + */ + //% helper=imageFillRect + fillRect(x: number, y: number, w: number, h: number, c: color): void; + + /** + * Draw a line + */ + //% helper=imageDrawLine + drawLine(x0: number, y0: number, x1: number, y1: number, c: color): void; + + /** + * Draw an empty rectangle + */ + //% helper=imageDrawRect + drawRect(x: number, y: number, w: number, h: number, c: color): void; + + /** + * Draw a circle + */ + //% helper=imageDrawCircle + drawCircle(cx: number, cy: number, r: number, c: color): void; + + /** + * Fills a circle + */ + //% helper=imageFillCircle + fillCircle(cx: number, cy: number, r: number, c: color): void; + + /** + * Fills a triangle + */ + //% helper=imageFillTriangle + fillTriangle(x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, col: number): void; + + /** + * Fills a 4-side-polygon + */ + //% helper=imageFillPolygon4 + fillPolygon4(x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, col: number): void; + + /** + * Returns an image rotated by -90, 0, 90, 180, 270 deg clockwise + */ + //% helper=imageRotated + rotated(deg: number): Bitmap; + + /** + * Scale and copy a row of pixels from a texture. + */ + //% helper=imageBlitRow + blitRow(dstX: number, dstY: number, from: Bitmap, fromX: number, fromH: number): void; + + /** + * Copy an image from a source rectangle to a destination rectangle, stretching or + * compressing to fit the dimensions of the destination rectangle, if necessary. + */ + //% helper=imageBlit + blit(xDst: number, yDst: number, wDst: number, hDst: number, src: Bitmap, xSrc: number, ySrc: number, wSrc: number, hSrc: number, transparent: boolean, check: boolean): boolean; +} + +interface ScreenBitmap extends Bitmap { + /** + * Sets the screen backlight brightness (10-100) + */ + //% helper=setScreenBrightness + setBrightness(deg: number): Bitmap; + + /** + * Gets current screen backlight brightness (0-100) + */ + //% helper=screenBrightness + brightness(): number; +} + +// pxt compiler currently crashes on non-functions in helpers namespace; will fix +namespace _helpers_workaround { + export let brightness = 100 +} + +namespace helpers { + //% shim=BitmapMethods::_drawLine + function _drawLine(img: Bitmap, xy: number, wh: number, c: color): void { } + + //% shim=BitmapMethods::_fillRect + function _fillRect(img: Bitmap, xy: number, wh: number, c: color): void { } + + //% shim=BitmapMethods::_mapRect + function _mapRect(img: Bitmap, xy: number, wh: number, m: Buffer): void { } + + //% shim=BitmapMethods::_drawIcon + function _drawIcon(img: Bitmap, icon: Buffer, xy: number, c: color): void { } + + //% shim=BitmapMethods::_fillCircle + declare function _fillCircle(img: Bitmap, cxy: number, r: number, c: color): void; + + //% shim=BitmapMethods::_blitRow + declare function _blitRow(img: Bitmap, xy: number, from: Bitmap, xh: number): void; + + //% shim=BitmapMethods::_blit + declare function _blit(img: Bitmap, src: Bitmap, args: number[]): boolean; + + //% shim=BitmapMethods::_fillTriangle + declare function _fillTriangle(img: Bitmap, args: number[]): void; + + //% shim=BitmapMethods::_fillPolygon4 + declare function _fillPolygon4(img: Bitmap, args: number[]): void; + + function pack(x: number, y: number) { + return (Math.clamp(-30000, 30000, x | 0) & 0xffff) | (Math.clamp(-30000, 30000, y | 0) << 16) + } + + let _blitArgs: number[]; + + export function imageBlit(img: Bitmap, xDst: number, yDst: number, wDst: number, hDst: number, src: Bitmap, xSrc: number, ySrc: number, wSrc: number, hSrc: number, transparent: boolean, check: boolean): boolean { + _blitArgs = _blitArgs || []; + _blitArgs[0] = xDst | 0; + _blitArgs[1] = yDst | 0; + _blitArgs[2] = wDst | 0; + _blitArgs[3] = hDst | 0; + _blitArgs[4] = xSrc | 0; + _blitArgs[5] = ySrc | 0; + _blitArgs[6] = wSrc | 0; + _blitArgs[7] = hSrc | 0; + _blitArgs[8] = transparent ? 1 : 0; + _blitArgs[9] = check ? 1 : 0; + return _blit(img, src, _blitArgs); + } + + export function imageBlitRow(img: Bitmap, dstX: number, dstY: number, from: Bitmap, fromX: number, fromH: number): void { + _blitRow(img, pack(dstX, dstY), from, pack(fromX, fromH)) + } + + export function imageDrawIcon(img: Bitmap, icon: Buffer, x: number, y: number, c: color): void { + _drawIcon(img, icon, pack(x, y), c) + } + export function imageFillRect(img: Bitmap, x: number, y: number, w: number, h: number, c: color): void { + _fillRect(img, pack(x, y), pack(w, h), c) + } + export function imageMapRect(img: Bitmap, x: number, y: number, w: number, h: number, m: Buffer): void { + _mapRect(img, pack(x, y), pack(w, h), m) + } + export function imageDrawLine(img: Bitmap, x: number, y: number, w: number, h: number, c: color): void { + _drawLine(img, pack(x, y), pack(w, h), c) + } + export function imageDrawRect(img: Bitmap, x: number, y: number, w: number, h: number, c: color): void { + if (w == 0 || h == 0) return + w-- + h-- + imageDrawLine(img, x, y, x + w, y, c) + imageDrawLine(img, x, y, x, y + h, c) + imageDrawLine(img, x + w, y + h, x + w, y, c) + imageDrawLine(img, x + w, y + h, x, y + h, c) + } + + export function imageDrawCircle(img: Bitmap, cx: number, cy: number, r: number, col: number) { + cx = cx | 0; + cy = cy | 0; + r = r | 0; + // short cuts + if (r < 0) + return; + + // Bresenham's algorithm + let x = 0 + let y = r + let d = 3 - 2 * r + + while (y >= x) { + img.setPixel(cx + x, cy + y, col) + img.setPixel(cx - x, cy + y, col) + img.setPixel(cx + x, cy - y, col) + img.setPixel(cx - x, cy - y, col) + img.setPixel(cx + y, cy + x, col) + img.setPixel(cx - y, cy + x, col) + img.setPixel(cx + y, cy - x, col) + img.setPixel(cx - y, cy - x, col) + x++ + if (d > 0) { + y-- + d += 4 * (x - y) + 10 + } else { + d += 4 * x + 6 + } + } + } + + export function imageFillCircle(img: Bitmap, cx: number, cy: number, r: number, col: number) { + _fillCircle(img, pack(cx, cy), r, col); + } + + export function imageFillTriangle(img: Bitmap, x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, col: number) { + _blitArgs = _blitArgs || []; + _blitArgs[0] = x0; + _blitArgs[1] = y0; + _blitArgs[2] = x1; + _blitArgs[3] = y1; + _blitArgs[4] = x2; + _blitArgs[5] = y2; + _blitArgs[6] = col; + _fillTriangle(img, _blitArgs); + } + + export function imageFillPolygon4(img: Bitmap, x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, col: number) { + _blitArgs = _blitArgs || []; + _blitArgs[0] = x0; + _blitArgs[1] = y0; + _blitArgs[2] = x1; + _blitArgs[3] = y1; + _blitArgs[4] = x2; + _blitArgs[5] = y2; + _blitArgs[6] = x3; + _blitArgs[7] = y3; + _blitArgs[8] = col; + _fillPolygon4(img, _blitArgs); + } + + /** + * Returns an image rotated by 90, 180, 270 deg clockwise + */ + export function imageRotated(img: Bitmap, deg: number) { + if (deg == -90 || deg == 270) { + let r = img.transposed(); + r.flipY(); + return r; + } else if (deg == 180 || deg == -180) { + let r = img.clone(); + r.flipX(); + r.flipY(); + return r; + } else if (deg == 90) { + let r = img.transposed(); + r.flipX(); + return r; + } else { + return null; + } + } + + //% shim=pxt::setScreenBrightness + function _setScreenBrightness(brightness: number) { } + + export function setScreenBrightness(img: Bitmap, b: number) { + b = Math.clamp(10, 100, b | 0); + _helpers_workaround.brightness = b + _setScreenBrightness(_helpers_workaround.brightness) + } + + export function screenBrightness(img: Bitmap) { + return _helpers_workaround.brightness + } +} diff --git a/libs/bitmap/pxt.json b/libs/bitmap/pxt.json new file mode 100644 index 000000000..8e1648ca9 --- /dev/null +++ b/libs/bitmap/pxt.json @@ -0,0 +1,17 @@ +{ + "name": "bitmap", + "description": "Support for bitmaps (micro:bit V2 only).", + "dependencies": { + "core": "file:../core" + }, + "files": [ + "bitmap.cpp", + "bitmap.ts", + "shims.d.ts" + ], + "public": false, + "preferredEditor": "tsprj", + "disablesVariants": [ + "mbdal" + ] +} diff --git a/libs/bitmap/shims.d.ts b/libs/bitmap/shims.d.ts new file mode 100644 index 000000000..77ac3775a --- /dev/null +++ b/libs/bitmap/shims.d.ts @@ -0,0 +1,159 @@ +// Auto-generated. Do not edit. + + +declare interface Bitmap { + /** + * Get underlying buffer + */ + //% property shim=BitmapMethods::__buffer + __buffer: Buffer; + + /** + * Get the width of the bitmap + */ + //% property shim=BitmapMethods::width + width: int32; + + /** + * Get the height of the bitmap + */ + //% property shim=BitmapMethods::height + height: int32; + + /** + * True if the bitmap is monochromatic (black and white) + */ + //% property shim=BitmapMethods::isMono + isMono: boolean; + + /** + * Sets all pixels in the current bitmap from the other bitmap, which has to be of the same size and + * bpp. + */ + //% shim=BitmapMethods::copyFrom + copyFrom(from: Bitmap): void; + + /** + * Set pixel color + */ + //% shim=BitmapMethods::setPixel + setPixel(x: int32, y: int32, c: int32): void; + + /** + * Get a pixel color + */ + //% shim=BitmapMethods::getPixel + getPixel(x: int32, y: int32): int32; + + /** + * Fill entire bitmap with a given color + */ + //% shim=BitmapMethods::fill + fill(c: int32): void; + + /** + * Copy row(s) of pixel from bitmap to buffer (8 bit per pixel). + */ + //% shim=BitmapMethods::getRows + getRows(x: int32, dst: Buffer): void; + + /** + * Copy row(s) of pixel from buffer to bitmap. + */ + //% shim=BitmapMethods::setRows + setRows(x: int32, src: Buffer): void; + + /** + * Return a copy of the current bitmap + */ + //% shim=BitmapMethods::clone + clone(): Bitmap; + + /** + * Flips (mirrors) pixels horizontally in the current bitmap + */ + //% shim=BitmapMethods::flipX + flipX(): void; + + /** + * Flips (mirrors) pixels vertically in the current bitmap + */ + //% shim=BitmapMethods::flipY + flipY(): void; + + /** + * Returns a transposed bitmap (with X/Y swapped) + */ + //% shim=BitmapMethods::transposed + transposed(): Bitmap; + + /** + * Every pixel in bitmap is moved by (dx,dy) + */ + //% shim=BitmapMethods::scroll + scroll(dx: int32, dy: int32): void; + + /** + * Stretches the bitmap horizontally by 100% + */ + //% shim=BitmapMethods::doubledX + doubledX(): Bitmap; + + /** + * Stretches the bitmap vertically by 100% + */ + //% shim=BitmapMethods::doubledY + doubledY(): Bitmap; + + /** + * Replaces one color in an bitmap with another + */ + //% shim=BitmapMethods::replace + replace(from: int32, to: int32): void; + + /** + * Stretches the bitmap in both directions by 100% + */ + //% shim=BitmapMethods::doubled + doubled(): Bitmap; + + /** + * Draw given bitmap on the current bitmap + */ + //% shim=BitmapMethods::drawBitmap + drawBitmap(from: Bitmap, x: int32, y: int32): void; + + /** + * Draw given bitmap with transparent background on the current bitmap + */ + //% shim=BitmapMethods::drawTransparentBitmap + drawTransparentBitmap(from: Bitmap, x: int32, y: int32): void; + + /** + * Check if the current bitmap "collides" with another + */ + //% shim=BitmapMethods::overlapsWith + overlapsWith(other: Bitmap, x: int32, y: int32): boolean; +} +declare namespace bitmap { + + /** + * Create new bitmap with given content + */ + //% shim=bitmap::ofBuffer + function ofBuffer(buf: Buffer): Bitmap; + + /** + * Create new empty (transparent) bitmap + */ + //% shim=bitmap::create + function create(width: int32, height: int32): Bitmap; + + /** + * Double the size of an icon + */ + //% shim=bitmap::doubledIcon + function doubledIcon(icon: Buffer): Buffer; +} + +// Auto-generated. Do not edit. Really. diff --git a/libs/bitmap/sim/bitmap.ts b/libs/bitmap/sim/bitmap.ts new file mode 100644 index 000000000..072cc6585 --- /dev/null +++ b/libs/bitmap/sim/bitmap.ts @@ -0,0 +1,1135 @@ +namespace pxsim { + export class RefImage extends RefObject { + _width: number; + _height: number; + _bpp: number; + data: Uint8Array; + isStatic = true; + revision: number; + + constructor(w: number, h: number, bpp: number) { + super(); + this.revision = 0; + this.data = new Uint8Array(w * h) + this._width = w + this._height = h + this._bpp = bpp + } + + scan(mark: (path: string, v: any) => void) { } + gcKey() { return "Bitmap" } + gcSize() { return 4 + (this.data.length + 3 >> 3) } + gcIsStatic() { return this.isStatic } + + pix(x: number, y: number) { + return (x | 0) + (y | 0) * this._width + } + + inRange(x: number, y: number) { + return 0 <= (x | 0) && (x | 0) < this._width && + 0 <= (y | 0) && (y | 0) < this._height; + } + + color(c: number): number { + return c & 0xff + } + + clamp(x: number, y: number) { + x |= 0 + y |= 0 + + if (x < 0) x = 0 + else if (x >= this._width) + x = this._width - 1 + + if (y < 0) y = 0 + else if (y >= this._height) + y = this._height - 1 + + return [x, y] + } + + makeWritable() { + this.revision++; + this.isStatic = false + } + + toDebugString() { + return this._width + "x" + this._height + } + } +} + +namespace pxsim.BitmapMethods { + export function XX(x: number) { return (x << 16) >> 16 } + export function YY(x: number) { return x >> 16 } + + export function __buffer(img: RefImage): RefBuffer { + return new RefBuffer(img.data) // no clone for now + } + + export function width(img: RefImage) { return img._width } + + export function height(img: RefImage) { return img._height } + + export function isMono(img: RefImage) { return img._bpp == 1 } + + export function isStatic(img: RefImage) { return img.gcIsStatic() } + + export function revision(img: RefImage) { return img.revision } + + export function setPixel(img: RefImage, x: number, y: number, c: number) { + img.makeWritable() + if (img.inRange(x, y)) + img.data[img.pix(x, y)] = img.color(c) + } + + export function getPixel(img: RefImage, x: number, y: number) { + if (img.inRange(x, y)) + return img.data[img.pix(x, y)] + return 0 + } + + export function fill(img: RefImage, c: number) { + img.makeWritable() + img.data.fill(img.color(c)) + } + + export function fillRect(img: RefImage, x: number, y: number, w: number, h: number, c: number) { + if (w == 0 || h == 0 || x >= img._width || y >= img._height || x + w - 1 < 0 || y + h - 1 < 0) + return; + img.makeWritable() + let [x2, y2] = img.clamp(x + w - 1, y + h - 1); + [x, y] = img.clamp(x, y) + let p = img.pix(x, y) + w = x2 - x + 1 + h = y2 - y + 1 + let d = img._width - w + c = img.color(c) + while (h-- > 0) { + for (let i = 0; i < w; ++i) + img.data[p++] = c + p += d + } + } + + export function _fillRect(img: RefImage, xy: number, wh: number, c: number) { + fillRect(img, XX(xy), YY(xy), XX(wh), YY(wh), c) + } + + export function mapRect(img: RefImage, x: number, y: number, w: number, h: number, c: RefBuffer) { + if (c.data.length < 16) + return + img.makeWritable() + let [x2, y2] = img.clamp(x + w - 1, y + h - 1); + [x, y] = img.clamp(x, y) + let p = img.pix(x, y) + w = x2 - x + 1 + h = y2 - y + 1 + let d = img._width - w + + while (h-- > 0) { + for (let i = 0; i < w; ++i) { + img.data[p] = c.data[img.data[p]] + p++ + } + p += d + } + } + + export function _mapRect(img: RefImage, xy: number, wh: number, c: RefBuffer) { + mapRect(img, XX(xy), YY(xy), XX(wh), YY(wh), c) + } + + export function equals(img: RefImage, other: RefImage) { + if (!other || img._bpp != other._bpp || img._width != other._width || img._height != other._height) { + return false; + } + let imgData = img.data; + let otherData = other.data; + let len = imgData.length; + for (let i = 0; i < len; i++) { + if (imgData[i] != otherData[i]) { + return false; + } + } + return true; + } + + export function getRows(img: RefImage, x: number, dst: RefBuffer) { + x |= 0 + if (!img.inRange(x, 0)) + return + + let dp = 0 + let len = Math.min(dst.data.length, (img._width - x) * img._height) + let sp = x + let hh = 0 + while (len--) { + if (hh++ >= img._height) { + hh = 1 + sp = ++x + } + dst.data[dp++] = img.data[sp] + sp += img._width + } + } + + export function setRows(img: RefImage, x: number, src: RefBuffer) { + x |= 0 + if (!img.inRange(x, 0)) + return + + let sp = 0 + let len = Math.min(src.data.length, (img._width - x) * img._height) + let dp = x + let hh = 0 + while (len--) { + if (hh++ >= img._height) { + hh = 1 + dp = ++x + } + img.data[dp] = src.data[sp++] + dp += img._width + } + } + + export function clone(img: RefImage) { + let r = new RefImage(img._width, img._height, img._bpp) + r.data.set(img.data) + return r + } + + export function flipX(img: RefImage) { + img.makeWritable() + const w = img._width + const h = img._height + for (let i = 0; i < h; ++i) { + img.data.subarray(i * w, (i + 1) * w).reverse() + } + } + + + export function flipY(img: RefImage) { + img.makeWritable() + const w = img._width + const h = img._height + const d = img.data + for (let i = 0; i < w; ++i) { + let top = i + let bot = i + (h - 1) * w + while (top < bot) { + let c = d[top] + d[top] = d[bot] + d[bot] = c + top += w + bot -= w + } + } + } + + export function transposed(img: RefImage) { + const w = img._width + const h = img._height + const d = img.data + const r = new RefImage(h, w, img._bpp) + const n = r.data + let src = 0 + + for (let i = 0; i < h; ++i) { + let dst = i + for (let j = 0; j < w; ++j) { + n[dst] = d[src++] + dst += w + } + } + + return r + } + + export function copyFrom(img: RefImage, from: RefImage) { + if (img._width != from._width || img._height != from._height || + img._bpp != from._bpp) + return; + img.data.set(from.data) + } + + export function scroll(img: RefImage, dx: number, dy: number) { + img.makeWritable() + dx |= 0 + dy |= 0 + if (dx != 0) { + const img2 = clone(img) + img.data.fill(0) + drawTransparentImage(img, img2, dx, dy) + } else if (dy < 0) { + dy = -dy + if (dy < img._height) + img.data.copyWithin(0, dy * img._width) + else + dy = img._height + img.data.fill(0, (img._height - dy) * img._width) + } else if (dy > 0) { + if (dy < img._height) + img.data.copyWithin(dy * img._width, 0) + else + dy = img._height + img.data.fill(0, 0, dy * img._width) + } + // TODO implement dx + } + + export function replace(img: RefImage, from: number, to: number) { + to &= 0xf; + const d = img.data + for (let i = 0; i < d.length; ++i) + if (d[i] == from) d[i] = to + } + + export function doubledX(img: RefImage) { + const w = img._width + const h = img._height + const d = img.data + const r = new RefImage(w * 2, h, img._bpp) + const n = r.data + let dst = 0 + + for (let src = 0; src < d.length; ++src) { + let c = d[src] + n[dst++] = c + n[dst++] = c + } + + return r + } + + export function doubledY(img: RefImage) { + const w = img._width + const h = img._height + const d = img.data + const r = new RefImage(w, h * 2, img._bpp) + const n = r.data + + let src = 0 + let dst0 = 0 + let dst1 = w + for (let i = 0; i < h; ++i) { + for (let j = 0; j < w; ++j) { + let c = d[src++] + n[dst0++] = c + n[dst1++] = c + } + dst0 += w + dst1 += w + } + + return r + } + + + export function doubled(img: RefImage) { + return doubledX(doubledY(img)) + } + + function drawImageCore(img: RefImage, from: RefImage, x: number, y: number, clear: boolean, check: boolean) { + x |= 0 + y |= 0 + + const w = from._width + let h = from._height + const sh = img._height + const sw = img._width + + if (x + w <= 0) return false + if (x >= sw) return false + if (y + h <= 0) return false + if (y >= sh) return false + + if (clear) + fillRect(img, x, y, from._width, from._height, 0) + else if (!check) + img.makeWritable() + + const len = x < 0 ? Math.min(sw, w + x) : Math.min(sw - x, w) + const fdata = from.data + const tdata = img.data + + for (let p = 0; h--; y++ , p += w) { + if (0 <= y && y < sh) { + let dst = y * sw + let src = p + if (x < 0) + src += -x + else + dst += x + for (let i = 0; i < len; ++i) { + const v = fdata[src++] + if (v) { + if (check) { + if (tdata[dst]) + return true + } else { + tdata[dst] = v + } + } + dst++ + } + } + } + + return false + } + + export function drawImage(img: RefImage, from: RefImage, x: number, y: number) { + drawImageCore(img, from, x, y, true, false) + } + + export function drawTransparentImage(img: RefImage, from: RefImage, x: number, y: number) { + drawImageCore(img, from, x, y, false, false) + } + + export function overlapsWith(img: RefImage, other: RefImage, x: number, y: number) { + return drawImageCore(img, other, x, y, false, true) + } + + function drawLineLow(img: RefImage, x0: number, y0: number, x1: number, y1: number, c: number) { + let dx = x1 - x0; + let dy = y1 - y0; + let yi = img._width; + if (dy < 0) { + yi = -yi; + dy = -dy; + } + let D = 2 * dy - dx; + dx <<= 1; + dy <<= 1; + c = img.color(c); + let ptr = img.pix(x0, y0) + for (let x = x0; x <= x1; ++x) { + img.data[ptr] = c + if (D > 0) { + ptr += yi; + D -= dx; + } + D += dy; + ptr++; + } + } + + function drawLineHigh(img: RefImage, x0: number, y0: number, x1: number, y1: number, c: number) { + let dx = x1 - x0; + let dy = y1 - y0; + let xi = 1; + if (dx < 0) { + xi = -1; + dx = -dx; + } + let D = 2 * dx - dy; + dx <<= 1; + dy <<= 1; + c = img.color(c); + let ptr = img.pix(x0, y0); + for (let y = y0; y <= y1; ++y) { + img.data[ptr] = c; + if (D > 0) { + ptr += xi; + D -= dy; + } + D += dx; + ptr += img._width; + } + } + + export function _drawLine(img: RefImage, xy: number, wh: number, c: number) { + drawLine(img, XX(xy), YY(xy), XX(wh), YY(wh), c) + } + + export function drawLine(img: RefImage, x0: number, y0: number, x1: number, y1: number, c: number) { + x0 |= 0 + y0 |= 0 + x1 |= 0 + y1 |= 0 + + if (x1 < x0) { + drawLine(img, x1, y1, x0, y0, c); + return; + } + + let w = x1 - x0; + let h = y1 - y0; + + if (h == 0) { + if (w == 0) + setPixel(img, x0, y0, c); + else + fillRect(img, x0, y0, w + 1, 1, c); + return; + } + + if (w == 0) { + if (h > 0) + fillRect(img, x0, y0, 1, h + 1, c); + else + fillRect(img, x0, y1, 1, -h + 1, c); + return; + } + + if (x1 < 0 || x0 >= img._width) + return; + if (x0 < 0) { + y0 -= (h * x0 / w) | 0; + x0 = 0; + } + if (x1 >= img._width) { + let d = (img._width - 1) - x1; + y1 += (h * d / w) | 0; + x1 = img._width - 1 + } + + if (y0 < y1) { + if (y0 >= img._height || y1 < 0) + return; + if (y0 < 0) { + x0 -= (w * y0 / h) | 0; + y0 = 0; + } + if (y1 >= img._height) { + let d = (img._height - 1) - y1; + x1 += (w * d / h) | 0; + y1 = img._height + } + } else { + if (y1 >= img._height || y0 < 0) + return; + if (y1 < 0) { + x1 -= (w * y1 / h) | 0; + y1 = 0; + } + if (y0 >= img._height) { + let d = (img._height - 1) - y0; + x0 += (w * d / h) | 0; + y0 = img._height + } + } + + img.makeWritable() + + if (h < 0) { + h = -h; + if (h < w) + drawLineLow(img, x0, y0, x1, y1, c); + else + drawLineHigh(img, x1, y1, x0, y0, c); + } else { + if (h < w) + drawLineLow(img, x0, y0, x1, y1, c); + else + drawLineHigh(img, x0, y0, x1, y1, c); + } + } + + export function drawIcon(img: RefImage, icon: RefBuffer, x: number, y: number, color: number) { + const src: Uint8Array = icon.data + if (!bitmap.isValidImage(icon)) + return + if (src[1] != 1) + return // only mono + let width = bitmap.bufW(src) + let height = bitmap.bufH(src) + let byteH = bitmap.byteHeight(height, 1) + + x |= 0 + y |= 0 + const destHeight = img._height + const destWidth = img._width + + if (x + width <= 0) return + if (x >= destWidth) return + if (y + height <= 0) return + if (y >= destHeight) return + + img.makeWritable() + + let srcPointer = 8 + color = img.color(color) + const screen = img.data + + for (let i = 0; i < width; ++i) { + let destX = x + i + if (0 <= destX && destX < destWidth) { + let destIndex = destX + y * destWidth + let srcIndex = srcPointer + let destY = y + let destEnd = Math.min(destHeight, height + y) + if (y < 0) { + srcIndex += ((-y) >> 3) + destY += ((-y) >> 3) * 8 + destIndex += (destY - y) * destWidth + } + let mask = 0x01 + let srcByte = src[srcIndex++] + while (destY < destEnd) { + if (destY >= 0 && (srcByte & mask)) { + screen[destIndex] = color + } + mask <<= 1 + if (mask == 0x100) { + mask = 0x01 + srcByte = src[srcIndex++] + } + destIndex += destWidth + destY++ + } + } + srcPointer += byteH + } + } + + export function _drawIcon(img: RefImage, icon: RefBuffer, xy: number, color: number) { + drawIcon(img, icon, XX(xy), YY(xy), color) + } + + export function fillCircle(img: RefImage, cx: number, cy: number, r: number, c: number) { + let x = r - 1; + let y = 0; + let dx = 1; + let dy = 1; + let err = dx - (r << 1); + while (x >= y) { + fillRect(img, cx + x, cy - y, 1, 1 + (y << 1), c); + fillRect(img, cx + y, cy - x, 1, 1 + (x << 1), c); + fillRect(img, cx - x, cy - y, 1, 1 + (y << 1), c); + fillRect(img, cx - y, cy - x, 1, 1 + (x << 1), c); + if (err <= 0) { + y++; + err += dy; + dy += 2; + } + if (err > 0) { + x--; + dx += 2; + err += dx - (r << 1); + } + } + } + + export function _fillCircle(img: RefImage, cxy: number, r: number, c: number) { + fillCircle(img, XX(cxy), YY(cxy), r, c); + } + + interface LineGenState { + x: number; + y: number; + x0: number; + y0: number; + x1: number; + y1: number; + W: number; + H: number; + dx: number; + dy: number; + yi: number; + xi: number; + D: number; + nextFuncIndex: number; + } + interface ValueRange { + min: number; + max: number; + } + + function nextYRange_Low(x: number, line: LineGenState, yRange: ValueRange) { + while (line.x === x && line.x <= line.x1 && line.x < line.W) { + if (0 <= line.x) { + if (line.y < yRange.min) yRange.min = line.y; + if (line.y > yRange.max) yRange.max = line.y + } + if (line.D > 0) { + line.y += line.yi; + line.D -= line.dx; + } + line.D += line.dy; + ++line.x; + } + } + + function nextYRange_HighUp(x: number, line: LineGenState, yRange: ValueRange) { + while (line.x == x && line.y >= line.y1 && line.x < line.W) { + if (0 <= line.x) { + if (line.y < yRange.min) yRange.min = line.y; + if (line.y > yRange.max) yRange.max = line.y; + } + if (line.D > 0) { + line.x += line.xi; + line.D += line.dy; + } + line.D += line.dx; + --line.y; + } + } + + function nextYRange_HighDown(x: number, line: LineGenState, yRange: ValueRange) { + while (line.x == x && line.y <= line.y1 && line.x < line.W) { + if (0 <= line.x) { + if (line.y < yRange.min) yRange.min = line.y; + if (line.y > yRange.max) yRange.max = line.y; + } + if (line.D > 0) { + line.x += line.xi; + line.D -= line.dy; + } + line.D += line.dx; + ++line.y; + } + } + + function initYRangeGenerator(X0: number, Y0: number, X1: number, Y1: number): LineGenState { + const line: LineGenState = { + x: X0, + y: Y0, + x0: X0, + y0: Y0, + x1: X1, + y1: Y1, + W: 0, + H: 0, + dx: X1 - X0, + dy: Y1 - Y0, + yi: 0, + xi: 0, + D: 0, + nextFuncIndex: 0, + }; + + if ((line.dy < 0 ? -line.dy : line.dy) < line.dx) { + line.yi = 1; + if (line.dy < 0) { + line.yi = -1; + line.dy = -line.dy; + } + line.D = 2 * line.dy - line.dx; + line.dx = line.dx << 1; + line.dy = line.dy << 1; + + line.nextFuncIndex = 0; + return line; + } else { + line.xi = 1; + if (line.dy < 0) { + line.D = 2 * line.dx + line.dy; + line.dx = line.dx << 1; + line.dy = line.dy << 1; + + line.nextFuncIndex = 1; + return line; + } else { + line.D = 2 * line.dx - line.dy; + line.dx = line.dx << 1; + line.dy = line.dy << 1; + + line.nextFuncIndex = 2; + return line; + } + } + } + + export function fillTriangle(img: RefImage, x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, c: number) { + if (x1 < x0) { + [x1, x0] = [x0, x1]; + [y1, y0] = [y0, y1]; + } + if (x2 < x1) { + [x2, x1] = [x1, x2]; + [y2, y1] = [y1, y2]; + } + if (x1 < x0) { + [x1, x0] = [x0, x1]; + [y1, y0] = [y0, y1]; + } + + const lines: LineGenState[] = [ + initYRangeGenerator(x0, y0, x2, y2), + initYRangeGenerator(x0, y0, x1, y1), + initYRangeGenerator(x1, y1, x2, y2) + ]; + + lines[0].W = lines[1].W = lines[2].W = width(img); + lines[0].H = lines[1].H = lines[2].H = height(img); + + type FP_NEXT = (x: number, line: LineGenState, yRange: ValueRange) => void; + const nextFuncList: FP_NEXT[] = [ + nextYRange_Low, + nextYRange_HighUp, + nextYRange_HighDown + ]; + const fpNext0 = nextFuncList[lines[0].nextFuncIndex]; + const fpNext1 = nextFuncList[lines[1].nextFuncIndex]; + const fpNext2 = nextFuncList[lines[2].nextFuncIndex]; + + const yRange= { + min: lines[0].H, + max: -1 + }; + + for (let x = lines[1].x0; x <= lines[1].x1; x++) { + yRange.min = lines[0].H; yRange.max = -1; + fpNext0(x, lines[0], yRange); + fpNext1(x, lines[1], yRange); + fillRect(img, x, yRange.min, 1, yRange.max - yRange.min + 1, c); + } + + fpNext2(lines[2].x0, lines[2], yRange); + + for (let x = lines[2].x0 + 1; x <= lines[2].x1; x++) { + yRange.min = lines[0].H; yRange.max = -1; + fpNext0(x, lines[0], yRange); + fpNext2(x, lines[2], yRange); + fillRect(img, x, yRange.min, 1, yRange.max - yRange.min + 1, c); + } + } + + export function _fillTriangle(img: RefImage, args: RefCollection) { + fillTriangle( + img, + args.getAt(0) | 0, + args.getAt(1) | 0, + args.getAt(2) | 0, + args.getAt(3) | 0, + args.getAt(4) | 0, + args.getAt(5) | 0, + args.getAt(6) | 0, + ); + } + + export function fillPolygon4(img: RefImage, x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, c: number) { + const lines: LineGenState[]= [ + (x0 < x1) ? initYRangeGenerator(x0, y0, x1, y1) : initYRangeGenerator(x1, y1, x0, y0), + (x1 < x2) ? initYRangeGenerator(x1, y1, x2, y2) : initYRangeGenerator(x2, y2, x1, y1), + (x2 < x3) ? initYRangeGenerator(x2, y2, x3, y3) : initYRangeGenerator(x3, y3, x2, y2), + (x0 < x3) ? initYRangeGenerator(x0, y0, x3, y3) : initYRangeGenerator(x3, y3, x0, y0) + ]; + + lines[0].W = lines[1].W = lines[2].W = lines[3].W = width(img); + lines[0].H = lines[1].H = lines[2].H = lines[3].H = height(img); + + let minX = Math.min(Math.min(x0, x1), Math.min(x2, x3)); + let maxX = Math.min(Math.max(Math.max(x0, x1), Math.max(x2, x3)), lines[0].W - 1); + + type FP_NEXT = (x: number, line: LineGenState, yRange: ValueRange) => void; + const nextFuncList: FP_NEXT[] = [ + nextYRange_Low, + nextYRange_HighUp, + nextYRange_HighDown + ]; + + const fpNext0 = nextFuncList[lines[0].nextFuncIndex]; + const fpNext1 = nextFuncList[lines[1].nextFuncIndex]; + const fpNext2 = nextFuncList[lines[2].nextFuncIndex]; + const fpNext3 = nextFuncList[lines[3].nextFuncIndex]; + + const yRange: ValueRange = { + min: lines[0].H, + max: -1 + }; + + for (let x = minX; x <= maxX; x++) { + yRange.min = lines[0].H; yRange.max = -1; + fpNext0(x, lines[0], yRange); + fpNext1(x, lines[1], yRange); + fpNext2(x, lines[2], yRange); + fpNext3(x, lines[3], yRange); + fillRect(img, x,yRange.min, 1, yRange.max - yRange.min + 1, c); + } + } + + export function _fillPolygon4(img: RefImage, args: RefCollection) { + fillPolygon4( + img, + args.getAt(0) | 0, + args.getAt(1) | 0, + args.getAt(2) | 0, + args.getAt(3) | 0, + args.getAt(4) | 0, + args.getAt(5) | 0, + args.getAt(6) | 0, + args.getAt(7) | 0, + args.getAt(8) | 0, + ); + } + + export function _blitRow(img: RefImage, xy: number, from: RefImage, xh: number) { + blitRow(img, XX(xy), YY(xy), from, XX(xh), YY(xh)) + } + + export function blitRow(img: RefImage, x: number, y: number, from: RefImage, fromX: number, fromH: number) { + x |= 0 + y |= 0 + fromX |= 0 + fromH |= 0 + if (!img.inRange(x, 0) || !img.inRange(fromX, 0) || fromH <= 0) + return + let fy = 0 + let stepFY = ((from._width << 16) / fromH) | 0 + let endY = y + fromH + if (endY > img._height) + endY = img._height + if (y < 0) { + fy += -y * stepFY + y = 0 + } + while (y < endY) { + img.data[img.pix(x, y)] = from.data[from.pix(fromX, fy >> 16)] + y++ + fy += stepFY + } + } + + export function _blit(img: RefImage, src: RefImage, args: RefCollection): boolean { + return blit(img, src, args); + } + + export function blit(dst: RefImage, src: RefImage, args: RefCollection): boolean { + const xDst = args.getAt(0) as number; + const yDst = args.getAt(1) as number; + const wDst = args.getAt(2) as number; + const hDst = args.getAt(3) as number; + const xSrc = args.getAt(4) as number; + const ySrc = args.getAt(5) as number; + const wSrc = args.getAt(6) as number; + const hSrc = args.getAt(7) as number; + const transparent = args.getAt(8) as number; + const check = args.getAt(9) as number; + + const xSrcStep = ((wSrc << 16) / wDst) | 0; + const ySrcStep = ((hSrc << 16) / hDst) | 0; + + const xDstClip = Math.abs(Math.min(0, xDst)); + const yDstClip = Math.abs(Math.min(0, yDst)); + const xDstStart = xDst + xDstClip; + const yDstStart = yDst + yDstClip; + const xDstEnd = Math.min(dst._width, xDst + wDst); + const yDstEnd = Math.min(dst._height, yDst + hDst); + + const xSrcStart = Math.max(0, (xSrc << 16) + xDstClip * xSrcStep); + const ySrcStart = Math.max(0, (ySrc << 16) + yDstClip * ySrcStep); + const xSrcEnd = Math.min(src._width, xSrc + wSrc) << 16; + const ySrcEnd = Math.min(src._height, ySrc + hSrc) << 16; + + if (!check) + dst.makeWritable(); + + for (let yDstCur = yDstStart, ySrcCur = ySrcStart; yDstCur < yDstEnd && ySrcCur < ySrcEnd; ++yDstCur, ySrcCur += ySrcStep) { + const ySrcCurI = ySrcCur >> 16; + for (let xDstCur = xDstStart, xSrcCur = xSrcStart; xDstCur < xDstEnd && xSrcCur < xSrcEnd; ++xDstCur, xSrcCur += xSrcStep) { + const xSrcCurI = xSrcCur >> 16; + const cSrc = getPixel(src, xSrcCurI, ySrcCurI); + if (check && cSrc) { + const cDst = getPixel(dst, xDstCur, yDstCur); + if (cDst) { + return true; + } + continue; + } + if (!transparent || cSrc) { + setPixel(dst, xDstCur, yDstCur, cSrc); + } + } + } + return false; + } +} + + +namespace pxsim.bitmap { + export function byteHeight(h: number, bpp: number) { + if (bpp == 1) + return h * bpp + 7 >> 3 + else + return ((h * bpp + 31) >> 5) << 2 + } + + function isLegacyImage(buf: RefBuffer) { + if (!buf || buf.data.length < 5) + return false; + + if (buf.data[0] != 0xe1 && buf.data[0] != 0xe4) + return false; + + const bpp = buf.data[0] & 0xf; + const sz = buf.data[1] * byteHeight(buf.data[2], bpp) + if (4 + sz != buf.data.length) + return false; + + return true; + } + + export function bufW(data: Uint8Array) { + return data[2] | (data[3] << 8) + } + + export function bufH(data: Uint8Array) { + return data[4] | (data[5] << 8) + } + + export function isValidImage(buf: RefBuffer) { + if (!buf || buf.data.length < 5) + return false; + + if (buf.data[0] != 0x87) + return false + + if (buf.data[1] != 1 && buf.data[1] != 4) + return false; + + const bpp = buf.data[1]; + const sz = bufW(buf.data) * byteHeight(bufH(buf.data), bpp) + if (8 + sz != buf.data.length) + return false; + + return true; + } + + + export function create(w: number, h: number) { + // truncate decimal sizes + w |= 0 + h |= 0 + return new RefImage(w, h, 4) + } + + // TODO: move to image namespace + export function ofBuffer(buf: RefBuffer): RefImage { + const src: Uint8Array = buf.data + + let srcP = 4 + let w = 0, h = 0, bpp = 0 + + if (isLegacyImage(buf)) { + w = src[1] + h = src[2] + bpp = src[0] & 0xf; + // console.log("using legacy image") + } else if (isValidImage(buf)) { + srcP = 8 + w = bufW(src) + h = bufH(src) + bpp = src[1] + } + + if (w == 0 || h == 0) + return null + const r = new RefImage(w, h, bpp) + const dst = r.data + + r.isStatic = buf.isStatic + + if (bpp == 1) { + for (let i = 0; i < w; ++i) { + let dstP = i + let mask = 0x01 + let v = src[srcP++] + for (let j = 0; j < h; ++j) { + if (mask == 0x100) { + mask = 0x01 + v = src[srcP++] + } + if (v & mask) + dst[dstP] = 1 + dstP += w + mask <<= 1 + } + } + } else if (bpp == 4) { + for (let i = 0; i < w; ++i) { + let dstP = i + for (let j = 0; j < h >> 1; ++j) { + const v = src[srcP++] + dst[dstP] = v & 0xf + dstP += w + dst[dstP] = v >> 4 + dstP += w + } + if (h & 1) + dst[dstP] = src[srcP++] & 0xf + srcP = (srcP + 3) & ~3 + } + } + + return r + } + + export function toBuffer(img: RefImage): RefBuffer { + let col = byteHeight(img._height, img._bpp) + let sz = 8 + img._width * col + let r = new Uint8Array(sz) + r[0] = 0x87 + r[1] = img._bpp + r[2] = img._width & 0xff + r[3] = img._width >> 8 + r[4] = img._height & 0xff + r[5] = img._height >> 8 + let dstP = 8 + const w = img._width + const h = img._height + const data = img.data + for (let i = 0; i < w; ++i) { + if (img._bpp == 4) { + let p = i + for (let j = 0; j < h; j += 2) { + r[dstP++] = ((data[p + w] & 0xf) << 4) | ((data[p] || 0) & 0xf) + p += 2 * w + } + dstP = (dstP + 3) & ~3 + } else if (img._bpp == 1) { + let mask = 0x01 + let p = i + for (let j = 0; j < h; j++) { + if (data[p]) + r[dstP] |= mask + mask <<= 1 + p += w + if (mask == 0x100) { + mask = 0x01 + dstP++ + } + } + if (mask != 0x01) + dstP++ + } + } + + return new RefBuffer(r) + } + + export function doubledIcon(buf: RefBuffer): RefBuffer { + let img = ofBuffer(buf) + if (!img) + return null + img = BitmapMethods.doubled(img) + return toBuffer(img) + } +} + +namespace pxsim.pxtcore { + export function updateScreen(img: RefImage) { + + } + export function updateStats(s: string) { + + } + export function setPalette(b: RefBuffer) { + + } + export function setScreenBrightness(b: number) { + + } + export function displayHeight(): number { + return 120 + } + export function displayWidth(): number { + return 160 + } + export function displayPresent(): boolean { + return true + } +}