Add pixel-perfect drawing mode for freehand tools (issue 315)

* Added new app::tools::IntertwineAsPixelPerfect intertwiner
* Added app::tools::FreehandAlgorithm enum
* Added app::tools::WellKnownIntertwiners constants
This commit is contained in:
David Capello 2014-01-25 16:26:49 -03:00
parent f19aae9232
commit 1554875618
14 changed files with 212 additions and 7 deletions

View File

@ -0,0 +1,31 @@
/* Aseprite
* Copyright (C) 2001-2013 David Capello
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifndef APP_SETTINGS_FREEHAND_ALGORITHM_H_INCLUDED
#define APP_SETTINGS_FREEHAND_ALGORITHM_H_INCLUDED
namespace app {
enum FreehandAlgorithm {
kDefaultFreehandAlgorithm,
kPixelPerfectFreehandAlgorithm,
};
} // namespace app
#endif // APP_SETTINGS_FREEHAND_ALGORITHM_H_INCLUDED

View File

@ -41,4 +41,4 @@ namespace app {
} // namespace app
#endif // SETTINGS_INK_TYPE_H_INCLUDED
#endif // APP_SETTINGS_INK_TYPE_H_INCLUDED

View File

@ -20,6 +20,7 @@
#define APP_SETTINGS_SETTINGS_H_INCLUDED
#include "app/color.h"
#include "app/settings/freehand_algorithm.h"
#include "app/settings/ink_type.h"
#include "app/settings/rotation_algorithm.h"
#include "gfx/point.h"
@ -91,6 +92,7 @@ namespace app {
virtual int getSprayWidth() = 0;
virtual int getSpraySpeed() = 0;
virtual InkType getInkType() = 0;
virtual FreehandAlgorithm getFreehandAlgorithm() = 0;
virtual void setOpacity(int opacity) = 0;
virtual void setTolerance(int tolerance) = 0;
@ -99,6 +101,7 @@ namespace app {
virtual void setSprayWidth(int width) = 0;
virtual void setSpraySpeed(int speed) = 0;
virtual void setInkType(InkType inkType) = 0;
virtual void setFreehandAlgorithm(FreehandAlgorithm algorithm) = 0;
virtual void addObserver(ToolSettingsObserver* observer) = 0;
virtual void removeObserver(ToolSettingsObserver* observer) = 0;

View File

@ -20,6 +20,7 @@
#define APP_SETTINGS_SETTINGS_OBSERVERS_H_INCLUDED
#include "app/color.h"
#include "app/settings/freehand_algorithm.h"
#include "app/settings/ink_type.h"
#include "app/settings/rotation_algorithm.h"
#include "raster/pen_type.h"
@ -51,6 +52,7 @@ namespace app {
virtual void onSetSprayWidth(int newSprayWidth) {}
virtual void onSetSpraySpeed(int newSpraySpeed) {}
virtual void onSetInkType(InkType newInkType) {}
virtual void onSetFreehandAlgorithm(FreehandAlgorithm algorithm) {}
};
class SelectionSettingsObserver {

View File

@ -26,6 +26,7 @@
#include "app/color_swatches.h"
#include "app/ini_file.h"
#include "app/settings/document_settings.h"
#include "app/tools/controller.h"
#include "app/tools/point_shape.h"
#include "app/tools/tool.h"
#include "app/tools/tool_box.h"
@ -537,6 +538,7 @@ class UIToolSettingsImpl
int m_spray_width;
int m_spray_speed;
InkType m_inkType;
FreehandAlgorithm m_freehandAlgorithm;
public:
@ -554,6 +556,7 @@ public:
m_spray_width = 16;
m_spray_speed = 32;
m_inkType = (InkType)get_config_int(cfg_section.c_str(), "InkType", (int)kDefaultInk);
m_freehandAlgorithm = kDefaultFreehandAlgorithm;
m_pen.enableSignals(false);
m_pen.setType((PenType)get_config_int(cfg_section.c_str(), "PenType", (int)PEN_TYPE_CIRCLE));
@ -566,6 +569,12 @@ public:
m_spray_width = get_config_int(cfg_section.c_str(), "SprayWidth", m_spray_width);
m_spray_speed = get_config_int(cfg_section.c_str(), "SpraySpeed", m_spray_speed);
}
if (m_tool->getController(0)->isFreehand() ||
m_tool->getController(1)->isFreehand()) {
m_freehandAlgorithm = (FreehandAlgorithm)get_config_int(cfg_section.c_str(), "FreehandAlgorithm", (int)kDefaultFreehandAlgorithm);
setFreehandAlgorithm(m_freehandAlgorithm);
}
}
~UIToolSettingsImpl()
@ -586,6 +595,11 @@ public:
set_config_int(cfg_section.c_str(), "SpraySpeed", m_spray_speed);
}
if (m_tool->getController(0)->isFreehand() ||
m_tool->getController(1)->isFreehand()) {
set_config_int(cfg_section.c_str(), "FreehandAlgorithm", m_freehandAlgorithm);
}
set_config_bool(cfg_section.c_str(), "PreviewFilled", m_previewFilled);
}
@ -598,6 +612,7 @@ public:
int getSprayWidth() OVERRIDE { return m_spray_width; }
int getSpraySpeed() OVERRIDE { return m_spray_speed; }
InkType getInkType() OVERRIDE { return m_inkType; }
FreehandAlgorithm getFreehandAlgorithm() OVERRIDE { return m_freehandAlgorithm; }
void setOpacity(int opacity) OVERRIDE { m_opacity = opacity; }
void setTolerance(int tolerance) OVERRIDE { m_tolerance = tolerance; }
@ -606,6 +621,21 @@ public:
void setSprayWidth(int width) OVERRIDE { m_spray_width = width; }
void setSpraySpeed(int speed) OVERRIDE { m_spray_speed = speed; }
void setInkType(InkType inkType) OVERRIDE { m_inkType = inkType; }
void setFreehandAlgorithm(FreehandAlgorithm algorithm) OVERRIDE {
m_freehandAlgorithm = algorithm;
tools::ToolBox* toolBox = App::instance()->getToolBox();
for (int i=0; i<2; ++i) {
if (algorithm == kPixelPerfectFreehandAlgorithm) {
m_tool->setIntertwine(i, toolBox->getIntertwinerById(tools::WellKnownIntertwiners::AsPixelPerfect));
m_tool->setTracePolicy(i, tools::TracePolicyLast);
}
else {
m_tool->setIntertwine(i, toolBox->getIntertwinerById(tools::WellKnownIntertwiners::AsLines));
m_tool->setTracePolicy(i, tools::TracePolicyAccumulate);
}
}
}
void addObserver(ToolSettingsObserver* observer) OVERRIDE {
base::Observable<ToolSettingsObserver>::addObserver(observer);

View File

@ -35,7 +35,9 @@ namespace app {
typedef std::vector<gfx::Point> Points;
virtual ~Controller() { }
virtual bool canSnapToGrid() { return true; }
virtual bool isFreehand() { return false; }
// Called when the user starts drawing and each time a new button is
// pressed. The controller could be sure that this method is called

View File

@ -31,6 +31,10 @@ using namespace gfx;
// Controls clicks for tools like pencil
class FreehandController : public Controller {
public:
bool isFreehand()
{
return true;
}
void pressButton(Points& points, const Point& point)
{
points.push_back(point);

View File

@ -33,6 +33,7 @@ namespace app {
virtual ~Intertwine() { }
virtual bool snapByAngle() { return false; }
virtual void prepareIntertwine() { }
virtual void joinPoints(ToolLoop* loop, const Points& points) = 0;
virtual void fillPoints(ToolLoop* loop, const Points& points) = 0;

View File

@ -220,5 +220,80 @@ public:
}
};
class IntertwineAsPixelPerfect : public Intertwine {
struct PPData {
Points& pts;
ToolLoop* loop;
PPData(Points& pts, ToolLoop* loop) : pts(pts), loop(loop) { }
};
static void pixelPerfectLine(int x, int y, PPData* data)
{
gfx::Point newPoint(x, y);
if (data->pts.empty()
|| data->pts[data->pts.size()-1] != newPoint) {
data->pts.push_back(newPoint);
}
}
Points m_pts;
public:
void prepareIntertwine() OVERRIDE {
m_pts.clear();
}
void joinPoints(ToolLoop* loop, const Points& points) OVERRIDE {
if (points.size() == 0)
return;
else if (m_pts.empty() && points.size() == 1) {
m_pts = points;
}
else {
PPData data(m_pts, loop);
for (size_t c=0; c+1<points.size(); ++c) {
int x1 = points[c].x;
int y1 = points[c].y;
int x2 = points[c+1].x;
int y2 = points[c+1].y;
algo_line(x1, y1, x2, y2,
(void*)&data,
(AlgoPixel)&IntertwineAsPixelPerfect::pixelPerfectLine);
}
}
for (size_t c=0; c<m_pts.size(); ++c) {
// We ignore a pixel that is between other two pixels in the
// corner of a L-like shape.
if (c > 0 && c+1 < m_pts.size()
&& (m_pts[c-1].x == m_pts[c].x || m_pts[c-1].y == m_pts[c].y)
&& (m_pts[c+1].x == m_pts[c].x || m_pts[c+1].y == m_pts[c].y)
&& m_pts[c-1].x != m_pts[c+1].x
&& m_pts[c-1].y != m_pts[c+1].y) {
++c;
}
doPointshapePoint(m_pts[c].x, m_pts[c].y, loop);
}
}
void fillPoints(ToolLoop* loop, const Points& points)
{
if (points.size() < 3) {
joinPoints(loop, points);
return;
}
// Contour
joinPoints(loop, points);
// Fill content
algo_polygon(points.size(), (const int*)&points[0], loop, (AlgoHLine)doPointshapeHline);
}
};
} // namespace tools
} // namespace app

View File

@ -69,6 +69,13 @@ const char* WellKnownInks::Move = "move";
const char* WellKnownInks::Blur = "blur";
const char* WellKnownInks::Jumble = "jumble";
const char* WellKnownIntertwiners::None = "none";
const char* WellKnownIntertwiners::AsLines = "as_lines";
const char* WellKnownIntertwiners::AsRectangles = "as_rectangles";
const char* WellKnownIntertwiners::AsEllipses = "as_ellipses";
const char* WellKnownIntertwiners::AsBezier = "as_bezier";
const char* WellKnownIntertwiners::AsPixelPerfect = "as_pixel_perfect";
ToolBox::ToolBox()
{
PRINTF("Toolbox module: installing\n");
@ -102,11 +109,12 @@ ToolBox::ToolBox()
m_pointshapers["floodfill"] = new FloodFillPointShape();
m_pointshapers["spray"] = new SprayPointShape();
m_intertwiners["none"] = new IntertwineNone();
m_intertwiners["as_lines"] = new IntertwineAsLines();
m_intertwiners["as_rectangles"] = new IntertwineAsRectangles();
m_intertwiners["as_ellipses"] = new IntertwineAsEllipses();
m_intertwiners["as_bezier"] = new IntertwineAsBezier();
m_intertwiners[WellKnownIntertwiners::None] = new IntertwineNone();
m_intertwiners[WellKnownIntertwiners::AsLines] = new IntertwineAsLines();
m_intertwiners[WellKnownIntertwiners::AsRectangles] = new IntertwineAsRectangles();
m_intertwiners[WellKnownIntertwiners::AsEllipses] = new IntertwineAsEllipses();
m_intertwiners[WellKnownIntertwiners::AsBezier] = new IntertwineAsBezier();
m_intertwiners[WellKnownIntertwiners::AsPixelPerfect] = new IntertwineAsPixelPerfect();
loadTools();
@ -152,9 +160,14 @@ Ink* ToolBox::getInkById(const std::string& id)
return m_inks[id];
}
Intertwine* ToolBox::getIntertwinerById(const std::string& id)
{
return m_intertwiners[id];
}
void ToolBox::loadTools()
{
PRINTF("Loading ASEPRITE tools\n");
PRINTF("Loading Aseprite tools\n");
XmlDocumentRef doc(GuiXml::instance()->doc());
TiXmlHandle handle(doc);

View File

@ -53,6 +53,15 @@ namespace app {
extern const char* Jumble;
};
namespace WellKnownIntertwiners {
extern const char* None;
extern const char* AsLines;
extern const char* AsRectangles;
extern const char* AsEllipses;
extern const char* AsBezier;
extern const char* AsPixelPerfect;
};
typedef std::list<Tool*> ToolList;
typedef ToolList::iterator ToolIterator;
typedef ToolList::const_iterator ToolConstIterator;
@ -75,6 +84,7 @@ namespace app {
Tool* getToolById(const std::string& id);
Ink* getInkById(const std::string& id);
Intertwine* getIntertwinerById(const std::string& id);
int getGroupsCount() const { return m_groups.size(); }
private:

View File

@ -67,6 +67,7 @@ void ToolLoopManager::prepareLoop(const Pointer& pointer)
// Prepare the ink
m_toolLoop->getInk()->prepareInk(m_toolLoop);
m_toolLoop->getIntertwine()->prepareIntertwine();
// Prepare preview image (the destination image will be our preview
// in the tool-loop time, so we can see what we are drawing)

View File

@ -27,9 +27,11 @@
#include "app/settings/ink_type.h"
#include "app/settings/settings.h"
#include "app/settings/settings_observers.h"
#include "app/tools/controller.h"
#include "app/tools/ink.h"
#include "app/tools/point_shape.h"
#include "app/tools/tool.h"
#include "app/tools/tool_box.h"
#include "app/ui/button_set.h"
#include "app/ui/color_button.h"
#include "app/ui/skin/skin_theme.h"
@ -387,6 +389,24 @@ private:
};
};
class ContextBar::FreehandAlgorithmField : public CheckBox
{
public:
FreehandAlgorithmField() : CheckBox("Pixel-perfect") {
}
void onClick(Event& ev) OVERRIDE {
CheckBox::onClick(ev);
ISettings* settings = UIContext::instance()->getSettings();
Tool* currentTool = settings->getCurrentTool();
settings->getToolSettings(currentTool)
->setFreehandAlgorithm(isSelected() ?
kPixelPerfectFreehandAlgorithm:
kDefaultFreehandAlgorithm);
}
};
ContextBar::ContextBar()
: Box(JI_HORIZONTAL)
{
@ -412,6 +432,9 @@ ContextBar::ContextBar()
// addChild(new InkShadeField());
// addChild(new InkSelectionField());
addChild(m_freehandBox = new HBox());
m_freehandBox->addChild(m_freehandAlgo = new FreehandAlgorithmField());
addChild(m_sprayBox = new HBox());
m_sprayBox->addChild(new Label("Spray:"));
m_sprayBox->addChild(m_sprayWidth = new SprayWidthField());
@ -488,6 +511,8 @@ void ContextBar::onCurrentToolChange()
m_inkType->setInkType(toolSettings->getInkType());
m_inkOpacity->setTextf("%d", toolSettings->getOpacity());
m_freehandAlgo->setSelected(toolSettings->getFreehandAlgorithm() == kPixelPerfectFreehandAlgorithm);
m_sprayWidth->setValue(toolSettings->getSprayWidth());
m_spraySpeed->setValue(toolSettings->getSpraySpeed());
@ -512,6 +537,10 @@ void ContextBar::onCurrentToolChange()
bool hasSelectOptions = (currentTool->getInk(0)->isSelection() ||
currentTool->getInk(1)->isSelection());
bool isFreehand =
(currentTool->getController(0)->isFreehand() ||
currentTool->getController(1)->isFreehand());
// Show/Hide fields
m_brushLabel->setVisible(hasOpacity);
m_brushType->setVisible(hasOpacity);
@ -521,6 +550,7 @@ void ContextBar::onCurrentToolChange()
m_inkLabel->setVisible(hasInk);
m_inkType->setVisible(hasInk);
m_inkOpacity->setVisible(hasOpacity);
m_freehandBox->setVisible(isFreehand);
m_toleranceLabel->setVisible(hasTolerance);
m_tolerance->setVisible(hasTolerance);
m_sprayBox->setVisible(hasSprayOptions);

View File

@ -52,6 +52,7 @@ namespace app {
class SpraySpeedField;
class TransparentColorField;
class RotAlgorithmField;
class FreehandAlgorithmField;
ui::Label* m_brushLabel;
BrushTypeField* m_brushType;
@ -63,6 +64,8 @@ namespace app {
InkTypeField* m_inkType;
ui::Label* m_opacityLabel;
InkOpacityField* m_inkOpacity;
ui::Box* m_freehandBox;
FreehandAlgorithmField* m_freehandAlgo;
ui::Box* m_sprayBox;
SprayWidthField* m_sprayWidth;
SpraySpeedField* m_spraySpeed;