Add Edit > FXs > Outline command (fix #371, #1198)

This commit is contained in:
David Capello 2019-03-11 15:30:55 -03:00
parent db348ea3b5
commit 9cf408541f
9 changed files with 507 additions and 1 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Aseprite -->
<!-- Copyright (C) 2018 Igara Studio S.A. -->
<!-- Copyright (C) 2018-2019 Igara Studio S.A. -->
<!-- Copyright (C) 2001-2018 David Capello -->
<gui version="1.3-dev">
<!-- Keyboard shortcuts -->
@ -53,6 +53,7 @@
<key command="ReplaceColor" shortcut="Shift+R" />
<key command="HueSaturation" shortcut="Ctrl+U" mac="Cmd+U" />
<key command="ConvolutionMatrix" shortcut="F9" />
<key command="Outline" shortcut="Shift+O" />
<key command="ColorCurve" shortcut="Ctrl+M" />
<key command="ColorCurve" shortcut="F10" />
<key command="PasteText" shortcut="T" />
@ -680,6 +681,7 @@
<item command="ColorCurve" text="@.edit_adjustments_color_curve" />
</menu>
<menu text="@.edit_fx" id="fx_popup_menu">
<item command="Outline" text="@.edit_fx_outline" />
<item command="ConvolutionMatrix" text="@.edit_fx_convolution_matrix" />
<item command="Despeckle" text="@.edit_fx_despeckle" />
</menu>

View File

@ -240,6 +240,7 @@ CloseFile = Close File
ColorCurve = Color Curve
ColorQuantization = Create Palette from Current Sprite (Color Quantization)
ContiguousFill = Switch Contiguous Fill
Outline = Outline
ConvolutionMatrix = Convolution Matrix
Copy = Copy
CopyCel = Copy Cel
@ -716,6 +717,7 @@ edit_adjustments_brightness_contrast = &Brightness/Contrast...
edit_adjustments_hue_saturation = &Hue/Saturation...
edit_adjustments_color_curve = &Color Curve...
edit_fx = F&X
edit_fx_outline = &Outline
edit_fx_convolution_matrix = Convolution &Matrix...
edit_fx_despeckle = &Despeckle (Median Filter)...
edit_insert_text = Insert Text
@ -1107,6 +1109,14 @@ ok = &OK
apply = &Apply
cancel = &Cancel
[outline]
color = Outline Color:
outside = Outside
inside = Inside
circle = Circle
square = Square
bg_color = Background Color:
[palette_from_sprite]
title = Palette from Sprite
new_palette = Create a new palette with a specific number of colors (or less):

22
data/widgets/outline.xml Normal file
View File

@ -0,0 +1,22 @@
<!-- Aseprite -->
<!-- Copyright (C) 2019 by Igara Studio S.A. -->
<gui>
<vbox id="outline" expansive="true">
<grid columns="2">
<label text="@.color" />
<colorpicker id="color" cell_align="horizontal" />
<label text="@.bg_color" />
<colorpicker id="bg_color" cell_align="horizontal" />
</grid>
<hbox>
<vbox>
<radio id="circle" text="@.circle" group="1" />
<radio id="square" text="@.square" group="1" />
</vbox>
<vbox>
<radio id="outside" text="@.outside" group="2" />
<radio id="inside" text="@.inside" group="2" />
</vbox>
</hbox>
</vbox>
</gui>

View File

@ -293,6 +293,7 @@ if(ENABLE_UI)
commands/filters/cmd_despeckle.cpp
commands/filters/cmd_hue_saturation.cpp
commands/filters/cmd_invert_color.cpp
commands/filters/cmd_outline.cpp
commands/filters/cmd_replace_color.cpp
commands/filters/color_curve_editor.cpp
commands/filters/convolution_matrix_stock.cpp

View File

@ -105,6 +105,7 @@ FOR_EACH_COMMAND(OpenGroup)
FOR_EACH_COMMAND(OpenInFolder)
FOR_EACH_COMMAND(OpenWithApp)
FOR_EACH_COMMAND(Options)
FOR_EACH_COMMAND(Outline)
FOR_EACH_COMMAND(PaletteEditor)
FOR_EACH_COMMAND(Paste)
FOR_EACH_COMMAND(PasteText)

View File

@ -0,0 +1,181 @@
// Aseprite
// Copyright (C) 2019 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/color.h"
#include "app/color_utils.h"
#include "app/commands/command.h"
#include "app/commands/filters/filter_manager_impl.h"
#include "app/commands/filters/filter_window.h"
#include "app/context.h"
#include "app/ini_file.h"
#include "app/modules/gui.h"
#include "app/pref/preferences.h"
#include "app/ui/color_bar.h"
#include "app/ui/color_button.h"
#include "base/bind.h"
#include "doc/image.h"
#include "doc/mask.h"
#include "doc/sprite.h"
#include "filters/outline_filter.h"
#include "ui/button.h"
#include "ui/label.h"
#include "ui/slider.h"
#include "ui/widget.h"
#include "ui/window.h"
#include "outline.xml.h"
namespace app {
static const char* ConfigSection = "Outline";
// Wrapper for ReplaceColorFilter to handle colors in an easy way
class OutlineFilterWrapper : public OutlineFilter {
public:
OutlineFilterWrapper(Layer* layer) : m_layer(layer) { }
void color(const app::Color& color) {
m_color = color;
if (m_layer)
OutlineFilter::color(color_utils::color_for_layer(color, m_layer));
}
void bgColor(const app::Color& color) {
m_bgColor = color;
if (m_layer)
OutlineFilter::bgColor(color_utils::color_for_layer(color, m_layer));
}
app::Color color() const { return m_color; }
app::Color bgColor() const { return m_bgColor; }
private:
Layer* m_layer;
app::Color m_color;
app::Color m_bgColor;
};
class OutlineWindow : public FilterWindow {
public:
OutlineWindow(OutlineFilterWrapper& filter,
FilterManagerImpl& filterMgr)
: FilterWindow("Outline", ConfigSection, &filterMgr,
WithChannelsSelector,
WithTiledCheckBox,
filter.tiledMode())
, m_filter(filter) {
getContainer()->addChild(&m_panel);
m_panel.color()->setColor(m_filter.color());
m_panel.bgColor()->setColor(m_filter.bgColor());
m_panel.outside()->setSelected(m_filter.place() == OutlineFilter::Place::Outside);
m_panel.inside()->setSelected(m_filter.place() == OutlineFilter::Place::Inside);
m_panel.circle()->setSelected(m_filter.shape() == OutlineFilter::Shape::Circle);
m_panel.square()->setSelected(m_filter.shape() == OutlineFilter::Shape::Square);
m_panel.color()->Change.connect(&OutlineWindow::onColorChange, this);
m_panel.bgColor()->Change.connect(&OutlineWindow::onBgColorChange, this);
m_panel.outside()->Click.connect([this](ui::Event&){ onPlaceChange(OutlineFilter::Place::Outside); });
m_panel.inside()->Click.connect([this](ui::Event&){ onPlaceChange(OutlineFilter::Place::Inside); });
m_panel.circle()->Click.connect([this](ui::Event&){ onShapeChange(OutlineFilter::Shape::Circle); });
m_panel.square()->Click.connect([this](ui::Event&){ onShapeChange(OutlineFilter::Shape::Square); });
}
private:
void onColorChange(const app::Color& color) {
m_filter.color(color);
restartPreview();
}
void onBgColorChange(const app::Color& color) {
m_filter.bgColor(color);
restartPreview();
}
void onPlaceChange(OutlineFilter::Place place) {
m_filter.place(place);
restartPreview();
}
void onShapeChange(OutlineFilter::Shape shape) {
m_filter.shape(shape);
restartPreview();
}
void setupTiledMode(TiledMode tiledMode) override {
m_filter.tiledMode(tiledMode);
}
OutlineFilterWrapper& m_filter;
gen::Outline m_panel;
};
class OutlineCommand : public Command {
public:
OutlineCommand();
protected:
bool onEnabled(Context* context) override;
void onExecute(Context* context) override;
};
OutlineCommand::OutlineCommand()
: Command(CommandId::Outline(), CmdRecordableFlag)
{
}
bool OutlineCommand::onEnabled(Context* context)
{
return context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
ContextFlags::HasActiveSprite);
}
void OutlineCommand::onExecute(Context* context)
{
Site site = context->activeSite();
DocumentPreferences& docPref = Preferences::instance()
.document(site.document());
OutlineFilterWrapper filter(site.layer());
filter.place((OutlineFilter::Place)get_config_int(ConfigSection, "Place", int(OutlineFilter::Place::Outside)));
filter.shape((OutlineFilter::Shape)get_config_int(ConfigSection, "Shape", int(OutlineFilter::Shape::Circle)));
filter.color(get_config_color(ConfigSection, "Color", ColorBar::instance()->getFgColor()));
filter.tiledMode(docPref.tiled.mode());
filter.bgColor(app::Color::fromMask());
if (site.layer() && site.layer()->isBackground() && site.image()) {
// TODO configure default pixel (same as Autocrop/Trim refpixel)
filter.bgColor(app::Color::fromImage(site.image()->pixelFormat(),
site.image()->getPixel(0, 0)));
}
FilterManagerImpl filterMgr(context, &filter);
filterMgr.setTarget(
site.sprite()->pixelFormat() == IMAGE_INDEXED ?
TARGET_INDEX_CHANNEL:
TARGET_RED_CHANNEL |
TARGET_GREEN_CHANNEL |
TARGET_BLUE_CHANNEL |
TARGET_GRAY_CHANNEL |
TARGET_ALPHA_CHANNEL);
OutlineWindow window(filter, filterMgr);
if (window.doModal()) {
set_config_int(ConfigSection, "Place", int(filter.place()));
set_config_int(ConfigSection, "Shape", int(filter.shape()));
set_config_color(ConfigSection, "Color", filter.color());
}
}
Command* CommandFactory::createOutlineCommand()
{
return new OutlineCommand;
}
} // namespace app

