Compose groups separately from all other layers

Maintain hierarchical structure of sprite groups instead of flattening.
Allows opacity and blend mode to be applied correctly to groups.
Sets the foundation for future features like mask layers.

Note:
Requires full image rendering and impacts performance in some scenarios.
Avoids complex code changes for minor performance gains.

Co-authored-by: Guilherme Marcondes <guilherme.marcondes@tecnico.ulisboa.pt>
This commit is contained in:
Guilherme Belchior 2024-06-03 23:00:04 +01:00 committed by David Capello
parent 6855c1368d
commit ffc3684b1b
23 changed files with 389 additions and 61 deletions

View File

@ -236,6 +236,7 @@
<option id="multiple_windows" type="bool" default="false" />
<option id="new_render_engine" type="bool" default="true" />
<option id="new_blend" type="bool" default="true" />
<option id="compose_groups" type="bool" default="false" />
<option id="use_native_clipboard" type="bool" default="true" />
<option id="use_native_file_dialog" type="bool" default="true" />
<option id="use_shaders_for_color_selectors" type="bool" default="true" />

View File

@ -1498,6 +1498,7 @@ color_quantization = Color Quantization
performance = Performance
multiple_windows = UI with multiple windows
new_blend = New layer blending method
compose_groups = Compose groups separately
new_render_engine = New render engine for sprite editor
native_clipboard = Use native clipboard
native_file_dialog = Use native file dialog

View File

@ -574,6 +574,11 @@
pref="experimental.new_blend" />
<link text="(#1096)" url="https://github.com/aseprite/aseprite/issues/1096" />
</hbox>
<hbox>
<check text="@.compose_groups"
pref="experimental.compose_groups" />
<link text="(#3225)" url="https://github.com/aseprite/aseprite/issues/3225" />
</hbox>
<check id="native_clipboard" text="@.native_clipboard"
pref="experimental.use_native_clipboard" />
<check id="native_file_dialog" text="@.native_file_dialog"

View File

