Add 9-slice transformation support

This commit is contained in:
Martín Capello 2024-07-31 16:50:40 -03:00 committed by David Capello
parent ca75a98679
commit fb74feea21
3 changed files with 220 additions and 121 deletions

View File

@ -21,7 +21,6 @@
#include "app/ui/status_bar.h"
#include "app/ui_context.h"
#include "app/util/expand_cel_canvas.h"
#include "app/util/new_image_from_mask.h"
#include "doc/algorithm/rotate.h"
#include "doc/blend_internals.h"
#include "doc/color.h"
@ -90,66 +89,44 @@ MovingSliceState::MovingSliceState(Editor* editor,
editor->captureMouse();
}
void MovingSliceState::initializeItemsContent() {
for (auto& item : m_items) {
// Align slice origin to tiles origin under Tiles mode.
if (m_site.tilemapMode() == TilemapMode::Tiles) {
auto origin = m_site.grid().tileToCanvas(m_site.grid().canvasToTile(item.newKey.bounds().origin()));
auto bounds = gfx::Rect(origin, item.newKey.bounds().size());
item.newKey.setBounds(bounds);
}
// Reserve one ItemContent slot for each selected layer.
item.content.reserve(m_selectedLayers.size());
for (const auto* layer : m_selectedLayers) {
Mask mask;
ImageRef image = ImageRef();
mask.add(item.newKey.bounds());
if (layer &&
layer->isTilemap() &&
m_site.tilemapMode() == TilemapMode::Tiles) {
image.reset(new_tilemap_from_mask(m_site, &mask));
}
else {
image.reset(new_image_from_mask(
*layer,
m_frame,
&mask,
Preferences::instance().experimental.newBlend()));
}
item.pushContent(image);
}
}
}
void MovingSliceState::onEnterState(Editor* editor)
{
if (editor->slicesTransforms() && !m_items.empty()) {
for (auto& item : m_items) {
// Align slice origin to tiles origin under Tiles mode.
if (m_site.tilemapMode() == TilemapMode::Tiles) {
auto origin = m_site.grid().tileToCanvas(m_site.grid().canvasToTile(item.newKey.bounds().origin()));
auto bounds = gfx::Rect(origin, item.newKey.bounds().size());
item.newKey.setBounds(bounds);
}
item.imgs.reserve(m_selectedLayers.size());
item.masks.reserve(m_selectedLayers.size());
int i = 0;
for (const auto* layer : m_selectedLayers) {
item.masks.push_back(std::make_shared<Mask>());
item.imgs.push_back(ImageRef());
item.masks[i]->add(item.newKey.bounds());
item.masks[i]->freeze();
if (layer &&
layer->isTilemap() &&
m_site.tilemapMode() == TilemapMode::Tiles) {
item.imgs[i].reset(new_tilemap_from_mask(m_site, item.masks[i].get()));
}
else {
item.imgs[i].reset(new_image_from_mask(
*layer,
m_frame,
item.masks[i].get(),
Preferences::instance().experimental.newBlend()));
// TODO: See if part of the code in fromImage can be replaced
// refactored to use mask_image (in cel_ops.h)?
item.masks[i]->fromImage(item.imgs[i].get(), item.masks[i]->origin());
}
// If there is just one layer selected, we can use the same image as the
// mergedImg.
if (m_selectedLayers.size() == 1) {
item.mergedImg = item.imgs[0];
item.mergedMask = item.masks[0];
}
else {
if (i == 0) {
const gfx::Rect& srcBounds = item.imgs[i]->bounds();
item.mergedImg.reset(Image::create(layer->sprite()->pixelFormat(), srcBounds.w, srcBounds.h));
item.mergedImg->clear(layer->sprite()->transparentColor());
item.mergedMask = std::make_shared<Mask>(*item.masks[i].get());
item.mergedMask->freeze();
}
else {
item.mergedMask->add(*item.masks[i].get());
}
copy_masked_zones(item.mergedImg.get(),
item.imgs[i].get(),
item.masks[i].get(),
item.masks[i]->bounds().x,
item.masks[i]->bounds().y);
}
i++;
}
}
initializeItemsContent();
// Clear brush preview, as the extra cel will be replaced with the
// transformed image.
@ -157,7 +134,7 @@ void MovingSliceState::onEnterState(Editor* editor)
clearSlices();
drawSliceContents();
drawExtraCel();
// Redraw the editor.
editor->invalidate();
@ -179,9 +156,10 @@ bool MovingSliceState::onMouseUp(Editor* editor, MouseMessage* msg)
auto* layer = m_selectedLayers[i];
m_site.layer(layer);
m_site.frame(m_frame);
drawSliceContentsByLayer(i);
drawExtraCel(i);
stampExtraCelImage();
}
m_site.document()->setExtraCel(ExtraCelRef(nullptr));
}
}
@ -226,41 +204,7 @@ void MovingSliceState::stampExtraCelImage()
expand.commit();
}
void MovingSliceState::drawSliceContents()
{
gfx::Rect bounds;
for (auto& item : m_items)
bounds |= item.newKey.bounds();
drawExtraCel(bounds, [this](const gfx::Rect& bounds, Image* dst){
for (auto& item : m_items) {
// Draw the transformed pixels in the extra-cel which is the chunk
// of pixels that the user is moving.
drawImage(dst,
item.mergedImg.get(),
item.mergedMask.get(),
gfx::Rect(item.newKey.bounds()).offset(-bounds.origin()));
}
});
}
void MovingSliceState::drawSliceContentsByLayer(int layerIdx)
{
gfx::Rect bounds;
for (auto& item : m_items)
bounds |= item.newKey.bounds();
drawExtraCel(bounds, [this, layerIdx](const gfx::Rect& bounds, Image* dst){
for (auto& item : m_items) {
drawImage(dst,
item.imgs[layerIdx].get(),
item.masks[layerIdx].get(),
gfx::Rect(item.newKey.bounds()).offset(-bounds.origin()));
}
});
}
void MovingSliceState::drawExtraCel(const gfx::Rect& bounds, DrawExtraCelContentFunc drawContent)
void MovingSliceState::drawExtraCel(int layerIdx)
{
int t, opacity = (m_site.layer()->isImage() ?
static_cast<LayerImage*>(m_site.layer())->opacity(): 255);
@ -270,6 +214,10 @@ void MovingSliceState::drawExtraCel(const gfx::Rect& bounds, DrawExtraCelContent
if (!m_extraCel)
m_extraCel.reset(new ExtraCel);
gfx::Rect bounds;
for (auto& item : m_items)
bounds |= item.newKey.bounds();
if (!bounds.isEmpty()) {
gfx::Size extraCelSize;
if (m_site.tilemapMode() == TilemapMode::Tiles) {
@ -319,13 +267,31 @@ void MovingSliceState::drawExtraCel(const gfx::Rect& bounds, DrawExtraCelContent
dst, m_site.layer(), m_site.frame(),
gfx::Clip(0, 0, bounds),
doc::BlendMode::SRC);
}
drawContent(bounds, dst);
for (auto& item : m_items) {
// Draw the transformed pixels in the extra-cel which is the chunk
// of pixels that the user is moving.
drawItem(dst, item, bounds.origin(), layerIdx);
}
}
}
void MovingSliceState::drawItem(doc::Image* dst,
const Item& item,
const gfx::Point& itemsBoundsOrigin,
int layerIdx)
{
const ItemContentRef content = (layerIdx >= 0 ? item.content[layerIdx]
: item.mergedContent);
content->forEachPart(
[this, dst, itemsBoundsOrigin]
(const doc::Image* src, const doc::Mask* mask, const gfx::Rect& bounds) {
drawImage(dst, src, mask, gfx::Rect(bounds).offset(-itemsBoundsOrigin));
});
}
void MovingSliceState::drawImage(doc::Image* dst,
const doc::Image* src,
const doc::Mask* mask,
@ -347,7 +313,7 @@ void MovingSliceState::drawImage(doc::Image* dst,
}
else {
doc::algorithm::parallelogram(
dst, src, mask->bitmap(),
dst, src, (mask ? mask->bitmap() : nullptr),
bounds.x , bounds.y,
bounds.x+bounds.w, bounds.y,
bounds.x+bounds.w, bounds.y+bounds.h,
@ -459,7 +425,7 @@ bool MovingSliceState::onMouseMove(Editor* editor, MouseMessage* msg)
}
if (editor->slicesTransforms())
drawSliceContents();
drawExtraCel();
// Redraw the editor.
editor->invalidate();

View File

@ -12,6 +12,7 @@
#include "app/ui/editor/editor_hit.h"
#include "app/ui/editor/standby_state.h"
#include "app/ui/editor/pixels_movement.h"
#include "app/util/new_image_from_mask.h"
#include "doc/frame.h"
#include "doc/image_ref.h"
#include "doc/mask.h"
@ -37,38 +38,167 @@ namespace app {
bool requireBrushPreview() override { return false; }
private:
using DrawExtraCelContentFunc = std::function<void(const gfx::Rect& bounds, Image* dst)>;
struct Item;
using ItemContentPartFunc = std::function<
void(const doc::Image* src,
const doc::Mask* mask,
const gfx::Rect& bounds)>;
class ItemContent {
public:
ItemContent(const Item *item) : m_item(item) {}
virtual ~ItemContent() {};
virtual void forEachPart(ItemContentPartFunc fn) = 0;
virtual void copy(const Image* src) = 0;
protected:
const Item* m_item = nullptr;
};
using ItemContentRef = std::shared_ptr<ItemContent>;
class SingleSlice : public ItemContent {
public:
SingleSlice(const Item *item,
const ImageRef& image) : SingleSlice(item, image, item->oldKey.bounds().origin()) {
}
SingleSlice(const Item *item,
const ImageRef& image,
const gfx::Point& origin) : ItemContent(item)
, m_img(image) {
m_mask = std::make_shared<Mask>();
m_mask->freeze();
m_mask->fromImage(m_img.get(), origin);
}
~SingleSlice() {
m_mask->unfreeze();
}
void forEachPart(ItemContentPartFunc fn) override {
fn(m_img.get(), m_mask.get(), m_item->newKey.bounds());
}
void copy(const Image* src) override {
doc::Mask srcMask;
srcMask.freeze();
srcMask.add(m_item->oldKey.bounds());
// TODO: See if part of the code in fromImage can be replaced or
// refactored to use mask_image (in cel_ops.h)?
srcMask.fromImage(src, srcMask.origin());
copy_masked_zones(m_img.get(), src, &srcMask, srcMask.bounds().x, srcMask.bounds().y);
m_mask->add(srcMask);
srcMask.unfreeze();
}
const Image* image() { return m_img.get(); }
Mask* mask() { return m_mask.get(); }
private:
// Images containing the parts of each selected layer of the sprite under
// the slice bounds that will be transformed when Slice Transform is
// enabled
ImageRef m_img;
// Masks for each of the images in imgs vector
MaskRef m_mask;
};
class NineSlice : public ItemContent {
public:
NineSlice(const Item *item,
const ImageRef& image) : ItemContent(item) {
if (!m_item->oldKey.hasCenter()) return;
const gfx::Rect totalBounds(m_item->oldKey.bounds().size());
gfx::Rect bounds[9];
totalBounds.nineSlice(m_item->oldKey.center(), bounds);
for (int i=0; i<9; ++i) {
if (!bounds[i].isEmpty()) {
ImageRef img;
img.reset(Image::create(image->pixelFormat(), bounds[i].w, bounds[i].h));
img->copy(image.get(), gfx::Clip(0, 0, bounds[i]));
m_part[i] = std::make_unique<SingleSlice>(m_item, img, bounds[i].origin());
}
}
}
~NineSlice() {}
void forEachPart(ItemContentPartFunc fn) override {
gfx::Rect bounds[9];
m_item->newKey.bounds().nineSlice(m_item->newKey.center(), bounds);
for (int i=0; i<9; ++i) {
if (m_part[i])
fn(m_part[i]->image(), m_part[i]->mask(), bounds[i]);
}
}
void copy(const Image* src) override {
if (!m_item->oldKey.hasCenter()) return;
const gfx::Rect totalBounds(m_item->oldKey.bounds().size());
gfx::Rect bounds[9];
totalBounds.nineSlice(m_item->oldKey.center(), bounds);
for (int i=0; i<9; ++i) {
if (!bounds[i].isEmpty()) {
ImageRef img;
img.reset(Image::create(src->pixelFormat(), bounds[i].w, bounds[i].h));
img->copy(src, gfx::Clip(0, 0, bounds[i]));
m_part[i]->copy(img.get());
}
}
}
private:
std::unique_ptr<SingleSlice> m_part[9] = {nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr};
};
struct Item {
doc::Slice* slice;
doc::SliceKey oldKey;
doc::SliceKey newKey;
// Images containing the parts of each selected layer of the sprite under
// Vector of each selected layer's part of the sprite under
// the slice bounds that will be transformed when Slice Transform is
// enabled
std::vector<ImageRef> imgs;
// Masks for each of the images in imgs vector
std::vector<MaskRef> masks;
std::vector<ItemContentRef> content;
ItemContentRef mergedContent;
// Image containing the result of merging all the images in the imgs
// vector
ImageRef mergedImg = nullptr;
MaskRef mergedMask = nullptr;
void pushContent(const ImageRef& image) {
if (content.empty()) {
const gfx::Rect& srcBounds = image->bounds();
ImageRef mergedImage;
mergedImage.reset(Image::create(image->pixelFormat(), srcBounds.w, srcBounds.h));
mergedImage->clear(image->maskColor());
mergedContent = (this->oldKey.hasCenter() ? (ItemContentRef)std::make_shared<NineSlice>(this, mergedImage)
: std::make_shared<SingleSlice>(this, mergedImage));
}
~Item() {
if (!masks.empty() && mergedMask != masks[0])
mergedMask->unfreeze();
for (auto& m : masks)
m->unfreeze();
mergedContent->copy(image.get());
ItemContentRef ssc = (this->oldKey.hasCenter() ? (ItemContentRef)std::make_shared<NineSlice>(this, image)
: std::make_shared<SingleSlice>(this, image));
content.push_back(ssc);
}
};
// Initializes the content of the Items. So each item will contain the
// part of the cel's layers within the corresponding slice.
void initializeItemsContent();
Item getItemForSlice(doc::Slice* slice);
gfx::Rect selectedSlicesBounds() const;
void drawSliceContents();
void drawSliceContentsByLayer(int layerIdx);
void drawExtraCel(const gfx::Rect& bounds, DrawExtraCelContentFunc drawContent);
void drawExtraCel(int layerIdx = -1);
void drawItem(doc::Image* dst,
const Item& item,
const gfx::Point& itemsBoundsOrigin,
int layerIdx);
void drawImage(doc::Image* dst,
const doc::Image* src,
const doc::Mask* mask,

View File

@ -127,13 +127,16 @@ doc::Image* new_image_from_mask(const Layer& layer,
src = dst.get();
}
else {
src = cel->image();
x = cel->x();
y = cel->y();
if (cel) {
src = cel->image();
x = cel->x();
y = cel->y();
}
}
// Copy the masked zones
copy_masked_zones(dst.get(), src, srcMask, x, y);
if (src)
// Copy the masked zones
copy_masked_zones(dst.get(), src, srcMask, x, y);
return dst.release();
}