Scripting: Export new image masking function

This commit is contained in:
Vicki Pfau 2024-03-18 23:17:42 -07:00
parent 4fdadc585d
commit 776d52a2c6
9 changed files with 246 additions and 19 deletions

View File

@ -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);

View File

@ -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)

BIN
res/scripts/logo-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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)

BIN
res/scripts/logo-fg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

BIN
res/scripts/wheel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -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);

View File

@ -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;

View File

@ -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),
)