@ -260,9 +260,9 @@ private:
if ((count > 1) ||
(count == 1 && m_layer && (newName != m_layer->name() ||
newUserData != m_layer->userData() ||
(m_layer->isImage() &&
(newOpacity != static_cast<LayerImage*>(m_layer)->opacity() ||
newBlendMode != static_cast<LayerImage*>(m_layer)->blendMode()))))) {
(m_layer->isImage() || (m_layer->isGroup() && Preferences::instance().experimental.composeGroups())) ||
(newOpacity != m_layer->opacity() ||
newBlendMode != m_layer->blendMode())))) {
try {
ContextWriter writer(UIContext::instance());
Tx tx(writer, "Set Layer Properties");
@ -275,10 +275,13 @@ private:
range.endRange(m_layer, -1);
}
const bool shouldChangeProperties = m_layer->isImage() ||
(m_layer->isGroup() && Preferences::instance().experimental.composeGroups());
const bool nameChanged = (newName != m_layer->name());
const bool userDataChanged = (newUserData != m_layer->userData());
const bool opacityChanged = (m_layer->isImage() && newOpacity != static_cast<LayerImage*>(m_layer)->opacity());
const bool blendModeChanged = (m_layer->isImage() && newBlendMode != static_cast<LayerImage*>(m_layer)->blendMode());
const bool opacityChanged = (shouldChangeProperties && newOpacity != m_layer->opacity());
const bool blendModeChanged = (shouldChangeProperties && newBlendMode != m_layer->blendMode());
for (Layer* layer : range.selectedLayers()) {
if (nameChanged && newName != layer->name())
@ -287,7 +290,7 @@ private:
if (userDataChanged && newUserData != layer->userData())
tx(new cmd::SetUserData(layer, newUserData, m_document));
if (layer->isImage()) {
if (layer->isImage() || (layer->isGroup() && Preferences::instance().experimental.composeGroups())) {
if (opacityChanged && newOpacity != static_cast<LayerImage*>(layer)->opacity())
tx(new cmd::SetLayerOpacity(static_cast<LayerImage*>(layer), newOpacity));
@ -430,21 +433,20 @@ private:
name()->setText(m_layer->name().c_str());
name()->setEnabled(true);
if (m_layer->isImage()) {
if (m_layer->isImage() || (m_layer->isGroup() && Preferences::instance().experimental.composeGroups())) {
mode()->setSelectedItem(nullptr);
for (auto item : *mode()) {
if (auto blendModeItem = dynamic_cast<BlendModeItem*>(item)) {
if (blendModeItem->mode() == static_cast<LayerImage*>(m_layer)->blendMode()) {
if (blendModeItem->mode() == m_layer->blendMode()) {
mode()->setSelectedItem(item);
break;
}
}
}
mode()->setEnabled(!m_layer->isBackground());
opacity()->setValue(static_cast<LayerImage*>(m_layer)->opacity());
opacity()->setValue(m_layer->opacity());
opacity()->setEnabled(!m_layer->isBackground());
}
else {
} else {
mode()->setEnabled(false);
opacity()->setEnabled(false);
}

View File

@ -42,6 +42,11 @@ namespace app {
// True if renderSprite() composite an unpremultiplied RGBA
// surface when we draw on a transparent background.
bool outputsUnpremultiplied = false;
// True if the renderer can compose groups of layers in a
// layer image, in other case the renderer should render each
// layer separately merging one by one.
bool composeGroups = false;
};
virtual ~Renderer() { }
@ -56,6 +61,7 @@ namespace app {
virtual void setRefLayersVisiblity(const bool visible) = 0;
virtual void setNonactiveLayersOpacity(const int opacity) = 0;
virtual void setNewBlendMethod(const bool newBlend) = 0;
virtual void setComposeGroups(bool composeGroups) = 0;
virtual void setBgOptions(const render::BgOptions& bg) = 0;
virtual void setProjection(const render::Projection& projection) = 0;

View File

@ -110,6 +110,11 @@ void ShaderRenderer::setNewBlendMethod(const bool newBlend)
// TODO impl
}
void ShaderRenderer::setComposeGroups(const bool m_composeGroups)
{
// TODO impl
}
void ShaderRenderer::setBgOptions(const render::BgOptions& bg)
{
m_bgOptions = bg;

View File

@ -39,6 +39,7 @@ namespace app {
void setRefLayersVisiblity(const bool visible) override;
void setNonactiveLayersOpacity(const int opacity) override;
void setNewBlendMethod(const bool newBlend) override;
void setComposeGroups(const bool composeGroups) override;
void setBgOptions(const render::BgOptions& bg) override;
void setProjection(const render::Projection& projection) override;

View File

@ -37,6 +37,11 @@ void SimpleRenderer::setNewBlendMethod(const bool newBlend)
m_render.setNewBlend(newBlend);
}
void SimpleRenderer::setComposeGroups(const bool composeGroups)
{
m_render.setComposeGroups(composeGroups);
}
void SimpleRenderer::setBgOptions(const render::BgOptions& bg)
{
m_render.setBgOptions(bg);

View File

@ -24,6 +24,7 @@ namespace app {
void setRefLayersVisiblity(const bool visible) override;
void setNonactiveLayersOpacity(const int opacity) override;
void setNewBlendMethod(const bool newBlend) override;
void setComposeGroups(bool composeGroups) override;
void setBgOptions(const render::BgOptions& bg) override;
void setProjection(const render::Projection& projection) override;

View File

@ -107,6 +107,7 @@ void render_sprite(Image* dst,
{
render::Render render;
render.setNewBlend(true);
render.setComposeGroups(Preferences::instance().experimental.composeGroups());
render.renderSprite(
dst, sprite, frame,
gfx::Clip(x, y,

View File

@ -20,6 +20,7 @@
#include "app/script/engine.h"
#include "app/script/luacpp.h"
#include "app/script/userdata.h"
#include "app/pref/preferences.h"
#include "app/tx.h"
#include "doc/layer.h"
#include "doc/layer_tilemap.h"
@ -130,7 +131,7 @@ int Layer_get_name(lua_State* L)
int Layer_get_opacity(lua_State* L)
{
auto layer = get_docobj<Layer>(L, 1);
if (layer->isImage()) {
if (layer->isImage() || (layer->isGroup() && app::Preferences::instance().experimental.composeGroups())) {
lua_pushinteger(L, static_cast<LayerImage*>(layer)->opacity());
return 1;
}
@ -141,7 +142,7 @@ int Layer_get_opacity(lua_State* L)
int Layer_get_blendMode(lua_State* L)
{
auto layer = get_docobj<Layer>(L, 1);
if (layer->isImage()) {
if (layer->isImage() || (layer->isGroup() && app::Preferences::instance().experimental.composeGroups())) {
lua_pushinteger(
L, int(base::convert_to<app::script::BlendMode>(
static_cast<LayerImage*>(layer)->blendMode())));
@ -261,7 +262,7 @@ int Layer_set_opacity(lua_State* L)
{
auto layer = get_docobj<Layer>(L, 1);
const int opacity = lua_tointeger(L, 2);
if (layer->isImage()) {
if (layer->isImage() || layer->isGroup()) {
Tx tx(layer->sprite());
tx(new cmd::SetLayerOpacity(static_cast<LayerImage*>(layer), opacity));
tx.commit();
@ -273,7 +274,7 @@ int Layer_set_blendMode(lua_State* L)
{
auto layer = get_docobj<Layer>(L, 1);
auto blendMode = app::script::BlendMode(lua_tointeger(L, 2));
if (layer->isImage()) {
if (layer->isImage() || layer->isGroup()) {
Tx tx(layer->sprite());
tx(new cmd::SetLayerBlendMode(static_cast<LayerImage*>(layer),
base::convert_to<doc::BlendMode>(blendMode)));

View File

@ -670,6 +670,7 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& sprite
// the original cel) before it can be used by the RenderEngine.
m_document->notifyExposeSpritePixels(m_sprite, gfx::Region(expose));
m_renderEngine->setComposeGroups(pref.experimental.composeGroups());
m_renderEngine->setNewBlendMethod(pref.experimental.newBlend());
m_renderEngine->setRefLayersVisiblity(true);
m_renderEngine->setSelectedLayer(m_layer);

View File

@ -26,6 +26,8 @@ EditorRender::EditorRender()
{
m_renderer->setNewBlendMethod(
Preferences::instance().experimental.newBlend());
m_renderer->setComposeGroups(
Preferences::instance().experimental.composeGroups());
}
EditorRender::~EditorRender()
@ -72,6 +74,11 @@ void EditorRender::setNewBlendMethod(const bool newBlend)
m_renderer->setNewBlendMethod(newBlend);
}
void EditorRender::setComposeGroups(const bool composeGroups)
{
m_renderer->setComposeGroups(composeGroups);
}
void EditorRender::setProjection(const render::Projection& projection)
{
m_renderer->setProjection(projection);

View File

@ -57,6 +57,7 @@ namespace app {
void setRefLayersVisiblity(const bool visible);
void setNonactiveLayersOpacity(const int opacity);
void setNewBlendMethod(const bool newBlend);
void setComposeGroups(bool composeGroups);
void setProjection(const render::Projection& projection);

View File

@ -29,6 +29,8 @@ Layer::Layer(ObjectType type, Sprite* sprite)
, m_flags(LayerFlags(
int(LayerFlags::Visible) |
int(LayerFlags::Editable)))
, m_blendmode(BlendMode::NORMAL)
, m_opacity(255)
{
ASSERT(type == ObjectType::LayerImage ||
type == ObjectType::LayerGroup ||
@ -228,8 +230,6 @@ Cel* Layer::cel(frame_t frame) const
LayerImage::LayerImage(ObjectType type, Sprite* sprite)
: Layer(type, sprite)
, m_blendmode(BlendMode::NORMAL)
, m_opacity(255)
{
}

View File

@ -125,6 +125,12 @@ namespace doc {
m_flags = LayerFlags(int(m_flags) & ~int(flags));
}
BlendMode blendMode() const { return m_blendmode; }
void setBlendMode(BlendMode blendmode) { m_blendmode = blendmode; }
int opacity() const { return m_opacity; }
void setOpacity(int opacity) { m_opacity = opacity; }
virtual Grid grid() const;
virtual Cel* cel(frame_t frame) const;
virtual void getCels(CelList& cels) const = 0;
@ -136,6 +142,9 @@ namespace doc {
LayerGroup* m_parent; // parent layer
LayerFlags m_flags; // stack order cannot be changed
BlendMode m_blendmode;
int m_opacity;
// Disable assigment
Layer& operator=(const Layer& other);
};
@ -151,12 +160,6 @@ namespace doc {
virtual int getMemSize() const override;
BlendMode blendMode() const { return m_blendmode; }
void setBlendMode(BlendMode blendmode) { m_blendmode = blendmode; }
int opacity() const { return m_opacity; }
void setOpacity(int opacity) { m_opacity = opacity; }
void addCel(Cel *cel);
void removeCel(Cel *cel);
void moveCel(Cel *cel, frame_t frame);
@ -181,8 +184,6 @@ namespace doc {
private:
void destroyAllCels();
BlendMode m_blendmode;
int m_opacity;
CelList m_cels; // List of all cels inside this layer used by frames.
};

View File

@ -35,21 +35,12 @@ void RenderPlan::addLayer(const Layer* layer,
if (!layer->isVisible())
return;
switch (layer->type()) {
case ObjectType::LayerImage:
case ObjectType::LayerTilemap: {
m_items.emplace_back(m_order, layer, layer->cel(frame));
break;
}
case ObjectType::LayerGroup: {
for (const auto child : static_cast<const LayerGroup*>(layer)->layers()) {
if (layer->isGroup() && !m_composeGroups) {
for (auto *const child : static_cast<const LayerGroup*>(layer)->layers()) {
addLayer(child, frame);
}
break;
}
} else {
m_items.emplace_back(m_order, layer, layer->cel(frame));
}
}

View File

@ -33,6 +33,8 @@ namespace doc {
using Items = std::vector<Item>;
RenderPlan();
RenderPlan(const bool composeGroups)
: m_composeGroups(composeGroups) { }
const Items& items() const {
if (m_processZIndex)
@ -49,6 +51,7 @@ namespace doc {
int m_order = 0;
mutable Items m_items;
mutable bool m_processZIndex = true;
bool m_composeGroups = false;
};
} // namespace doc

View File

@ -25,16 +25,19 @@ using namespace doc;
#define HELPER_LOG(a, b) \
a->layer()->name() << " instead of " << b->layer()->name()
#define HELPER_LOG_LAYER(a, b) \
a->name() << " instead of " << b->name()
#define EXPECT_PLAN(a, b, c, d) \
{ \
RenderPlan plan; \
plan.addLayer(spr->root(), 0); \
const auto& items = plan.items(); \
const auto items = plan.items(); \
EXPECT_EQ(a, items[0].cel) << HELPER_LOG(items[0].cel, a); \
EXPECT_EQ(b, items[1].cel) << HELPER_LOG(items[1].cel, b); \
EXPECT_EQ(c, items[2].cel) << HELPER_LOG(items[2].cel, c); \
EXPECT_EQ(d, items[3].cel) << HELPER_LOG(items[3].cel, d); \
}
} \
TEST(RenderPlan, ZIndex)
{
@ -134,6 +137,59 @@ TEST(RenderPlan, ZIndexBugWithEmptyCels)
d->setZIndex(-3); EXPECT_PLAN(d, a, b);
}
TEST(RenderPlan, DontAddChildrenOnComposeGroupFlag)
{
#undef EXPECT_PLAN
#define EXPECT_PLAN(a, b, c, d) \
{ \
RenderPlan plan(true), subplan(true); \
plan.addLayer(spr->root(), 0); \
const auto& items = plan.items(); \
EXPECT_EQ(spr->root(), items[0].layer) << HELPER_LOG_LAYER(items[0].layer, spr->root()); \
EXPECT_EQ(items.size(), 1); \
const auto& subItems = subplan.items(); \
for (const Layer* child : static_cast<const LayerGroup*>(spr->root())->layers()) \
if (child->isVisible()) subplan.addLayer(child, 0); \
EXPECT_EQ(subItems.size(), 4); \
EXPECT_EQ(a, subItems[0].layer) << HELPER_LOG_LAYER(subItems[0].layer, a); \
EXPECT_EQ(b, subItems[1].layer) << HELPER_LOG_LAYER(subItems[1].layer, b); \
EXPECT_EQ(c, subItems[2].layer) << HELPER_LOG_LAYER(subItems[2].layer, c); \
EXPECT_EQ(d, subItems[3].layer) << HELPER_LOG_LAYER(subItems[2].layer, d); \
}
auto doc = std::make_shared<Document>();
ImageSpec spec(ColorMode::INDEXED, 2, 2);
Sprite* spr;
doc->sprites().add(spr = Sprite::MakeStdSprite(spec));
LayerImage
*lay0 = static_cast<LayerImage*>(spr->root()->firstLayer()),
*lay1 = new LayerImage(spr),
*lay2 = new LayerImage(spr);
LayerGroup
*group0 = new LayerGroup(spr),
*group1 = new LayerGroup(spr);
lay0->setName("a");
lay1->setName("b");
lay2->setName("c");
group0->setName("g0");
group1->setName("g1");
group0->addLayer(lay1);
Cel *a, *b;
lay1->addCel(a = new Cel(0, ImageRef(Image::create(spec))));
lay2->addCel(b = new Cel(0, ImageRef(Image::create(spec))));
spr->root()->insertLayer(group0, lay0);
spr->root()->insertLayer(group1, group0);
spr->root()->insertLayer(lay2, group1);
EXPECT_PLAN(lay0, group0, group1, lay2);
}
int main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);

View File

@ -565,6 +565,11 @@ void Render::setNewBlend(const bool newBlend)
m_newBlendMethod = newBlend;
}
void Render::setComposeGroups(const bool composeGroup)
{
m_composeGroups = composeGroup;
}
void Render::setProjection(const Projection& projection)
{
m_proj = projection;
@ -669,7 +674,7 @@ void Render::renderLayer(
m_globalOpacity = 255;
doc::RenderPlan plan;
doc::RenderPlan plan(m_composeGroups);
plan.addLayer(layer, frame);
renderPlan(
plan, dstImage, area,
@ -768,7 +773,7 @@ void Render::renderSpriteLayers(Image* dstImage,
frame_t frame,
CompositeImageFunc compositeImage)
{
doc::RenderPlan plan;
doc::RenderPlan plan(m_composeGroups);
plan.addLayer(m_sprite->root(), frame);
// Draw the background layer.
@ -884,7 +889,7 @@ void Render::renderOnionskin(
else if (m_onionskin.type() == OnionskinType::RED_BLUE_TINT)
blendMode = (frameOut < frame ? BlendMode::RED_TINT: BlendMode::BLUE_TINT);
doc::RenderPlan plan;
doc::RenderPlan plan(m_composeGroups);
plan.addLayer(onionLayer, frameIn);
renderPlan(
plan, dstImage,
@ -1082,21 +1087,20 @@ void Render::renderPlan(
}
if (celImage) {
const LayerImage* imgLayer = static_cast<const LayerImage*>(layer);
BlendMode layerBlendMode =
(blendMode == BlendMode::UNSPECIFIED ?
imgLayer->blendMode():
layer->blendMode():
blendMode);
ASSERT(cel->opacity() >= 0);
ASSERT(cel->opacity() <= 255);
ASSERT(imgLayer->opacity() >= 0);
ASSERT(imgLayer->opacity() <= 255);
ASSERT(layer->opacity() >= 0);
ASSERT(layer->opacity() <= 255);
// Multiple three opacities: cel*layer*global (*nonactive-layer-opacity)
int t;
int opacity = cel->opacity();
opacity = MUL_UN8(opacity, imgLayer->opacity(), t);
opacity = MUL_UN8(opacity, layer->opacity(), t);
opacity = MUL_UN8(opacity, m_globalOpacity, t);
if (!isSelected && m_nonactiveLayersOpacity != 255)
opacity = MUL_UN8(opacity, m_nonactiveLayersOpacity, t);
@ -1140,10 +1144,39 @@ void Render::renderPlan(
break;
}
case ObjectType::LayerGroup:
case ObjectType::LayerGroup: {
if (!m_composeGroups) {
ASSERT(false);
break;
}
RenderPlan subPlan(m_composeGroups);
for (const Layer* child : static_cast<const LayerGroup*>(layer)->layers()) {
if (child->isVisible())
subPlan.addLayer(child, frame);
}
// We treat the group layer as a separate image so we can apply modifiers
// in the whole group while not affecting the layers behind it.
ImageRef groupImage(Image::createCopy(image));
groupImage.get()->clear(0);
// Render the group sublayers
// We don't apply any blend mode here, as the group layer is a separate image
// and we want to first calculate the layer blendmodes separately and then merge the images
renderPlan(subPlan, groupImage.get(), area, frame, compositeImage,
render_background, render_transparent, BlendMode::UNSPECIFIED);
// Get the pallete of the sprite in the current frame
Palette* pal = m_sprite->palette(frame);
// Render the group image in the main image, applying the group modifiers
// The global opacity is not applied here, as it is applied in the LayerImage case.
composite_image(image, groupImage.get(), pal, 0 ,0 , layer->opacity(), layer->blendMode());
break;
}
}
// Draw extras

View File

@ -60,6 +60,7 @@ namespace render {
void setRefLayersVisiblity(const bool visible);
void setNonactiveLayersOpacity(const int opacity);
void setNewBlend(const bool newBlend);
void setComposeGroups(bool composeGroup);
void setProjection(const Projection& projection);
void setBgOptions(const BgOptions& bg);
void setSelectedLayer(const Layer* layer);
@ -224,6 +225,7 @@ namespace render {
BlendMode m_previewBlendMode;
OnionskinOptions m_onionskin;
ImageBufferPtr m_tmpBuf;
bool m_composeGroups = false;
};
void composite_image(Image* dst,

View File

@ -0,0 +1,186 @@
-- Copyright (C) 2024 Igara Studio S.A.
--
-- This file is released under the terms of the MIT license.
-- Read LICENSE.txt for more information.
dofile('./test_utils.lua')
-- Enable experimental compose groups feature
app.preferences.experimental.compose_groups = true
function create_group_layer()
local s = Sprite(2, 2)
assert(#s.layers == 1)
-- create two layers and a group layer
local a = s.layers[1] a.name = "a"
assert(#s.layers == 1)
local b = s:newLayer() b.name = "b"
assert(#s.layers == 2)
local g = s:newGroup() g.name = "g"
assert(#s.layers == 3)
-- b is child of g
b.parent = g
assert(g.parent == s)
assert(b.parent == g)
assert(#s.layers == 2)
assert(s.layers[1] == a)
assert(s.layers[2] == g)
return s, a, b, g
end
-- Test groups opacity to impact on children layers
do
assert(app.preferences.experimental.compose_groups == true)
local s, a, b, g = create_group_layer()
-- draw in b layer
local cel = s:newCel(b, 1, Image(2, 2, ColorMode.RGB))
local img = cel.image
img:drawPixel(0, 0, Color(255, 0, 0, 255))
img:drawPixel(1, 0, Color(0, 255, 0, 255))
img:drawPixel(0, 1, Color(0, 0, 255, 255))
img:drawPixel(1, 1, Color(255, 255, 0, 255))
local r = Image(s.spec) -- render
r:drawSprite(s, 1, 0, 0)
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 0, 255))
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 255, 0, 255))
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 255, 255))
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(255, 255, 0, 255))
-- Set opacity to 50%
g.opacity = 128
r = Image(s.spec)
r:drawSprite(s, 1, 0, 0)
print(g.opacity)
print(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 0, 255))
-- Assert that the image is drawn with 50% opacity
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 0, 128))
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 255, 0, 128))
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 255, 128))
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(255, 255, 0, 128))
-- Set opacity to 100%
g.opacity = 255
r = Image(s.spec)
r:drawSprite(s, 1, 0, 0)
expect_eq(g.opacity, 255)
-- Assert that the image is drawn with 100% opacity
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 0, 255))
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 255, 0, 255))
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 255, 255))
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(255, 255, 0, 255))
-- Set group opacity to 50% and layer opacity to 50%
g.opacity = 128
b.opacity = 128
r = Image(s.spec)
r:drawSprite(s, 1, 0, 0)
assert(g.opacity == 128)
assert(b.opacity == 128)
-- Assert that the image is drawn with 25% opacity
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 0, 64))
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 255, 0, 64))
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 255, 64))
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(255, 255, 0, 64))
-- Create a new layer in front of the group and check that it's not affected by the group opacity
local c = s:newLayer()
c.name = "c"
-- draw in c layer
local cel = s:newCel(c, 1, Image(1, 1, ColorMode.RGB))
local img = cel.image
img:drawPixel(0, 0, Color(255, 0, 255, 255))
r = Image(s.spec)
r:drawSprite(s, 1, 0, 0)
-- Assert that the first pixel is drawn with 100% opacity and the remaining ones with 25% opacity
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 255, 255))
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 255, 0, 64))
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 255, 64))
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(255, 255, 0, 64))
end
do -- test group blend to impact children layers
assert(app.preferences.experimental.compose_groups == true)
local s, a, b, g = create_group_layer()
-- draw all black in layer a
local cel = s:newCel(a, 1, Image(2, 2, ColorMode.RGB))
local img = cel.image
img:drawPixel(0, 0, Color(0, 0, 0, 255))
img:drawPixel(1, 0, Color(0, 0, 0, 255))
img:drawPixel(0, 1, Color(0, 0, 0, 255))
img:drawPixel(1, 1, Color(0, 0, 0, 255))
-- draw in b layer
local cel = s:newCel(b, 1, Image(2, 2, ColorMode.RGB))
local img = cel.image
img:drawPixel(0, 0, Color(255, 0, 0, 255))
img:drawPixel(1, 0, Color(0, 255, 0, 255))
img:drawPixel(0, 1, Color(0, 0, 255, 255))
img:drawPixel(1, 1, Color(255, 255, 0, 255))
local r = Image(s.spec) -- render
r:drawSprite(s, 1, 0, 0)
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 0, 255))
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 255, 0, 255))
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 255, 255))
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(255, 255, 0, 255))
-- Set blend to MULTIPLY
g.blendMode = BlendMode.MULTIPLY
r = Image(s.spec)
r:drawSprite(s, 1, 0, 0)
-- Assert that the image is drawn with MULTIPLY blend mode
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(0, 0, 0, 255))
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 0, 0, 255))
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 0, 255))
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(0, 0, 0, 255))
-- Create a new layer in front of the group and check that it's not affected by the group blend mode
local c = s:newLayer()
c.name = "c"
-- draw in c layer
local cel = s:newCel(c, 1, Image(1, 1, ColorMode.RGB))
local img = cel.image
img:drawPixel(0, 0, Color(255, 0, 255, 255))
r = Image(s.spec)
r:drawSprite(s, 1, 0, 0)
-- Assert that the first pixel is drawn with Normal blend mode and the remaining ones with Multiply blend mode
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 255, 255))
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 0, 0, 255))
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 0, 255))
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(0, 0, 0, 255))
end

View File

@ -42,6 +42,24 @@ local function dump_img(image)
print('}')
end
function expect_clr(color, expectedColor)
if color ~= expectedColor then
print(debug.traceback())
print('Expected A == B but:')
print(string.format(' - Value A = rgba(%d,%d,%d,%d)',
app.pixelColor.rgbaR(color),
app.pixelColor.rgbaG(color),
app.pixelColor.rgbaB(color),
app.pixelColor.rgbaA(color)))
print(string.format(' - Value B = rgba(%d,%d,%d,%d)',
app.pixelColor.rgbaR(expectedColor),
app.pixelColor.rgbaG(expectedColor),
app.pixelColor.rgbaB(expectedColor),
app.pixelColor.rgbaA(expectedColor)))
assert(color == expectedColor)
end
end
function expect_img(image, expectedPixels)
local w = image.width
local h = image.height