View File

@ -10,6 +10,7 @@ add_library(filters-lib
hue_saturation_filter.cpp
invert_color_filter.cpp
median_filter.cpp
outline_filter.cpp
replace_color_filter.cpp)
target_link_libraries(filters-lib

View File

@ -0,0 +1,236 @@
// Aseprite
// Copyright (C) 2019 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 "filters/outline_filter.h"
#include "doc/image.h"
#include "doc/palette.h"
#include "doc/rgbmap.h"
#include "filters/filter_indexed_data.h"
#include "filters/filter_manager.h"
#include "filters/neighboring_pixels.h"
#include <algorithm>
namespace filters {
using namespace doc;
namespace {
static const int kCircleMatrix = 0272;
static const int kSquareMatrix = 0777;
struct GetPixelsDelegate {
color_t bgColor;
int transparent; // Transparent pixels
int opaque; // Opaque pixels
int matrix;
int bit;
void init(color_t bgColor, OutlineFilter::Shape shape) {
this->bgColor = bgColor;
this->matrix = (shape == OutlineFilter::Shape::Circle ? kCircleMatrix:
kSquareMatrix);
}
void reset() {
transparent = opaque = 0;
bit = 1;
}
};
struct GetPixelsDelegateRgba : public GetPixelsDelegate {
void operator()(RgbTraits::pixel_t color) {
if (rgba_geta(color) == 0 || color == bgColor)
transparent += (matrix & bit ? 1: 0);
else
opaque += (matrix & bit ? 1: 0);
bit <<= 1;
}
};
struct GetPixelsDelegateGrayscale : public GetPixelsDelegate {
void operator()(GrayscaleTraits::pixel_t color) {
if (graya_geta(color) == 0 || color == bgColor)
transparent += (matrix & bit ? 1: 0);
else
opaque += (matrix & bit ? 1: 0);
bit <<= 1;
}
};
struct GetPixelsDelegateIndexed : public GetPixelsDelegate {
const Palette* pal;
GetPixelsDelegateIndexed(const Palette* pal) : pal(pal) { }
void operator()(IndexedTraits::pixel_t color) {
color_t rgba = pal->getEntry(color);
if (rgba_geta(rgba) == 0 || color == bgColor)
transparent += (matrix & bit ? 1: 0);
else
opaque += (matrix & bit ? 1: 0);
bit <<= 1;
}
};
}
OutlineFilter::OutlineFilter()
: m_place(Place::Outside)
, m_shape(Shape::Circle)
, m_tiledMode(TiledMode::NONE)
, m_color(0)
, m_bgColor(0)
{
}
const char* OutlineFilter::getName()
{
return "Outline";
}
void OutlineFilter::applyToRgba(FilterManager* filterMgr)
{
const Image* src = filterMgr->getSourceImage();
const uint32_t* src_address = (uint32_t*)filterMgr->getSourceAddress();
uint32_t* dst_address = (uint32_t*)filterMgr->getDestinationAddress();
int x = filterMgr->x();
const int x2 = x+filterMgr->getWidth();
const int y = filterMgr->y();
Target target = filterMgr->getTarget();
int r, g, b, a, n;
color_t c;
bool isTransparent;
GetPixelsDelegateRgba delegate;
delegate.init(m_bgColor, m_shape);
for (; x<x2; ++x, ++src_address, ++dst_address) {
if (filterMgr->skipPixel())
continue;
delegate.reset();
get_neighboring_pixels<RgbTraits>(src, x, y, 3, 3, 1, 1, m_tiledMode, delegate);
c = *src_address;
n = (m_place == Place::Outside ? delegate.opaque: delegate.transparent);
isTransparent = (rgba_geta(c) == 0 || c == m_bgColor);
if ((n >= 1) &&
((m_place == Place::Outside && isTransparent) ||
(m_place == Place::Inside && !isTransparent))) {
r = (target & TARGET_RED_CHANNEL ? rgba_getr(m_color): rgba_getr(c));
g = (target & TARGET_GREEN_CHANNEL ? rgba_getg(m_color): rgba_getg(c));
b = (target & TARGET_BLUE_CHANNEL ? rgba_getb(m_color): rgba_getb(c));
a = (target & TARGET_ALPHA_CHANNEL ? rgba_geta(m_color): rgba_geta(c));
c = rgba(r, g, b, a);
}
*dst_address = c;
}
}
void OutlineFilter::applyToGrayscale(FilterManager* filterMgr)
{
const Image* src = filterMgr->getSourceImage();
const uint16_t* src_address = (uint16_t*)filterMgr->getSourceAddress();
uint16_t* dst_address = (uint16_t*)filterMgr->getDestinationAddress();
int x = filterMgr->x();
const int x2 = x+filterMgr->getWidth();
const int y = filterMgr->y();
Target target = filterMgr->getTarget();
int k, a, n;
color_t c;
bool isTransparent;
GetPixelsDelegateGrayscale delegate;
delegate.init(m_bgColor, m_shape);
for (; x<x2; ++x, ++src_address, ++dst_address) {
if (filterMgr->skipPixel())
continue;
delegate.reset();
get_neighboring_pixels<GrayscaleTraits>(src, x, y, 3, 3, 1, 1, m_tiledMode, delegate);
c = *src_address;
n = (m_place == Place::Outside ? delegate.opaque: delegate.transparent);
isTransparent = (graya_geta(c) == 0 || c == m_bgColor);
if ((n >= 1) &&
((m_place == Place::Outside && isTransparent) ||
(m_place == Place::Inside && !isTransparent))) {
k = (target & TARGET_GRAY_CHANNEL ? graya_getv(m_color): graya_getv(c));
a = (target & TARGET_ALPHA_CHANNEL ? graya_geta(m_color): graya_geta(c));
c = graya(k, a);
}
*dst_address = c;
}
}
void OutlineFilter::applyToIndexed(FilterManager* filterMgr)
{
const Image* src = filterMgr->getSourceImage();
const uint8_t* src_address = (uint8_t*)filterMgr->getSourceAddress();
uint8_t* dst_address = (uint8_t*)filterMgr->getDestinationAddress();
const Palette* pal = filterMgr->getIndexedData()->getPalette();
const RgbMap* rgbmap = filterMgr->getIndexedData()->getRgbMap();
int x = filterMgr->x();
const int x2 = x+filterMgr->getWidth();
const int y = filterMgr->y();
Target target = filterMgr->getTarget();
int r, g, b, a, n;
color_t c;
bool isTransparent;
GetPixelsDelegateIndexed delegate(pal);
delegate.init(m_bgColor, m_shape);
for (; x<x2; ++x, ++src_address, ++dst_address) {
if (filterMgr->skipPixel())
continue;
delegate.reset();
get_neighboring_pixels<IndexedTraits>(src, x, y, 3, 3, 1, 1, m_tiledMode, delegate);
c = *src_address;
n = (m_place == Place::Outside ? delegate.opaque: delegate.transparent);
if (target & TARGET_INDEX_CHANNEL) {
isTransparent = (c == m_bgColor);
}
else {
isTransparent = (rgba_geta(pal->getEntry(c)) == 0 || c == m_bgColor);
}
if ((n >= 1) &&
((m_place == Place::Outside && isTransparent) ||
(m_place == Place::Inside && !isTransparent))) {
if (target & TARGET_INDEX_CHANNEL) {
c = m_color;
}
else {
c = pal->getEntry(c);
r = (target & TARGET_RED_CHANNEL ? rgba_getr(m_color): rgba_getr(c));
g = (target & TARGET_GREEN_CHANNEL ? rgba_getg(m_color): rgba_getg(c));
b = (target & TARGET_BLUE_CHANNEL ? rgba_getb(m_color): rgba_getb(c));
a = (target & TARGET_ALPHA_CHANNEL ? rgba_geta(m_color): rgba_geta(c));
c = rgbmap->mapColor(r, g, b, a);
}
}
*dst_address = c;
}
}
} // namespace filters

View File

@ -0,0 +1,52 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef FILTERS_INVERT_COLOR_FILTER_H_INCLUDED
#define FILTERS_INVERT_COLOR_FILTER_H_INCLUDED
#pragma once
#include "doc/color.h"
#include "filters/filter.h"
#include "filters/tiled_mode.h"
namespace filters {
class OutlineFilter : public Filter {
public:
enum class Place { Outside, Inside };
enum class Shape { Circle, Square };
OutlineFilter();
void place(const Place place) { m_place = place; }
void shape(const Shape shape) { m_shape = shape; }
void tiledMode(const TiledMode tiledMode) { m_tiledMode = m_tiledMode; }
void color(const doc::color_t color) { m_color = color; }
void bgColor(const doc::color_t color) { m_bgColor = color; }
Place place() const { return m_place; }
Shape shape() const { return m_shape; }
TiledMode tiledMode() const { return m_tiledMode; }
doc::color_t color() const { return m_color; }
doc::color_t bgColor() const { return m_bgColor; }
// Filter implementation
const char* getName();
void applyToRgba(FilterManager* filterMgr);
void applyToGrayscale(FilterManager* filterMgr);
void applyToIndexed(FilterManager* filterMgr);
private:
Place m_place;
Shape m_shape;
TiledMode m_tiledMode;
doc::color_t m_color;
doc::color_t m_bgColor;
};
} // namespace filters
#endif