mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-14 13:21:34 +00:00
Merge branch 'slices-transform-switch' into beta (#4533)
This commit is contained in:
commit
132405b3cd
@ -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)
|
||||
|
@ -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
|
||||
|
152
src/app/cmd/clear_slices.cpp
Normal file
152
src/app/cmd/clear_slices.cpp
Normal 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
|
78
src/app/cmd/clear_slices.h
Normal file
78
src/app/cmd/clear_slices.h
Normal 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
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user