diff --git a/include/mgba-util/image.h b/include/mgba-util/image.h index 31514687d..b86b2135a 100644 --- a/include/mgba-util/image.h +++ b/include/mgba-util/image.h @@ -138,6 +138,7 @@ void mPainterInit(struct mPainter*, struct mImage* backing); void mPainterDrawRectangle(struct mPainter*, int x, int y, int width, int height); void mPainterDrawLine(struct mPainter*, int x1, int y1, int x2, int y2); void mPainterDrawCircle(struct mPainter*, int x, int y, int diameter); +void mPainterDrawMask(struct mPainter*, const struct mImage* mask, int x, int y); uint32_t mColorConvert(uint32_t color, enum mColorFormat from, enum mColorFormat to); uint32_t mImageColorConvert(uint32_t color, const struct mImage* from, enum mColorFormat to); diff --git a/res/scripts/color-mask.lua b/res/scripts/color-mask.lua new file mode 100644 index 000000000..25a51f805 --- /dev/null +++ b/res/scripts/color-mask.lua @@ -0,0 +1,34 @@ +local state = {} +state.wheel = image.load(script.dir .. "/wheel.png") +state.overlay = canvas:newLayer(state.wheel.width, state.wheel.height) +state.painter = image.newPainter(state.overlay.image) +state.phase = 0 +state.speed = 0.01 +state.painter:setFill(true) +state.painter:setStrokeWidth(0) + +function state.update() + local r = math.fmod(state.phase * 3, math.pi * 2) + local g = math.fmod(state.phase * 5, math.pi * 2) + local b = math.fmod(state.phase * 7, math.pi * 2) + local color = 0xFF000000 + color = color | math.floor((math.sin(r) + 1) * 127.5) << 16 + color = color | math.floor((math.sin(g) + 1) * 127.5) << 8 + color = color | math.floor((math.sin(b) + 1) * 127.5) + + -- Clear image + state.painter:setBlend(false) + state.painter:setFillColor(0) + state.painter:drawRectangle(0, 0, state.wheel.width, state.wheel.height) + -- Draw mask + state.painter:setBlend(true) + state.painter:setFillColor(color | 0xFF000000) + state.painter:drawMask(state.wheel, 0, 0) + + state.overlay:update() + state.phase = math.fmod(state.phase + state.speed, math.pi * 2 * 3 * 5 * 7) +end + +callbacks:add("frame", state.update) + + diff --git a/res/scripts/logo-bg.png b/res/scripts/logo-bg.png new file mode 100644 index 000000000..908e410d9 Binary files /dev/null and b/res/scripts/logo-bg.png differ diff --git a/res/scripts/logo-bounce.lua b/res/scripts/logo-bounce.lua index c8192638c..aae32ce40 100644 --- a/res/scripts/logo-bounce.lua +++ b/res/scripts/logo-bounce.lua @@ -1,44 +1,61 @@ math.randomseed(os.time()) local state = {} -state.logo = image.load(script.dir .. "/logo.png") -state.overlay = canvas:newLayer(state.logo.width, state.logo.height) -state.overlay.image:drawImageOpaque(state.logo, 0, 0) -state.x = math.random() * (canvas:screenWidth() - state.logo.width) -state.y = math.random() * (canvas:screenHeight() - state.logo.height) +state.logo_fg = image.load(script.dir .. "/logo-fg.png") +state.logo_bg = image.load(script.dir .. "/logo-bg.png") +state.overlay = canvas:newLayer(state.logo_fg.width, state.logo_fg.height) +state.x = math.random() * (canvas:screenWidth() - state.logo_fg.width) +state.y = math.random() * (canvas:screenHeight() - state.logo_fg.height) +state.overlay:setPosition(math.floor(state.x), math.floor(state.y)) state.direction = math.floor(math.random() * 3) state.speed = 0.5 -state.overlay:setPosition(math.floor(state.x), math.floor(state.y)) -state.overlay:update() +function state.recolor() + local r = math.floor(math.random() * 255) + local g = math.floor(math.random() * 255) + local b = math.floor(math.random() * 255) + local color = 0xFF000000 | (r << 16) | (g << 8) | b + state.overlay.image:drawImageOpaque(state.logo_bg, 0, 0) + local painter = image.newPainter(state.overlay.image) + painter:setFill(true) + painter:setFillColor(color) + painter:setBlend(true) + painter:drawMask(state.logo_fg, 0, 0) + state.overlay:update() +end function state.update() if state.direction & 1 == 1 then state.x = state.x + 1 - if state.x > canvas:screenWidth() - state.logo.width then - state.x = (canvas:screenWidth() - state.logo.width) * 2 - state.x + if state.x > canvas:screenWidth() - state.logo_fg.width then + state.x = (canvas:screenWidth() - state.logo_fg.width) * 2 - state.x state.direction = state.direction ~ 1 + state.recolor() end else state.x = state.x - 1 if state.x < 0 then state.x = -state.x state.direction = state.direction ~ 1 + state.recolor() end end if state.direction & 2 == 2 then state.y = state.y + 1 - if state.y > canvas:screenHeight() - state.logo.height then - state.y = (canvas:screenHeight() - state.logo.height) * 2 - state.y + if state.y > canvas:screenHeight() - state.logo_fg.height then + state.y = (canvas:screenHeight() - state.logo_fg.height) * 2 - state.y state.direction = state.direction ~ 2 + state.recolor() end else state.y = state.y - 1 if state.y < 0 then state.y = -state.y state.direction = state.direction ~ 2 + state.recolor() end end state.overlay:setPosition(math.floor(state.x), math.floor(state.y)) end +state.recolor() callbacks:add("frame", state.update) diff --git a/res/scripts/logo-fg.png b/res/scripts/logo-fg.png new file mode 100644 index 000000000..c401fb011 Binary files /dev/null and b/res/scripts/logo-fg.png differ diff --git a/res/scripts/wheel.png b/res/scripts/wheel.png new file mode 100644 index 000000000..5e7bcca12 Binary files /dev/null and b/res/scripts/wheel.png differ diff --git a/src/script/image.c b/src/script/image.c index f056c7d49..7705ebd9a 100644 --- a/src/script/image.c +++ b/src/script/image.c @@ -141,6 +141,7 @@ mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mPainter, setStrokeColor, _mPainterSetStrokeC mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mPainter, drawRectangle, mPainterDrawRectangle, 4, S32, x, S32, y, S32, width, S32, height); mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mPainter, drawLine, mPainterDrawLine, 4, S32, x1, S32, y1, S32, x2, S32, y2); mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mPainter, drawCircle, mPainterDrawCircle, 3, S32, x, S32, y, S32, diameter); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mPainter, drawMask, mPainterDrawMask, 3, CS(mImage), mask, S32, x, S32, y); mSCRIPT_DEFINE_STRUCT(mPainter) mSCRIPT_DEFINE_CLASS_DOCSTRING( @@ -162,6 +163,13 @@ mSCRIPT_DEFINE_STRUCT(mPainter) mSCRIPT_DEFINE_STRUCT_METHOD(mPainter, drawLine) mSCRIPT_DEFINE_DOCSTRING("Draw a circle with the specified diameter with the given origin at the top-left corner of the bounding box") mSCRIPT_DEFINE_STRUCT_METHOD(mPainter, drawCircle) + mSCRIPT_DEFINE_DOCSTRING( + "Draw a mask image with each color channel multiplied by the current fill color. This can " + "be useful for displaying graphics with dynamic colors. By making a grayscale template " + "image on a transparent background in advance, a script can set the fill color to a desired " + "target color and use this function to draw it into a destination image." + ) + mSCRIPT_DEFINE_STRUCT_METHOD(mPainter, drawMask) mSCRIPT_DEFINE_END; mSCRIPT_DECLARE_STRUCT_METHOD(mScriptPainter, W(mPainter), _get, _mScriptPainterGet, 1, CHARP, name); diff --git a/src/util/image.c b/src/util/image.c index a0fefafe7..f1f1a0fef 100644 --- a/src/util/image.c +++ b/src/util/image.c @@ -42,6 +42,53 @@ memcpy((void*) (DST), &_color, (DEPTH)); \ } while (0); +static uint32_t _mColorMultiply(uint32_t colorA, uint32_t colorB) { + uint32_t color = 0; + + uint32_t a, b; + a = colorA & 0xFF; + b = colorB & 0xFF; + b = a * b; + b /= 0xFF; + if (b > 0xFF) { + color |= 0xFF; + } else { + color |= b; + } + + a = (colorA >> 8) & 0xFF; + b = (colorB >> 8) & 0xFF; + b = a * b; + b /= 0xFF; + if (b > 0xFF) { + color |= 0xFF00; + } else { + color |= b << 8; + } + + a = (colorA >> 16) & 0xFF; + b = (colorB >> 16) & 0xFF; + b = a * b; + b /= 0xFF; + if (b > 0xFF) { + color |= 0xFF0000; + } else { + color |= b << 16; + } + + a = (colorA >> 24) & 0xFF; + b = (colorB >> 24) & 0xFF; + b = a * b; + b /= 0xFF; + if (b > 0xFF) { + color |= 0xFF000000; + } else { + color |= b << 24; + } + + return color; +} + struct mImage* mImageCreate(unsigned width, unsigned height, enum mColorFormat format) { return mImageCreateWithStride(width, height, width, format); } @@ -395,18 +442,18 @@ void mImageSetPaletteEntry(struct mImage* image, unsigned index, uint32_t color) image->palette[index] = color; } -#define COMPOSITE_BOUNDS_INIT \ +#define COMPOSITE_BOUNDS_INIT(SOURCE, DEST) \ struct mRectangle dstRect = { \ .x = 0, \ .y = 0, \ - .width = image->width, \ - .height = image->height \ + .width = (DEST)->width, \ + .height = (DEST)->height \ }; \ struct mRectangle srcRect = { \ .x = x, \ .y = y, \ - .width = source->width, \ - .height = source->height \ + .width = (SOURCE)->width, \ + .height = (SOURCE)->height \ }; \ if (!mRectangleIntersection(&srcRect, &dstRect)) { \ return; \ @@ -436,7 +483,7 @@ void mImageBlit(struct mImage* image, const struct mImage* source, int x, int y) return; } - COMPOSITE_BOUNDS_INIT; + COMPOSITE_BOUNDS_INIT(source, image); for (y = 0; y < srcRect.height; ++y) { uintptr_t srcPixel = (uintptr_t) PIXEL(source, srcStartX, srcStartY + y); @@ -461,7 +508,7 @@ void mImageComposite(struct mImage* image, const struct mImage* source, int x, i return; } - COMPOSITE_BOUNDS_INIT; + COMPOSITE_BOUNDS_INIT(source, image); for (y = 0; y < srcRect.height; ++y) { uintptr_t srcPixel = (uintptr_t) PIXEL(source, srcStartX, srcStartY + y); @@ -498,7 +545,7 @@ void mImageCompositeWithAlpha(struct mImage* image, const struct mImage* source, alpha = 256; } - COMPOSITE_BOUNDS_INIT; + COMPOSITE_BOUNDS_INIT(source, image); int fixedAlpha = alpha * 0x200; @@ -821,6 +868,33 @@ void mPainterDrawCircle(struct mPainter* painter, int x, int y, int diameter) { } } +void mPainterDrawMask(struct mPainter* painter, const struct mImage* mask, int x, int y) { + if (!painter->fill) { + return; + } + + COMPOSITE_BOUNDS_INIT(mask, painter->backing); + + for (y = 0; y < srcRect.height; ++y) { + uintptr_t dstPixel = (uintptr_t) PIXEL(painter->backing, dstStartX, dstStartY + y); + uintptr_t maskPixel = (uintptr_t) PIXEL(mask, srcStartX, srcStartY + y); + for (x = 0; x < srcRect.width; ++x, dstPixel += painter->backing->depth, maskPixel += mask->depth) { + uint32_t color; + GET_PIXEL(color, maskPixel, mask->depth); + color = mColorConvert(color, mask->format, mCOLOR_ARGB8); + color = _mColorMultiply(painter->fillColor, color); + if (painter->blend || painter->fillColor < 0xFF000000) { + uint32_t current; + GET_PIXEL(current, dstPixel, painter->backing->depth); + current = mColorConvert(current, painter->backing->format, mCOLOR_ARGB8); + color = mColorMixARGB8(color, current); + } + color = mColorConvert(color, mCOLOR_ARGB8, painter->backing->format); + PUT_PIXEL(color, dstPixel, painter->backing->depth); + } + } +} + uint32_t mColorConvert(uint32_t color, enum mColorFormat from, enum mColorFormat to) { if (from == to) { return color; diff --git a/src/util/test/image.c b/src/util/test/image.c index a27e53d5c..e3ea16e24 100644 --- a/src/util/test/image.c +++ b/src/util/test/image.c @@ -2038,6 +2038,97 @@ M_TEST_DEFINE(painterDrawCircleInvalid) { mImageDestroy(image); } +M_TEST_DEFINE(painterDrawMask) { + struct mImage* image; + struct mImage* mask; + struct mPainter painter; + + image = mImageCreate(4, 4, mCOLOR_XRGB8); + mPainterInit(&painter, image); + painter.blend = false; + painter.fill = true; + + mask = mImageCreate(2, 2, mCOLOR_XRGB8); + mImageSetPixel(mask, 0, 0, 0xFFFFFFFF); + mImageSetPixel(mask, 1, 0, 0xFFFF0000); + mImageSetPixel(mask, 0, 1, 0xFF00FF00); + mImageSetPixel(mask, 1, 1, 0xFF0000FF); + + painter.fillColor = 0xFFFFFFFF; + mPainterDrawMask(&painter, mask, 0, 0); + painter.fillColor = 0xFFFF0000; + mPainterDrawMask(&painter, mask, 2, 0); + painter.fillColor = 0xFF00FF00; + mPainterDrawMask(&painter, mask, 0, 2); + painter.fillColor = 0xFF0000FF; + mPainterDrawMask(&painter, mask, 2, 2); + + COMPARE4X(0xFFFFFF, 0xFF0000, 0xFF0000, 0xFF0000, + 0x00FF00, 0x0000FF, 0x000000, 0x000000, + 0x00FF00, 0x000000, 0x0000FF, 0x000000, + 0x00FF00, 0x000000, 0x000000, 0x0000FF); + + painter.fillColor = 0xFF808080; + mPainterDrawMask(&painter, mask, 0, 0); + painter.fillColor = 0xFFFFFF00; + mPainterDrawMask(&painter, mask, 2, 0); + painter.fillColor = 0xFF00FFFF; + mPainterDrawMask(&painter, mask, 0, 2); + painter.fillColor = 0xFFFF00FF; + mPainterDrawMask(&painter, mask, 2, 2); + + COMPARE4X(0x808080, 0x800000, 0xFFFF00, 0xFF0000, + 0x008000, 0x000080, 0x00FF00, 0x000000, + 0x00FFFF, 0x000000, 0xFF00FF, 0xFF0000, + 0x00FF00, 0x0000FF, 0x000000, 0x0000FF); + + painter.fillColor = 0xFFFFFFFF; + mPainterDrawMask(&painter, mask, -1, -1); + mPainterDrawMask(&painter, mask, 3, 3); + assert_int_equal(0xFF0000FF, mImageGetPixel(image, 0, 0)); + assert_int_equal(0xFFFFFFFF, mImageGetPixel(image, 3, 3)); + + mImageDestroy(image); + mImageDestroy(mask); +} + +M_TEST_DEFINE(painterDrawMaskBlend) { + struct mImage* image; + struct mImage* mask; + struct mPainter painter; + const uint8_t lut[4] = { 0x00, 0x55, 0xAA, 0xFF }; + int x, y; + + image = mImageCreate(4, 4, mCOLOR_XRGB8); + mPainterInit(&painter, image); + painter.blend = true; + painter.fill = true; + painter.fillColor = 0xFFFF8000; + + for (y = 0; y < 4; ++y) { + for (x = 0; x < 4; ++x) { + mImageSetPixel(image, x, y, 0xFF808080); + } + } + + mask = mImageCreate(4, 4, mCOLOR_ARGB8); + for (y = 0; y < 4; ++y) { + for (x = 0; x < 4; ++x) { + mImageSetPixel(mask, x, y, (lut[x] << 24) | (lut[y] * 0x010101)); + } + } + + mPainterDrawMask(&painter, mask, 0, 0); + + COMPARE4X(0x808080, 0x555555, 0x2A2A2A, 0x000000, + 0x808080, 0x716355, 0x63462A, 0x552A00, + 0x808080, 0x8E7155, 0x9C632A, 0xAA5500, + 0x808080, 0xAA8055, 0xD4802A, 0xFF8000); + + mImageDestroy(image); + mImageDestroy(mask); +} + #undef COMPARE3X #undef COMPARE3 #undef COMPARE4X @@ -2083,4 +2174,6 @@ M_TEST_SUITE_DEFINE(Image, cmocka_unit_test(painterDrawCircleOffset), cmocka_unit_test(painterDrawCircleBlend), cmocka_unit_test(painterDrawCircleInvalid), + cmocka_unit_test(painterDrawMask), + cmocka_unit_test(painterDrawMaskBlend), )