Merge branch 'slices-transform-switch' into beta (#4533)

This commit is contained in:
David Capello 2024-09-10 15:58:54 -03:00
commit 132405b3cd
15 changed files with 937 additions and 81 deletions

View File

@ -578,6 +578,8 @@ all = All
none = None
select_slices = Select All Slices
deselect_slices = Deselect Slices
slice_transform = Transform
slice_transform_tip = Transform pixels along slice modification
slice_props = Slice Properties
delete_slice = Delete Slice
discard_brush = Discard Brush (Esc)

View File

@ -272,6 +272,7 @@ target_sources(app-lib PRIVATE
cmd/clear_image.cpp
cmd/clear_mask.cpp
cmd/clear_rect.cpp
cmd/clear_slices.cpp
cmd/configure_background.cpp
cmd/convert_color_profile.cpp
cmd/copy_cel.cpp

View File

@ -0,0 +1,152 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/cmd/clear_slices.h"
#include "app/doc.h"
#include "app/site.h"
#include "app/util/cel_ops.h"
#include "doc/algorithm/fill_selection.h"
#include "doc/cel.h"
#include "doc/grid.h"
#include "doc/layer.h"
#include "doc/layer_list.h"
#include "doc/primitives.h"
namespace app {
namespace cmd {
using namespace doc;
ClearSlices::ClearSlices(const Site& site,
const LayerList& layers,
frame_t frame,
const std::vector<SliceKey>& slicesKeys)
: m_tilemapMode(site.tilemapMode())
, m_tilesetMode(site.tilesetMode())
{
if (layers.empty())
return;
Doc* doc = static_cast<Doc*>((*layers.begin())->sprite()->document());
for (auto* layer : layers) {
Cel* cel = layer->cel(frame);
if (!cel)
continue;
SlicesContent sc(cel);
for (const auto& sk : slicesKeys) {
sc.mask.add(sk.bounds());
}
gfx::Rect maskBounds = sc.mask.bounds();
Image* image = cel->image();
assert(image);
if (!image)
continue;
gfx::Rect imageBounds = cel->bounds();
color_t bgcolor = doc->bgColor(layer);
if (image->pixelFormat() == IMAGE_TILEMAP) {
auto grid = cel->grid();
imageBounds = gfx::Rect(grid.canvasToTile(cel->position()),
cel->image()->size());
maskBounds = grid.canvasToTile(maskBounds);
bgcolor = doc::notile; // TODO configurable empty tile
}
gfx::Rect cropBounds = (imageBounds & maskBounds);
if (cropBounds.isEmpty())
continue;
cropBounds.offset(-imageBounds.origin());
sc.cropPos = cropBounds.origin();
sc.bgcolor = bgcolor;
sc.copy.reset(crop_image(image, cropBounds, sc.bgcolor));
m_slicesContents.push_back(sc);
}
}
void ClearSlices::onExecute()
{
m_seq.execute(context());
clear();
}
void ClearSlices::onUndo()
{
restore();
m_seq.undo();
}
void ClearSlices::onRedo()
{
m_seq.redo();
clear();
}
void ClearSlices::clear()
{
for (auto& sc : m_slicesContents) {
if (!sc.copy)
continue;
if (sc.cel()->layer()->isTilemap() && m_tilemapMode == TilemapMode::Pixels) {
Doc* doc = static_cast<Doc*>(sc.cel()->document());
color_t bgcolor = doc->bgColor(sc.cel()->layer());
modify_tilemap_cel_region(
&m_seq, sc.cel(), nullptr,
gfx::Region(sc.mask.bounds()),
m_tilesetMode,
[sc, bgcolor](const doc::ImageRef& origTile,
const gfx::Rect& tileBoundsInCanvas) -> doc::ImageRef {
doc::ImageRef modified(doc::Image::createCopy(origTile.get()));
doc::algorithm::fill_selection(
modified.get(),
tileBoundsInCanvas,
&sc.mask,
bgcolor,
nullptr);
return modified;
});
}
else {
Grid grid = sc.cel()->grid();
doc::algorithm::fill_selection(
sc.cel()->image(),
sc.cel()->bounds(),
&sc.mask,
sc.bgcolor,
(sc.cel()->image()->isTilemap() ? &grid: nullptr));
}
}
}
void ClearSlices::restore()
{
for (auto& sc : m_slicesContents) {
if (!sc.copy)
continue;
copy_image(sc.cel()->image(),
sc.copy.get(),
sc.cropPos.x,
sc.cropPos.y);
}
}
} // namespace cmd
} // namespace app

View File

@ -0,0 +1,78 @@
// Aseprite
// Copyright (C) 2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_CMD_CLEAR_SLICES_H_INCLUDED
#define APP_CMD_CLEAR_SLICES_H_INCLUDED
#pragma once
#include "app/cmd.h"
#include "app/cmd_sequence.h"
#include "app/cmd/with_cel.h"
#include "app/tilemap_mode.h"
#include "app/tileset_mode.h"
#include "doc/cel.h"
#include "doc/image_ref.h"
#include "doc/layer_list.h"
#include "doc/mask.h"
#include "doc/slice.h"
#include <vector>
namespace app {
class Site;
namespace cmd {
using namespace doc;
// Clears the enclosed content of the passed slices for each layer in the
// layers list for the specified frame.
class ClearSlices : public Cmd {
public:
ClearSlices(const Site& site,
const LayerList& layers,
frame_t frame,
const std::vector<SliceKey>& slicesKeys);
protected:
void onExecute() override;
void onUndo() override;
void onRedo() override;
size_t onMemSize() const override {
size_t sliceContentsSize = 0;
for (const auto& sc : m_slicesContents) {
sliceContentsSize += sc.memSize();
}
return sizeof(*this) + m_seq.memSize() + sliceContentsSize;
}
private:
struct SlicesContent : public WithCel {
SlicesContent(Cel* cel) : WithCel(cel) {}
// Image having a copy of the content of each selected slice.
ImageRef copy = nullptr;
Mask mask;
gfx::Point cropPos;
color_t bgcolor;
size_t memSize() const {
return sizeof(*this) + (copy ? copy->getMemSize(): 0);
}
};
void clear();
void restore();
CmdSequence m_seq;
// Slices content for each selected layer's cel
std::vector<SlicesContent> m_slicesContents;
TilemapMode m_tilemapMode;
TilesetMode m_tilesetMode;
};
} // namespace cmd
} // namespace app
#endif

View File

@ -1650,6 +1650,7 @@ public:
: m_doc(nullptr)
, m_sel(2)
, m_combobox(this)
, m_transform(Strings::context_bar_slice_transform())
, m_action(2)
{
auto* theme = SkinTheme::get(this);
@ -1665,6 +1666,12 @@ public:
m_combobox.setExpansive(true);
m_combobox.setMinSize(gfx::Size(256*guiscale(), 0));
m_transform.Click.connect(
[this]() {
if (auto* editor = Editor::activeEditor())
editor->slicesTransforms(m_transform.isSelected());
});
m_action.addItem(theme->parts.iconUserData(), theme->styles.buttonsetItemIconMono());
m_action.addItem(theme->parts.iconClose(), theme->styles.buttonsetItemIconMono());
m_action.ItemChange.connect(
@ -1674,6 +1681,7 @@ public:
addChild(&m_sel);
addChild(&m_combobox);
addChild(&m_transform);
addChild(&m_action);
m_combobox.setVisible(false);
@ -1685,6 +1693,8 @@ public:
m_sel.at(0), Strings::context_bar_select_slices(), BOTTOM);
tooltipManager->addTooltipFor(
m_sel.at(1), Strings::context_bar_deselect_slices(), BOTTOM);
tooltipManager->addTooltipFor(
&m_transform, Strings::context_bar_slice_transform_tip(), BOTTOM);
tooltipManager->addTooltipFor(
m_action.at(0), Strings::context_bar_slice_props(), BOTTOM);
tooltipManager->addTooltipFor(
@ -1731,6 +1741,12 @@ public:
}
private:
void onInitTheme(InitThemeEvent& ev) override {
HBox::onInitTheme(ev);
auto* theme = SkinTheme::get(this);
m_transform.setStyle(theme->styles.miniCheckBox());
}
void onVisible(bool visible) override {
HBox::onVisible(visible);
m_combobox.closeListBox();
@ -1764,8 +1780,12 @@ private:
visible != m_action.isVisible());
m_combobox.setVisible(visible);
m_transform.setVisible(visible);
m_action.setVisible(visible);
if (auto* editor = Editor::activeEditor())
m_transform.setSelected(editor->slicesTransforms());
if (relayout)
parent()->layout();
}
@ -1833,6 +1853,7 @@ private:
Doc* m_doc;
ButtonSet m_sel;
Combo m_combobox;
CheckBox m_transform;
ButtonSet m_action;
bool m_changeFromEntry;
std::string m_filter;

View File

@ -314,6 +314,8 @@ namespace app {
bool selectSliceBox(const gfx::Rect& box);
void selectAllSlices();
bool hasSelectedSlices() const { return !m_selectedSlices.empty(); }
void slicesTransforms(bool value) { m_slicesTransforms = value; }
bool slicesTransforms() const { return m_slicesTransforms; }
// Called by DocView's InputChainElement::onCancel() impl when Esc
// key is pressed to cancel the active selection.
@ -490,6 +492,9 @@ namespace app {
// For slices
doc::SelectedObjects m_selectedSlices;
// When true, modifications to slices positions/sizes will transform the
// pixels inside their boundaries.
bool m_slicesTransforms = false;
// Active sprite editor with the keyboard focus.
static Editor* m_activeEditor;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2017-2018 David Capello
//
// This program is distributed under the terms of
@ -12,16 +12,22 @@
#include "app/ui/editor/moving_slice_state.h"
#include "app/cmd/set_slice_key.h"
#include "app/cmd/clear_slices.h"
#include "app/context_access.h"
#include "app/tx.h"
#include "app/ui/editor/editor.h"
#include "app/ui/status_bar.h"
#include "app/ui_context.h"
#include "app/util/expand_cel_canvas.h"
#include "doc/algorithm/rotate.h"
#include "doc/blend_internals.h"
#include "doc/slice.h"
#include "ui/message.h"
#include "render/render.h"
#include <algorithm>
#include <cmath>
#include <vector>
namespace app {
@ -34,6 +40,9 @@ MovingSliceState::MovingSliceState(Editor* editor,
: m_frame(editor->frame())
, m_hit(hit)
, m_items(std::max<std::size_t>(1, selectedSlices.size()))
, m_tx(Tx::DontLockDoc, UIContext::instance(),
UIContext::instance()->activeDocument(),
(editor->slicesTransforms() ? "Slices Transformation" : "Slice Movement"))
{
m_mouseStart = editor->screenToEditor(msg->position());
@ -48,34 +57,277 @@ MovingSliceState::MovingSliceState(Editor* editor,
}
}
editor->getSite(&m_site);
// Prevent using different tilemap and tileset modes when the last selected
// layer is not a tilemap.
if (!m_site.layer()->isTilemap()) {
m_site.tilemapMode(TilemapMode::Pixels);
m_site.tilesetMode(TilesetMode::Auto);
}
if (editor->slicesTransforms() && !m_items.empty()) {
DocRange range = m_site.range();
SelectedLayers selectedLayers = range.selectedLayers();
// Do not take into account invisible layers.
for (auto it = selectedLayers.begin(); it != selectedLayers.end(); ++it) {
if (!(*it)->isVisible()) {
range.eraseAndAdjust(*it);
}
}
m_selectedLayers = range.selectedLayers().toAllLayersList();
if (m_selectedLayers.empty() && m_site.layer()->isVisible()) {
m_selectedLayers.push_back(m_site.layer());
}
}
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()) {
initializeItemsContent();
// Clear brush preview, as the extra cel will be replaced with the
// transformed image.
editor->brushPreview().hide();
clearSlices();
drawExtraCel();
// Redraw the editor.
editor->invalidate();
}
}
bool MovingSliceState::onMouseUp(Editor* editor, MouseMessage* msg)
{
{
ContextWriter writer(UIContext::instance(), 1000);
Tx tx(writer, "Slice Movement", ModifyDocument);
CmdTransaction* cmds = m_tx;
for (const auto& item : m_items) {
item.slice->insert(m_frame, item.oldKey);
tx(new cmd::SetSliceKey(item.slice, m_frame, item.newKey));
cmds->addAndExecute(writer.context(),
new cmd::SetSliceKey(item.slice, m_frame, item.newKey));
if (editor->slicesTransforms()) {
for (int i=0; i<m_selectedLayers.size(); ++i) {
auto* layer = m_selectedLayers[i];
m_site.layer(layer);
m_site.frame(m_frame);
drawExtraCel(i);
stampExtraCelImage();
}
m_site.document()->setExtraCel(ExtraCelRef(nullptr));
}
}
tx.commit();
m_tx.commit();
}
editor->backToPreviousState();
editor->releaseMouse();
editor->invalidate();
return true;
}
void MovingSliceState::stampExtraCelImage()
{
const Image* image = m_extraCel->image();
if (!image)
return;
const Cel* cel = m_extraCel->cel();
ExpandCelCanvas expand(
m_site, m_site.layer(),
TiledMode::NONE, m_tx,
ExpandCelCanvas::None);
gfx::Point dstPt;
gfx::Size canvasImageSize = image->size();
if (m_site.tilemapMode() == TilemapMode::Tiles) {
doc::Grid grid = m_site.grid();
dstPt = grid.canvasToTile(cel->position());
canvasImageSize = grid.tileToCanvas(gfx::Rect(dstPt, canvasImageSize)).size();
}
else {
dstPt = cel->position() - expand.getCel()->position();
}
expand.validateDestCanvas(
gfx::Region(gfx::Rect(cel->position(), canvasImageSize)));
expand.getDestCanvas()->copy(image, gfx::Clip(dstPt, image->bounds()));
expand.commit();
}
void MovingSliceState::drawExtraCel(int layerIdx)
{
int t, opacity = (m_site.layer()->isImage() ?
static_cast<LayerImage*>(m_site.layer())->opacity(): 255);
Cel* cel = m_site.cel();
if (cel) opacity = MUL_UN8(opacity, cel->opacity(), t);
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) {
// Transforming tiles
extraCelSize = m_site.grid().canvasToTile(bounds).size();
}
else {
// Transforming pixels
extraCelSize = bounds.size();
}
m_extraCel->create(
m_site.tilemapMode(),
m_site.document()->sprite(),
bounds,
extraCelSize,
m_site.frame(),
opacity);
m_extraCel->setType(render::ExtraType::PATCH);
m_extraCel->setBlendMode(m_site.layer()->isImage() ?
static_cast<LayerImage*>(m_site.layer())->blendMode():
doc::BlendMode::NORMAL);
}
else
m_extraCel.reset();
m_site.document()->setExtraCel(m_extraCel);
if (m_extraCel->image()) {
Image* dst = m_extraCel->image();
if (m_site.tilemapMode() == TilemapMode::Tiles) {
dst->setMaskColor(doc::notile);
dst->clear(dst->maskColor());
if (m_site.cel()) {
doc::Grid grid = m_site.grid();
dst->copy(m_site.cel()->image(),
gfx::Clip(0, 0, grid.canvasToTile(bounds)));
}
}
else {
dst->setMaskColor(m_site.sprite()->transparentColor());
dst->clear(dst->maskColor());
render::Render render;
render.renderLayer(
dst, m_site.layer(), m_site.frame(),
gfx::Clip(0, 0, bounds),
doc::BlendMode::SRC);
}
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,
const gfx::Rect& bounds)
{
ASSERT(dst);
if (!src) return;
if (m_site.tilemapMode() == TilemapMode::Tiles) {
gfx::Rect tilesBounds = m_site.grid().canvasToTile(bounds);
doc::algorithm::parallelogram(
dst, src, nullptr,
tilesBounds.x , tilesBounds.y,
tilesBounds.x+tilesBounds.w, tilesBounds.y,
tilesBounds.x+tilesBounds.w, tilesBounds.y+tilesBounds.h,
tilesBounds.x , tilesBounds.y+tilesBounds.h
);
}
else {
doc::algorithm::parallelogram(
dst, src, (mask ? mask->bitmap() : nullptr),
bounds.x , bounds.y,
bounds.x+bounds.w, bounds.y,
bounds.x+bounds.w, bounds.y+bounds.h,
bounds.x , bounds.y+bounds.h
);
}
}
bool MovingSliceState::onMouseMove(Editor* editor, MouseMessage* msg)
{
gfx::Point newCursorPos = editor->screenToEditor(msg->position());
gfx::Point delta = newCursorPos - m_mouseStart;
gfx::Rect totalBounds = selectedSlicesBounds();
// Move by tile size under Tiles mode.
if (editor->slicesTransforms() && m_site.tilemapMode() == TilemapMode::Tiles) {
delta = m_site.grid().tileToCanvas(m_site.grid().canvasToTile(delta));
}
ASSERT(totalBounds.w > 0);
ASSERT(totalBounds.h > 0);
@ -153,15 +405,27 @@ bool MovingSliceState::onMouseMove(Editor* editor, MouseMessage* msg)
}
}
// Align slice origin to tiles origin under Tiles mode.
if (editor->slicesTransforms() && m_site.tilemapMode() == TilemapMode::Tiles) {
rc.setOrigin(m_site.grid().tileToCanvas(m_site.grid().canvasToTile(rc.origin())));
}
if (m_hit.type() == EditorHit::SliceCenter)
key.setCenter(rc);
else
else {
key.setBounds(rc);
if (item.isNineSlice()) {
key.setBorder(item.border());
}
}
// Update the slice key
item.slice->insert(m_frame, key);
}
if (editor->slicesTransforms())
drawExtraCel();
// Redraw the editor.
editor->invalidate();
@ -205,7 +469,7 @@ MovingSliceState::Item MovingSliceState::getItemForSlice(doc::Slice* slice)
Item item;
item.slice = slice;
auto keyPtr = slice->getByFrame(m_frame);
const auto* keyPtr = slice->getByFrame(m_frame);
ASSERT(keyPtr);
if (keyPtr)
item.oldKey = item.newKey = *keyPtr;
@ -221,4 +485,19 @@ gfx::Rect MovingSliceState::selectedSlicesBounds() const
return bounds;
}
void MovingSliceState::clearSlices()
{
ContextWriter writer(UIContext::instance(), 1000);
if (writer.cel()) {
std::vector<SliceKey> slicesKeys;
slicesKeys.reserve(m_items.size());
for (auto& item : m_items) {
slicesKeys.push_back(item.newKey);
}
CmdTransaction* cmds = m_tx;
cmds->executeAndAdd(new cmd::ClearSlices(m_site, m_selectedLayers, m_frame, slicesKeys));
}
}
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2017 David Capello
//
// This program is distributed under the terms of
@ -11,9 +11,15 @@
#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"
#include "doc/selected_layers.h"
#include "doc/selected_objects.h"
#include "doc/slice.h"
#include "gfx/border.h"
namespace app {
class Editor;
@ -25,6 +31,7 @@ namespace app {
const EditorHit& hit,
const doc::SelectedObjects& selectedSlices);
void onEnterState(Editor* editor) override;
bool onMouseUp(Editor* editor, ui::MouseMessage* msg) override;
bool onMouseMove(Editor* editor, ui::MouseMessage* msg) override;
bool onSetCursor(Editor* editor, const gfx::Point& mouseScreenPos) override;
@ -32,19 +39,205 @@ namespace app {
bool requireBrushPreview() override { return false; }
private:
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;
// Vector of each selected layer's part of the sprite under
// the slice bounds that will be transformed when Slice Transform is
// enabled. Contains one ItemContentRef by layer.
std::vector<ItemContentRef> content;
// Part of the sprite of each selected layer's merged into one image per
// slice. This is used to give feedback to the users when they are
// transforming the selected slices.
ItemContentRef mergedContent;
// Adds image to the content vector. The image should correspond to some
// part of a single layer cel's image.
// Internally this method builds a merged version of the images to speed
// up the drawing when the user updates this Item's slice in real time.
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));
}
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);
}
bool isNineSlice() const { return oldKey.hasCenter(); }
// If this item manages a 9-slice key, returns the border surrounding the
// center rectangle of the SliceKey. Otherwise returns an empty border.
gfx::Border border() const {
gfx::Border border;
if (isNineSlice()) {
border = gfx::Border(oldKey.center().x,
oldKey.center().y,
oldKey.bounds().w-oldKey.center().x2(),
oldKey.bounds().h-oldKey.center().y2());
}
return border;
}
};
// 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 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,
const gfx::Rect& bounds);
void stampExtraCelImage();
void clearSlices();
doc::frame_t m_frame;
EditorHit m_hit;
gfx::Point m_mouseStart;
std::vector<Item> m_items;
LayerList m_selectedLayers;
Site m_site;
ExtraCelRef m_extraCel = nullptr;
Tx m_tx;
};
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2020-2024 Igara Studio S.A.
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
@ -37,63 +37,7 @@ void select_layer_boundaries(Layer* layer,
const Cel* cel = layer->cel(frame);
if (cel) {
const Image* image = cel->image();
if (image) {
newMask.replace(cel->bounds());
newMask.freeze();
{
LockImageBits<BitmapTraits> maskBits(newMask.bitmap());
auto maskIt = maskBits.begin();
auto maskEnd = maskBits.end();
switch (image->pixelFormat()) {
case IMAGE_RGB: {
LockImageBits<RgbTraits> rgbBits(image);
auto rgbIt = rgbBits.begin();
#if _DEBUG
auto rgbEnd = rgbBits.end();
#endif
for (; maskIt != maskEnd; ++maskIt, ++rgbIt) {
ASSERT(rgbIt != rgbEnd);
color_t c = *rgbIt;
*maskIt = (rgba_geta(c) >= 128); // TODO configurable threshold
}
break;
}
case IMAGE_GRAYSCALE: {
LockImageBits<GrayscaleTraits> grayBits(image);
auto grayIt = grayBits.begin();
#if _DEBUG
auto grayEnd = grayBits.end();
#endif
for (; maskIt != maskEnd; ++maskIt, ++grayIt) {
ASSERT(grayIt != grayEnd);
color_t c = *grayIt;
*maskIt = (graya_geta(c) >= 128); // TODO configurable threshold
}
break;
}
case IMAGE_INDEXED: {
const doc::color_t maskColor = image->maskColor();
LockImageBits<IndexedTraits> idxBits(image);
auto idxIt = idxBits.begin();
#if _DEBUG
auto idxEnd = idxBits.end();
#endif
for (; maskIt != maskEnd; ++maskIt, ++idxIt) {
ASSERT(idxIt != idxEnd);
color_t c = *idxIt;
*maskIt = (c != maskColor);
}
break;
}
}
}
newMask.unfreeze();
}
newMask.fromImage(image, cel->bounds().origin(), 128); // TODO configurable alpha threshold
}
try {

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -80,7 +80,81 @@ doc::Image* new_image_from_mask(const Site& site,
}
// Copy the masked zones
copy_masked_zones(dst.get(), src, srcMask, x, y);
return dst.release();
}
doc::Image* new_image_from_mask(const Layer& layer,
frame_t frame,
const doc::Mask* srcMask,
const bool newBlend)
{
const Sprite* srcSprite = layer.sprite();
ASSERT(srcSprite);
ASSERT(srcMask);
const Image* srcMaskBitmap = srcMask->bitmap();
const gfx::Rect& srcBounds = srcMask->bounds();
ASSERT(srcMaskBitmap);
ASSERT(!srcBounds.isEmpty());
std::unique_ptr<Image> dst(Image::create(srcSprite->pixelFormat(), srcBounds.w, srcBounds.h));
if (!dst)
return nullptr;
// Clear the new image
dst->setMaskColor(srcSprite->transparentColor());
clear_image(dst.get(), dst->maskColor());
const Image* src = nullptr;
int x = 0, y = 0;
auto* cel = layer.cel(frame);
if (layer.isTilemap()) {
render::Render render;
render.setNewBlend(newBlend);
ASSERT(layer.isTilemap());
if (cel) {
render.renderCel(
dst.get(), cel, srcSprite,
cel->image(), cel->layer(),
srcSprite->palette(cel->frame()),
cel->bounds(),
gfx::Clip(0, 0, srcBounds),
255, BlendMode::NORMAL);
}
src = dst.get();
}
else {
if (cel) {
src = cel->image();
x = cel->x();
y = cel->y();
}
}
if (src)
// Copy the masked zones
copy_masked_zones(dst.get(), src, srcMask, x, y);
return dst.release();
}
void copy_masked_zones(Image* dst,
const Image* src,
const Mask* srcMask,
int srcXoffset, int srcYoffset)
{
ASSERT(srcMask);
if (src) {
const Image* srcMaskBitmap = srcMask->bitmap();
const gfx::Rect& srcBounds = srcMask->bounds();
ASSERT(srcMaskBitmap);
ASSERT(!srcBounds.isEmpty());
if (srcMaskBitmap) {
// Copy active layer with mask
const LockImageBits<BitmapTraits> maskBits(srcMaskBitmap, gfx::Rect(0, 0, srcBounds.w, srcBounds.h));
@ -90,13 +164,15 @@ doc::Image* new_image_from_mask(const Site& site,
for (int u=0; u<srcBounds.w; ++u, ++mask_it) {
ASSERT(mask_it != maskBits.end());
if (src != dst.get()) {
if (src != dst) {
if (*mask_it) {
int getx = u+srcBounds.x-x;
int gety = v+srcBounds.y-y;
int getx = u+srcBounds.x-srcXoffset;
int gety = v+srcBounds.y-srcYoffset;
if ((getx >= 0) && (getx < src->width()) &&
(gety >= 0) && (gety < src->height()))
(gety >= 0) && (gety < src->height()) &&
(u < dst->width()) &&
(v < dst->height()))
dst->putPixel(u, v, src->getPixel(getx, gety));
}
}
@ -108,12 +184,10 @@ doc::Image* new_image_from_mask(const Site& site,
}
}
}
else if (src != dst.get()) {
copy_image(dst.get(), src, -srcBounds.x, -srcBounds.y);
else if (src != dst) {
copy_image(dst, src, -srcBounds.x, -srcBounds.y);
}
}
return dst.release();
}
doc::Image* new_tilemap_from_mask(const Site& site,

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -9,9 +9,12 @@
#define APP_UTIL_NEW_IMAGE_FROM_MASK_H_INCLUDED
#pragma once
#include "doc/frame.h"
namespace doc {
class Image;
class Mask;
class Layer;
}
namespace app {
@ -23,9 +26,18 @@ namespace app {
const doc::Mask* mask,
const bool newBlend,
bool merged = false);
doc::Image* new_image_from_mask(const doc::Layer& layer,
doc::frame_t frame,
const doc::Mask* srcMask,
const bool newBlend);
doc::Image* new_tilemap_from_mask(const Site& site,
const doc::Mask* mask);
void copy_masked_zones(doc::Image* dst,
const doc::Image* src,
const doc::Mask* srcMask,
int srcXoffset, int srcYoffset);
} // namespace app
#endif

View File

@ -314,6 +314,25 @@ public:
}
};
class TilemapDelegate : public GenericDelegate<TilemapTraits> {
public:
TilemapDelegate(color_t mask_color) :
m_mask_color(mask_color) {
}
void putPixel(const Image* spr, int spr_x, int spr_y) {
ASSERT(m_it != m_end);
color_t c = get_pixel_fast<TilemapTraits>(spr, spr_x, spr_y);
if (c != m_mask_color)
*m_it = c;
}
private:
color_t m_mask_color;
};
/* _parallelogram_map:
* Worker routine for drawing rotated and/or scaled and/or flipped sprites:
* It actually maps the sprite to any parallelogram-shaped area of the
@ -773,6 +792,12 @@ static void ase_parallelogram_map_standard(
ase_parallelogram_map<BitmapTraits, BitmapDelegate>(bmp, sprite, mask, xs, ys, false, delegate);
break;
}
case IMAGE_TILEMAP: {
TilemapDelegate delegate(sprite->maskColor());
ase_parallelogram_map<TilemapTraits, TilemapDelegate>(bmp, sprite, mask, xs, ys, false, delegate);
break;
}
}
}

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -10,9 +10,8 @@
#endif
#include "doc/mask.h"
#include "base/memory.h"
#include "doc/image_impl.h"
#include "gfx/point.h"
#include <cstdlib>
#include <cstring>
@ -129,6 +128,67 @@ void Mask::copyFrom(const Mask* sourceMask)
}
}
void Mask::fromImage(const Image* image, const gfx::Point& maskOrigin, uint8_t alphaThreshold)
{
if (image) {
replace(image->bounds().setOrigin(maskOrigin));
freeze();
{
LockImageBits<BitmapTraits> maskBits(bitmap());
auto maskIt = maskBits.begin();
auto maskEnd = maskBits.end();
switch (image->pixelFormat()) {
case IMAGE_RGB: {
LockImageBits<RgbTraits> rgbBits(image);
auto rgbIt = rgbBits.begin();
#if _DEBUG
auto rgbEnd = rgbBits.end();
#endif
for (; maskIt != maskEnd; ++maskIt, ++rgbIt) {
ASSERT(rgbIt != rgbEnd);
color_t c = *rgbIt;
*maskIt = (rgba_geta(c) > alphaThreshold);
}
break;
}
case IMAGE_GRAYSCALE: {
LockImageBits<GrayscaleTraits> grayBits(image);
auto grayIt = grayBits.begin();
#if _DEBUG
auto grayEnd = grayBits.end();
#endif
for (; maskIt != maskEnd; ++maskIt, ++grayIt) {
ASSERT(grayIt != grayEnd);
color_t c = *grayIt;
*maskIt = (graya_geta(c) > alphaThreshold);
}
break;
}
case IMAGE_INDEXED: {
const doc::color_t maskColor = image->maskColor();
LockImageBits<IndexedTraits> idxBits(image);
auto idxIt = idxBits.begin();
#if _DEBUG
auto idxEnd = idxBits.end();
#endif
for (; maskIt != maskEnd; ++maskIt, ++idxIt) {
ASSERT(idxIt != idxEnd);
color_t c = *idxIt;
*maskIt = (c != maskColor);
}
break;
}
}
}
unfreeze();
}
}
void Mask::offsetOrigin(int dx, int dy)
{
m_bounds.offset(dx, dy);

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2020 Igara Studio S.A.
// Copyright (c) 2020-2024 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -76,6 +76,7 @@ namespace doc {
// Copies the data from the given mask.
void copyFrom(const Mask* sourceMask);
void fromImage(const Image* image, const gfx::Point& maskOrigin, uint8_t alphaThreshold = 0);
// Replace the whole mask with the given region.
void replace(const gfx::Rect& bounds);
@ -118,6 +119,8 @@ namespace doc {
Mask& operator=(const Mask& mask);
};
typedef std::shared_ptr<Mask> MaskRef;
} // namespace doc
#endif

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -12,6 +12,7 @@
#include "doc/frame.h"
#include "doc/keyframes.h"
#include "doc/with_user_data.h"
#include "gfx/border.h"
#include "gfx/point.h"
#include "gfx/rect.h"
@ -41,6 +42,12 @@ namespace doc {
void setBounds(const gfx::Rect& bounds) { m_bounds = bounds; }
void setCenter(const gfx::Rect& center) { m_center = center; }
void setPivot(const gfx::Point& pivot) { m_pivot = pivot; }
void setBorder(const gfx::Border& border) {
m_center.x = border.left();
m_center.y = border.top();
m_center.w = m_bounds.w - border.width();
m_center.h = m_bounds.h - border.height();
}
private:
gfx::Rect m_bounds;