Add/process dynamic data in/from stroke points (related to #710)

* Converted Stroke points from gfx::Point to Stroke::Pt
* Process gradient data to create shading betweens points or dithering
  with new doc::Brush patterns
This commit is contained in:
David Capello 2020-04-22 20:17:14 -03:00
parent 79f9e28ce8
commit 3c1ea2f407
21 changed files with 662 additions and 282 deletions

View File

@ -9,6 +9,7 @@
#define APP_TOOLS_CONTROLLER_H_INCLUDED
#pragma once
#include "app/tools/stroke.h"
#include "app/tools/trace_policy.h"
#include "gfx/point.h"
@ -18,7 +19,6 @@
namespace app {
namespace tools {
class Stroke;
class ToolLoop;
// This class controls user input.
@ -37,13 +37,13 @@ namespace app {
// pressed. The controller could be sure that this method is called
// at least one time. The point is a position relative to sprite
// bounds.
virtual void pressButton(ToolLoop* loop, Stroke& stroke, const gfx::Point& point) = 0;
virtual void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) = 0;
// Called each time a mouse button is released.
virtual bool releaseButton(Stroke& stroke, const gfx::Point& point) = 0;
virtual bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) = 0;
// Called when the mouse is moved.
virtual void movement(ToolLoop* loop, Stroke& stroke, const gfx::Point& point) = 0;
virtual void movement(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) = 0;
// The input and output strokes are relative to sprite coordinates.
virtual void getStrokeToInterwine(const Stroke& input, Stroke& output) = 0;
@ -51,7 +51,7 @@ namespace app {
// Last point used by this controller, useful to save the last
// point of a freehand tool.
virtual gfx::Point getLastPoint() const { return gfx::Point(0, 0); }
virtual Stroke::Pt getLastPoint() const { return gfx::Point(0, 0); }
// Special trace policy that can change in the middle of the
// ToolLoop. This is for LineFreehandController which uses a

View File

@ -21,23 +21,24 @@ using namespace gfx;
// using the space bar.
class MoveOriginCapability : public Controller {
public:
void pressButton(ToolLoop* loop, Stroke& stroke, const Point& point) override {
m_last = point;
void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override {
m_last = pt;
}
protected:
bool isMovingOrigin(ToolLoop* loop, Stroke& stroke, const Point& point) {
bool isMovingOrigin(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) {
bool used = false;
if (int(loop->getModifiers()) & int(ToolLoopModifiers::kMoveOrigin)) {
Point delta = (point - m_last);
Point delta(pt.x - m_last.x,
pt.y - m_last.y);
stroke.offset(delta);
onMoveOrigin(delta);
used = true;
}
m_last = point;
m_last = pt;
return used;
}
@ -48,7 +49,7 @@ protected:
private:
// Last known mouse position used to calculate delta values (dx, dy)
// with the new mouse position to displace all points.
Point m_last;
Stroke::Pt m_last;
};
// Controls clicks for tools like pencil
@ -56,20 +57,20 @@ class FreehandController : public Controller {
public:
bool isFreehand() override { return true; }
gfx::Point getLastPoint() const override { return m_last; }
Stroke::Pt getLastPoint() const override { return m_last; }
void pressButton(ToolLoop* loop, Stroke& stroke, const Point& point) override {
m_last = point;
stroke.addPoint(point);
void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override {
m_last = pt;
stroke.addPoint(pt);
}
bool releaseButton(Stroke& stroke, const Point& point) override {
bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override {
return false;
}
void movement(ToolLoop* loop, Stroke& stroke, const Point& point) override {
m_last = point;
stroke.addPoint(point);
void movement(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override {
m_last = pt;
stroke.addPoint(pt);
}
void getStrokeToInterwine(const Stroke& input, Stroke& output) override {
@ -102,7 +103,7 @@ public:
}
private:
Point m_last;
Stroke::Pt m_last;
};
// Controls clicks for tools like line
@ -110,29 +111,29 @@ class TwoPointsController : public MoveOriginCapability {
public:
bool isTwoPoints() override { return true; }
void pressButton(ToolLoop* loop, Stroke& stroke, const Point& point) override {
MoveOriginCapability::pressButton(loop, stroke, point);
void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override {
MoveOriginCapability::pressButton(loop, stroke, pt);
m_first = m_center = point;
m_first = m_center = pt;
m_angle = 0.0;
stroke.addPoint(point);
stroke.addPoint(point);
stroke.addPoint(pt);
stroke.addPoint(pt);
if (loop->isSelectingTiles())
snapPointsToGridTiles(loop, stroke);
}
bool releaseButton(Stroke& stroke, const Point& point) override {
bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override {
return false;
}
void movement(ToolLoop* loop, Stroke& stroke, const Point& point) override {
void movement(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override {
ASSERT(stroke.size() >= 2);
if (stroke.size() < 2)
return;
if (MoveOriginCapability::isMovingOrigin(loop, stroke, point))
if (MoveOriginCapability::isMovingOrigin(loop, stroke, pt))
return;
if (!loop->getIntertwine()->snapByAngle() &&
@ -144,13 +145,13 @@ public:
m_center.x = (stroke[0].x+stroke[1].x)/2;
m_center.y = (stroke[0].y+stroke[1].y)/2;
}
m_angle = std::atan2(static_cast<double>(point.y-m_center.y),
static_cast<double>(point.x-m_center.x));
m_angle = std::atan2(static_cast<double>(pt.y-m_center.y),
static_cast<double>(pt.x-m_center.x));
return;
}
stroke[0] = m_first;
stroke[1] = point;
stroke[1] = pt;
if ((int(loop->getModifiers()) & int(ToolLoopModifiers::kSquareAspect))) {
int dx = stroke[1].x - m_first.x;
@ -285,10 +286,10 @@ private:
void snapPointsToGridTiles(ToolLoop* loop, Stroke& stroke) {
auto grid = loop->getGridBounds();
Rect a(snap_to_grid(grid, stroke[0], PreferSnapTo::BoxOrigin),
snap_to_grid(grid, stroke[0], PreferSnapTo::BoxEnd));
Rect b(snap_to_grid(grid, stroke[1], PreferSnapTo::BoxOrigin),
snap_to_grid(grid, stroke[1], PreferSnapTo::BoxEnd));
Rect a(snap_to_grid(grid, stroke[0].toPoint(), PreferSnapTo::BoxOrigin),
snap_to_grid(grid, stroke[0].toPoint(), PreferSnapTo::BoxEnd));
Rect b(snap_to_grid(grid, stroke[1].toPoint(), PreferSnapTo::BoxOrigin),
snap_to_grid(grid, stroke[1].toPoint(), PreferSnapTo::BoxEnd));
a |= b;
@ -301,12 +302,14 @@ private:
}
void onMoveOrigin(const Point& delta) override {
m_first += delta;
m_center += delta;
m_first.x += delta.x;
m_first.y += delta.y;
m_center.x += delta.x;
m_center.y += delta.y;
}
Point m_first;
Point m_center;
Stroke::Pt m_first;
Stroke::Pt m_center;
double m_angle;
};
@ -314,34 +317,34 @@ private:
class PointByPointController : public MoveOriginCapability {
public:
void pressButton(ToolLoop* loop, Stroke& stroke, const Point& point) override {
MoveOriginCapability::pressButton(loop, stroke, point);
void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override {
MoveOriginCapability::pressButton(loop, stroke, pt);
stroke.addPoint(point);
stroke.addPoint(point);
stroke.addPoint(pt);
stroke.addPoint(pt);
}
bool releaseButton(Stroke& stroke, const Point& point) override {
bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override {
ASSERT(!stroke.empty());
if (stroke.empty())
return false;
if (stroke[stroke.size()-2] == point &&
stroke[stroke.size()-1] == point)
if (stroke[stroke.size()-2] == pt &&
stroke[stroke.size()-1] == pt)
return false; // Click in the same point (no-drag), we are done
else
return true; // Continue adding points
}
void movement(ToolLoop* loop, Stroke& stroke, const Point& point) override {
void movement(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override {
ASSERT(!stroke.empty());
if (stroke.empty())
return;
if (MoveOriginCapability::isMovingOrigin(loop, stroke, point))
if (MoveOriginCapability::isMovingOrigin(loop, stroke, pt))
return;
stroke[stroke.size()-1] = point;
stroke[stroke.size()-1] = pt;
}
void getStrokeToInterwine(const Stroke& input, Stroke& output) override {
@ -371,16 +374,16 @@ public:
bool canSnapToGrid() override { return false; }
bool isOnePoint() override { return true; }
void pressButton(ToolLoop* loop, Stroke& stroke, const Point& point) override {
void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override {
if (stroke.size() == 0)
stroke.addPoint(point);
stroke.addPoint(pt);
}
bool releaseButton(Stroke& stroke, const Point& point) override {
bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override {
return false;
}
void movement(ToolLoop* loop, Stroke& stroke, const Point& point) override {
void movement(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override {
// Do nothing
}
@ -406,38 +409,38 @@ public:
class FourPointsController : public MoveOriginCapability {
public:
void pressButton(ToolLoop* loop, Stroke& stroke, const Point& point) override {
MoveOriginCapability::pressButton(loop, stroke, point);
void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override {
MoveOriginCapability::pressButton(loop, stroke, pt);
if (stroke.size() == 0) {
stroke.reset(4, point);
stroke.reset(4, pt);
m_clickCounter = 0;
}
else
m_clickCounter++;
}
bool releaseButton(Stroke& stroke, const Point& point) override {
bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override {
m_clickCounter++;
return m_clickCounter < 4;
}
void movement(ToolLoop* loop, Stroke& stroke, const Point& point) override {
if (MoveOriginCapability::isMovingOrigin(loop, stroke, point))
void movement(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override {
if (MoveOriginCapability::isMovingOrigin(loop, stroke, pt))
return;
switch (m_clickCounter) {
case 0:
for (int i=1; i<stroke.size(); ++i)
stroke[i] = point;
stroke[i] = pt;
break;
case 1:
case 2:
stroke[1] = point;
stroke[2] = point;
stroke[1] = pt;
stroke[2] = pt;
break;
case 3:
stroke[2] = point;
stroke[2] = pt;
break;
}
}
@ -472,14 +475,14 @@ class LineFreehandController : public Controller {
public:
bool isFreehand() override { return true; }
gfx::Point getLastPoint() const override { return m_last; }
Stroke::Pt getLastPoint() const override { return m_last; }
void prepareController(ToolLoop* loop) override {
m_controller = nullptr;
}
void pressButton(ToolLoop* loop, Stroke& stroke, const Point& point) override {
m_last = point;
void pressButton(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override {
m_last = pt;
if (m_controller == nullptr)
m_controller = &m_twoPoints;
@ -488,18 +491,18 @@ public:
return; // Don't send first pressButton() click to the freehand controller
}
m_controller->pressButton(loop, stroke, point);
m_controller->pressButton(loop, stroke, pt);
}
bool releaseButton(Stroke& stroke, const Point& point) override {
bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override {
if (!stroke.empty())
m_last = stroke.lastPoint();
return false;
}
void movement(ToolLoop* loop, Stroke& stroke, const Point& point) override {
m_last = point;
m_controller->movement(loop, stroke, point);
void movement(ToolLoop* loop, Stroke& stroke, const Stroke::Pt& pt) override {
m_last = pt;
m_controller->movement(loop, stroke, pt);
}
void getStrokeToInterwine(const Stroke& input, Stroke& output) override {
@ -519,7 +522,7 @@ public:
}
private:
Point m_last;
Stroke::Pt m_last;
TwoPointsController m_twoPoints;
FreehandController m_freehand;
Controller* m_controller;

View File

@ -130,6 +130,9 @@ template<typename ImageTraits>
class CopyInkProcessing : public SimpleInkProcessing<CopyInkProcessing<ImageTraits>, ImageTraits> {
public:
CopyInkProcessing(ToolLoop* loop) {
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
m_color = loop->getPrimaryColor();
if (loop->getLayer()->isBackground()) {
@ -156,8 +159,11 @@ template<typename ImageTraits>
class LockAlphaInkProcessing : public DoubleInkProcessing<LockAlphaInkProcessing<ImageTraits>, ImageTraits> {
public:
LockAlphaInkProcessing(ToolLoop* loop)
: m_color(loop->getPrimaryColor())
, m_opacity(loop->getOpacity()) {
: m_opacity(loop->getOpacity()) {
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
m_color = loop->getPrimaryColor();
}
void processPixel(int x, int y) {
@ -165,7 +171,7 @@ public:
}
private:
const color_t m_color;
color_t m_color;
const int m_opacity;
};
@ -193,11 +199,14 @@ public:
LockAlphaInkProcessing(ToolLoop* loop)
: m_palette(get_current_palette())
, m_rgbmap(loop->getRgbMap())
, m_color(m_palette->getEntry(loop->getPrimaryColor()))
, m_opacity(loop->getOpacity())
, m_maskIndex(loop->getLayer()->isBackground() ? -1: loop->sprite()->transparentColor()) {
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
m_color = m_palette->getEntry(loop->getPrimaryColor());
}
void processPixel(int x, int y) {
color_t c = *m_srcAddress;
if (int(c) == m_maskIndex)
@ -217,7 +226,7 @@ public:
private:
const Palette* m_palette;
const RgbMap* m_rgbmap;
const color_t m_color;
color_t m_color;
const int m_opacity;
const int m_maskIndex;
};
@ -230,10 +239,13 @@ template<typename ImageTraits>
class TransparentInkProcessing : public DoubleInkProcessing<TransparentInkProcessing<ImageTraits>, ImageTraits> {
public:
TransparentInkProcessing(ToolLoop* loop) {
m_color = loop->getPrimaryColor();
m_opacity = loop->getOpacity();
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
m_color = loop->getPrimaryColor();
}
void processPixel(int x, int y) {
// Do nothing
}
@ -260,10 +272,13 @@ public:
m_palette(get_current_palette()),
m_rgbmap(loop->getRgbMap()),
m_opacity(loop->getOpacity()),
m_color(m_palette->getEntry(loop->getPrimaryColor())),
m_maskIndex(loop->getLayer()->isBackground() ? -1: loop->sprite()->transparentColor()) {
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
m_color = m_palette->getEntry(loop->getPrimaryColor());
}
void processPixel(int x, int y) {
color_t c = *m_srcAddress;
if (int(c) == m_maskIndex)
@ -282,7 +297,7 @@ private:
const Palette* m_palette;
const RgbMap* m_rgbmap;
const int m_opacity;
const color_t m_color;
color_t m_color;
const int m_maskIndex;
};
@ -294,10 +309,13 @@ template<typename ImageTraits>
class MergeInkProcessing : public DoubleInkProcessing<MergeInkProcessing<ImageTraits>, ImageTraits> {
public:
MergeInkProcessing(ToolLoop* loop) {
m_color = loop->getPrimaryColor();
m_opacity = loop->getOpacity();
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
m_color = loop->getPrimaryColor();
}
void processPixel(int x, int y) {
// Do nothing
}
@ -324,10 +342,13 @@ public:
m_palette(get_current_palette()),
m_rgbmap(loop->getRgbMap()),
m_opacity(loop->getOpacity()),
m_maskIndex(loop->getLayer()->isBackground() ? -1: loop->sprite()->transparentColor()),
m_color(int(loop->getPrimaryColor()) == m_maskIndex ?
(m_palette->getEntry(loop->getPrimaryColor()) & rgba_rgb_mask):
(m_palette->getEntry(loop->getPrimaryColor()))) {
m_maskIndex(loop->getLayer()->isBackground() ? -1: loop->sprite()->transparentColor()) {
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
m_color = (int(loop->getPrimaryColor()) == m_maskIndex ?
(m_palette->getEntry(loop->getPrimaryColor()) & rgba_rgb_mask):
(m_palette->getEntry(loop->getPrimaryColor())));
}
void processPixel(int x, int y) {
@ -349,7 +370,7 @@ private:
const RgbMap* m_rgbmap;
const int m_opacity;
const int m_maskIndex;
const color_t m_color;
color_t m_color;
};
//////////////////////////////////////////////////////////////////////
@ -911,8 +932,8 @@ public:
return;
}
const gfx::Point u = strokes[0].firstPoint();
const gfx::Point v = strokes[0].lastPoint();
const gfx::Point u = strokes[0].firstPoint().toPoint();
const gfx::Point v = strokes[0].lastPoint().toPoint();
// The image position for the gradient depends on the first user
// click. The gradient depends on the first clicked tile.
@ -952,7 +973,6 @@ public:
, m_palette(get_current_palette())
, m_rgbmap(loop->getRgbMap())
, m_maskIndex(loop->getLayer()->isBackground() ? -1: loop->sprite()->transparentColor())
, m_matrix(loop->getDitheringMatrix())
{
}
@ -974,7 +994,6 @@ private:
const Palette* m_palette;
const RgbMap* m_rgbmap;
const int m_maskIndex;
const render::DitheringMatrix m_matrix;
};
template<>
@ -1115,8 +1134,10 @@ public:
m_bgColor = loop->getSecondaryColor();
m_palette = get_current_palette();
m_brush = loop->getBrush();
m_brushImage = m_brush->image();
m_brushImage = (m_brush->patternImage() ? m_brush->patternImage():
m_brush->image());
m_brushMask = m_brush->maskBitmap();
m_patternAlign = m_brush->pattern();
m_opacity = loop->getOpacity();
m_width = m_brush->bounds().w;
m_height = m_brush->bounds().h;
@ -1126,16 +1147,15 @@ public:
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
if ((m_brush->pattern() == BrushPattern::ALIGNED_TO_DST && firstPoint) ||
(m_brush->pattern() == BrushPattern::PAINT_BRUSH)) {
if ((m_patternAlign == BrushPattern::ALIGNED_TO_DST && firstPoint) ||
(m_patternAlign == BrushPattern::PAINT_BRUSH)) {
m_u = ((m_brush->patternOrigin().x % loop->sprite()->width()) - loop->getCelOrigin().x) % m_width;
m_v = ((m_brush->patternOrigin().y % loop->sprite()->height()) - loop->getCelOrigin().y) % m_height;
}
}
void prepareVForPointShape(ToolLoop* loop, int y) override {
if (m_brush->pattern() == doc::BrushPattern::ALIGNED_TO_SRC) {
if (m_patternAlign == doc::BrushPattern::ALIGNED_TO_SRC) {
m_v = (m_brush->patternOrigin().y - loop->getCelOrigin().y) % m_height;
if (m_v < 0) m_v += m_height;
}
@ -1151,7 +1171,7 @@ public:
}
void prepareUForPointShapeWholeScanline(ToolLoop* loop, int x1) override {
if (m_brush->pattern() == doc::BrushPattern::ALIGNED_TO_SRC) {
if (m_patternAlign == doc::BrushPattern::ALIGNED_TO_SRC) {
m_u = (m_brush->patternOrigin().x - loop->getCelOrigin().x) % m_width;
if (m_u < 0) m_u += m_height;
}
@ -1163,7 +1183,7 @@ public:
}
void prepareUForPointShapeSlicedScanline(ToolLoop* loop, bool leftSlice, int x1) override {
if (loop->getBrush()->pattern() == doc::BrushPattern::ALIGNED_TO_SRC) {
if (m_patternAlign == doc::BrushPattern::ALIGNED_TO_SRC) {
m_u = (m_brush->patternOrigin().x - loop->getCelOrigin().x) % m_width;
if (m_u < 0) m_u += m_height;
return;
@ -1189,11 +1209,27 @@ public:
}
protected:
void alignPixelPoint(int& x, int& y) {
x = (x - m_u) % m_width;
y = (y - m_v) % m_height;
bool alignPixelPoint(int& x0, int& y0) {
int x = (x0 - m_u) % m_width;
int y = (y0 - m_v) % m_height;
if (x < 0) x = m_width - ((-x) % m_width);
if (y < 0) y = m_height - ((-y) % m_height);
if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
return false;
if (m_brush->patternImage()) {
const int w = m_brush->patternImage()->width();
const int h = m_brush->patternImage()->height();
x = x0 % w;
y = y0 % h;
if (x < 0) x = w - ((-x) % w);
if (y < 0) y = h - ((-y) % h);
}
x0 = x;
y0 = y;
return true;
}
color_t m_fgColor;
@ -1202,6 +1238,7 @@ protected:
const Brush* m_brush;
const Image* m_brushImage;
const Image* m_brushMask;
BrushPattern m_patternAlign;
int m_opacity;
int m_u, m_v, m_width, m_height;
// When we have a image brush from an INDEXED sprite, we need to know
@ -1212,8 +1249,7 @@ protected:
template<>
bool BrushInkProcessingBase<RgbTraits>::preProcessPixel(int x, int y, color_t* result) {
alignPixelPoint(x, y);
if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
if (!alignPixelPoint(x, y))
return false;
color_t c;
@ -1264,8 +1300,7 @@ bool BrushInkProcessingBase<RgbTraits>::preProcessPixel(int x, int y, color_t* r
template<>
bool BrushInkProcessingBase<GrayscaleTraits>::preProcessPixel(int x, int y, color_t* result) {
alignPixelPoint(x, y);
if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
if (!alignPixelPoint(x, y))
return false;
color_t c;
@ -1313,8 +1348,7 @@ bool BrushInkProcessingBase<GrayscaleTraits>::preProcessPixel(int x, int y, colo
template<>
bool BrushInkProcessingBase<IndexedTraits>::preProcessPixel(int x, int y, color_t* result) {
alignPixelPoint(x, y);
if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
if (!alignPixelPoint(x, y))
return false;
color_t c;
@ -1444,8 +1478,7 @@ public:
template<>
void BrushEraserInkProcessing<RgbTraits>::processPixel(int x, int y) {
alignPixelPoint(x, y);
if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
if (!alignPixelPoint(x, y))
return;
color_t c;
@ -1502,8 +1535,7 @@ void BrushEraserInkProcessing<RgbTraits>::processPixel(int x, int y) {
template<>
void BrushEraserInkProcessing<GrayscaleTraits>::processPixel(int x, int y) {
alignPixelPoint(x, y);
if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
if (!alignPixelPoint(x, y))
return;
color_t c;
@ -1554,8 +1586,7 @@ void BrushEraserInkProcessing<GrayscaleTraits>::processPixel(int x, int y) {
template<>
void BrushEraserInkProcessing<IndexedTraits>::processPixel(int x, int y) {
alignPixelPoint(x, y);
if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
if (!alignPixelPoint(x, y))
return;
color_t c;
@ -1632,8 +1663,7 @@ RgbTraits::pixel_t BrushShadingInkProcessing<RgbTraits>::shadingProcessPixel(int
template <>
void BrushShadingInkProcessing<RgbTraits>::processPixel(int x, int y) {
alignPixelPoint(x, y);
if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
if (!alignPixelPoint(x, y))
return;
RgbTraits::pixel_t c;
@ -1689,8 +1719,7 @@ IndexedTraits::pixel_t BrushShadingInkProcessing<IndexedTraits>::shadingProcessP
template <>
void BrushShadingInkProcessing<IndexedTraits>::processPixel(int x, int y) {
alignPixelPoint(x, y);
if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
if (!alignPixelPoint(x, y))
return;
color_t c;
@ -1750,8 +1779,7 @@ GrayscaleTraits::pixel_t BrushShadingInkProcessing<GrayscaleTraits>::shadingProc
template <>
void BrushShadingInkProcessing<GrayscaleTraits>::processPixel(int x, int y) {
alignPixelPoint(x, y);
if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
if (!alignPixelPoint(x, y))
return;
int iBrush = -1;
@ -1819,8 +1847,7 @@ public:
template<>
void BrushCopyInkProcessing<RgbTraits>::processPixel(int x, int y) {
alignPixelPoint(x, y);
if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
if (!alignPixelPoint(x, y))
return;
color_t c;
@ -1866,8 +1893,7 @@ void BrushCopyInkProcessing<RgbTraits>::processPixel(int x, int y) {
template<>
void BrushCopyInkProcessing<IndexedTraits>::processPixel(int x, int y) {
alignPixelPoint(x, y);
if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
if (!alignPixelPoint(x, y))
return;
color_t c;
@ -1912,8 +1938,7 @@ void BrushCopyInkProcessing<IndexedTraits>::processPixel(int x, int y) {
template<>
void BrushCopyInkProcessing<GrayscaleTraits>::processPixel(int x, int y) {
alignPixelPoint(x, y);
if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
if (!alignPixelPoint(x, y))
return;
color_t c;

View File

@ -96,7 +96,7 @@ public:
break;
}
if (loop->getBrush()->type() == doc::kImageBrushType)
if (loop->getBrush()->type() == doc::kImageBrushType) {
switch (m_type) {
case Simple:
setProc(get_ink_proc<BrushSimpleInkProcessing>(loop));
@ -111,12 +111,19 @@ public:
setProc(get_ink_proc<BrushSimpleInkProcessing>(loop));
break;
}
}
else {
switch (m_type) {
case Simple: {
bool opaque = false;
if (loop->getOpacity() == 255) {
if (loop->getOpacity() == 255 &&
// The trace policy is "overlap" when the dynamics has
// a gradient between FG <-> BG
//
// TODO this trace policy is configured in
// ToolLoopBase() ctor, is there a better place?
loop->getTracePolicy() != TracePolicy::Overlap) {
color_t color = loop->getPrimaryColor();
switch (loop->sprite()->pixelFormat()) {

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2019 Igara Studio S.A.
// Copyright (C) 2018-2020 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -24,35 +24,76 @@ namespace tools {
using namespace gfx;
using namespace doc;
Intertwine::LineData::LineData(ToolLoop* loop,
const Stroke::Pt& a,
const Stroke::Pt& b)
: loop(loop)
, a(a)
, b(b)
, pt(a)
{
const int steps = std::max(std::abs(b.x - a.x),
std::abs(b.y - a.y))+1;
t = 0.0f;
step = 1.0f / steps;
}
void Intertwine::LineData::doStep(int x, int y)
{
t += step;
const float ti = 1.0f-t;
pt.x = x;
pt.y = y;
pt.size = ti*a.size + t*b.size;
pt.angle = ti*a.angle + t*b.angle;
pt.gradient = ti*a.gradient + t*b.gradient;
}
gfx::Rect Intertwine::getStrokeBounds(ToolLoop* loop, const Stroke& stroke)
{
return stroke.bounds();
}
// static
void Intertwine::doPointshapePoint(int x, int y, ToolLoop* loop)
void Intertwine::doPointshapeStrokePt(const Stroke::Pt& pt, ToolLoop* loop)
{
Symmetry* symmetry = loop->getSymmetry();
if (symmetry) {
// Convert the point to the sprite position so we can apply the
// symmetry transformation.
Stroke main_stroke;
main_stroke.addPoint(Point(x, y));
main_stroke.addPoint(pt);
Strokes strokes;
symmetry->generateStrokes(main_stroke, strokes, loop);
for (const auto& stroke : strokes) {
// We call transformPoint() moving back each point to the cel
// origin.
loop->getPointShape()->transformPoint(
loop, stroke[0].x, stroke[0].y);
loop->getPointShape()->transformPoint(loop, stroke[0]);
}
}
else {
loop->getPointShape()->transformPoint(loop, x, y);
loop->getPointShape()->transformPoint(loop, pt);
}
}
// static
void Intertwine::doPointshapePoint(int x, int y, ToolLoop* loop)
{
Stroke::Pt pt(x, y);
pt.size = loop->getBrush()->size();
pt.angle = loop->getBrush()->angle();
doPointshapeStrokePt(pt, loop);
}
// static
void Intertwine::doPointshapePointDynamics(int x, int y, Intertwine::LineData* data)
{
data->doStep(x, y);
doPointshapeStrokePt(data->pt, data->loop);
}
// static
void Intertwine::doPointshapeHline(int x1, int y, int x2, ToolLoop* loop)
{

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -9,13 +9,13 @@
#define APP_TOOLS_INTERTWINE_H_INCLUDED
#pragma once
#include "app/tools/stroke.h"
#include "doc/algo.h"
#include "gfx/point.h"
#include "gfx/rect.h"
namespace app {
namespace tools {
class Stroke;
class ToolLoop;
// Converts a sequence of points in several call to
@ -34,9 +34,19 @@ namespace app {
virtual gfx::Rect getStrokeBounds(ToolLoop* loop, const Stroke& stroke);
struct LineData {
ToolLoop* loop;
Stroke::Pt a, b, pt;
float t, step;
LineData(ToolLoop* loop, const Stroke::Pt& a, const Stroke::Pt& b);
void doStep(int x, int y);
};
protected:
static void doPointshapeStrokePt(const Stroke::Pt& pt, ToolLoop* loop);
// The given point must be relative to the cel origin.
static void doPointshapePoint(int x, int y, ToolLoop* loop);
static void doPointshapePointDynamics(int x, int y, LineData* data);
static void doPointshapeHline(int x1, int y, int x2, ToolLoop* loop);
static void doPointshapeLine(int x1, int y1, int x2, int y2, ToolLoop* loop);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2019 Igara Studio S.A.
// Copyright (C) 2018-2020 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -10,12 +10,22 @@
namespace app {
namespace tools {
static void addPointsWithoutDuplicatingLastOne(int x, int y, Stroke* stroke)
struct LineData2 {
Intertwine::LineData head;
Stroke& output;
LineData2(ToolLoop* loop, const Stroke::Pt& a, const Stroke::Pt& b,
Stroke& output)
: head(loop, a, b)
, output(output) {
}
};
static void addPointsWithoutDuplicatingLastOne(int x, int y, LineData2* data)
{
const gfx::Point newPoint(x, y);
if (stroke->empty() ||
stroke->lastPoint() != newPoint) {
stroke->addPoint(newPoint);
data->head.doStep(x, y);
if (data->output.empty() ||
data->output.lastPoint() != data->head.pt) {
data->output.addPoint(data->head.pt);
}
}
@ -24,7 +34,7 @@ public:
void joinStroke(ToolLoop* loop, const Stroke& stroke) override {
for (int c=0; c<stroke.size(); ++c)
doPointshapePoint(stroke[c].x, stroke[c].y, loop);
doPointshapeStrokePt(stroke[c], loop);
}
void fillStroke(ToolLoop* loop, const Stroke& stroke) override {
@ -56,7 +66,7 @@ public:
mid.y /= n;
}
else {
mid = stroke[0];
mid = stroke[0].toPoint();
}
doPointshapePoint(mid.x, mid.y, loop);
@ -107,9 +117,10 @@ public:
Stroke pts;
doc::AlgoLineWithAlgoPixel lineAlgo = getLineAlgo(loop);
for (int c=0; c+1<stroke.size(); ++c) {
LineData2 lineData(loop, stroke[c], stroke[c+1], pts);
lineAlgo(stroke[c].x, stroke[c].y,
stroke[c+1].x, stroke[c+1].y,
(void*)&pts,
(void*)&lineData,
(AlgoPixel)&addPointsWithoutDuplicatingLastOne);
}
@ -123,7 +134,7 @@ public:
(m_retainedTracePolicyLast || !m_firstStroke) ? 1: 0);
for (int c=start; c<pts.size(); ++c)
doPointshapePoint(pts[c].x, pts[c].y, loop);
doPointshapeStrokePt(pts[c], loop);
// Closed shape (polygon outline)
// Note: Contour tool was getting into the condition with no need, so
@ -160,7 +171,10 @@ public:
#endif
// Fill content
doc::algorithm::polygon(stroke.size(), (const int*)&stroke[0], loop, (AlgoHLine)doPointshapeHline);
auto v = stroke.toXYInts();
doc::algorithm::polygon(
v.size()/2, &v[0],
loop, (AlgoHLine)doPointshapeHline);
}
};
@ -233,8 +247,9 @@ public:
}
else {
Stroke p = rotateRectangle(x1, y1, x2, y2, angle);
auto v = p.toXYInts();
doc::algorithm::polygon(
p.size(), (const int*)&p[0],
v.size()/2, &v[0],
loop, (AlgoHLine)doPointshapeHline);
}
}
@ -445,17 +460,18 @@ public:
else if (stroke.size() == 1) {
if (m_pts.empty())
m_pts = stroke;
doPointshapePoint(stroke[0].x, stroke[0].y, loop);
doPointshapeStrokePt(stroke[0], loop);
return;
}
else {
for (int c=0; c+1<stroke.size(); ++c) {
LineData2 lineData(loop, stroke[c], stroke[c+1], m_pts);
algo_line_continuous(
stroke[c].x,
stroke[c].y,
stroke[c+1].x,
stroke[c+1].y,
(void*)&m_pts,
(void*)&lineData,
(AlgoPixel)&addPointsWithoutDuplicatingLastOne);
}
}
@ -477,7 +493,7 @@ public:
// the SHIFT key))
if (c == 0 && m_retainedTracePolicyLast)
continue;
doPointshapePoint(m_pts[c].x, m_pts[c].y, loop);
doPointshapeStrokePt(m_pts[c], loop);
}
}
@ -486,8 +502,12 @@ public:
joinStroke(loop, stroke);
return;
}
// Fill content
doc::algorithm::polygon(m_pts.size(), (const int*)&m_pts[0], loop, (AlgoHLine)doPointshapeHline);
auto v = m_pts.toXYInts();
doc::algorithm::polygon(
v.size()/2, &v[0],
loop, (AlgoHLine)doPointshapeHline);
}
};

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This program is distributed under the terms of
@ -8,6 +9,7 @@
#define APP_TOOLS_TOOL_POINT_SHAPE_H_INCLUDED
#pragma once
#include "app/tools/stroke.h"
#include "gfx/rect.h"
namespace app {
@ -24,7 +26,7 @@ namespace app {
virtual void preparePointShape(ToolLoop* loop) { }
// The x, y position must be relative to the cel/src/dst image origin.
virtual void transformPoint(ToolLoop* loop, int x, int y) = 0;
virtual void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) = 0;
virtual void getModifiedArea(ToolLoop* loop, int x, int y, gfx::Rect& area) = 0;
protected:

View File

@ -8,13 +8,14 @@
#include "app/util/wrap_point.h"
#include "app/tools/ink.h"
#include "render/gradient.h"
namespace app {
namespace tools {
class NonePointShape : public PointShape {
public:
void transformPoint(ToolLoop* loop, int x, int y) override {
void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) override {
// Do nothing
}
@ -27,8 +28,8 @@ class PixelPointShape : public PointShape {
public:
bool isPixel() override { return true; }
void transformPoint(ToolLoop* loop, int x, int y) override {
doInkHline(x, y, x, loop);
void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) override {
doInkHline(pt.x, pt.y, pt.x, loop);
}
void getModifiedArea(ToolLoop* loop, int x, int y, Rect& area) override {
@ -37,51 +38,162 @@ public:
};
class BrushPointShape : public PointShape {
Brush* m_lastBrush;
std::shared_ptr<CompressedImage> m_compressedImage;
bool m_firstPoint;
Brush* m_lastBrush;
BrushType m_origBrushType;
std::shared_ptr<CompressedImage> m_compressedImage;
// For dynamics
DynamicsOptions m_dynamics;
bool m_useDynamics;
bool m_hasDynamicGradient;
color_t m_primaryColor;
color_t m_secondaryColor;
float m_lastGradientValue;
public:
void preparePointShape(ToolLoop* loop) override {
m_firstPoint = true;
m_lastBrush = nullptr;
m_origBrushType = loop->getBrush()->type();
m_dynamics = loop->getDynamics();
m_useDynamics = (m_dynamics.isDynamic() &&
// TODO support custom brushes in future versions
m_origBrushType != kImageBrushType);
// For dynamic gradient
m_hasDynamicGradient = (m_dynamics.gradient != DynamicSensor::Static);
m_primaryColor = loop->getPrimaryColor();
m_secondaryColor = loop->getSecondaryColor();
m_lastGradientValue = -1;
}
void transformPoint(ToolLoop* loop, int x, int y) override {
Brush* m_brush = loop->getBrush();
if (m_lastBrush != m_brush) {
m_lastBrush = m_brush;
m_compressedImage.reset(new CompressedImage(m_brush->image(),
m_brush->maskBitmap(),
void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) override {
int x = pt.x;
int y = pt.y;
Brush* brush = loop->getBrush();
// Dynamics
if (m_useDynamics) {
// Dynamic gradient info
if (m_hasDynamicGradient &&
m_dynamics.ditheringMatrix.rows() == 1 &&
m_dynamics.ditheringMatrix.cols() == 1) {
color_t a = m_primaryColor;
color_t b = m_secondaryColor;
const float t = pt.gradient;
const float ti = 1.0f - pt.gradient;
switch (loop->sprite()->pixelFormat()) {
case IMAGE_RGB:
if (rgba_geta(a) == 0) a = b;
else if (rgba_geta(b) == 0) b = a;
a = doc::rgba(int(ti*rgba_getr(a) + t*rgba_getr(b)),
int(ti*rgba_getg(a) + t*rgba_getg(b)),
int(ti*rgba_getb(a) + t*rgba_getb(b)),
int(ti*rgba_geta(a) + t*rgba_geta(b)));
break;
case IMAGE_GRAYSCALE:
if (graya_geta(a) == 0) a = b;
else if (graya_geta(b) == 0) b = a;
a = doc::graya(int(ti*graya_getv(a) + t*graya_getv(b)),
int(ti*graya_geta(a) + t*graya_geta(b)));
break;
case IMAGE_INDEXED: {
int maskIndex = (loop->getLayer()->isBackground() ? -1: loop->sprite()->transparentColor());
// Convert index to RGBA
if (a == maskIndex) a = 0;
else a = get_current_palette()->getEntry(a);
if (b == maskIndex) b = 0;
else b = get_current_palette()->getEntry(b);
// Same as in RGBA gradient
if (rgba_geta(a) == 0) a = b;
else if (rgba_geta(b) == 0) b = a;
a = doc::rgba(int(ti*rgba_getr(a) + t*rgba_getr(b)),
int(ti*rgba_getg(a) + t*rgba_getg(b)),
int(ti*rgba_getb(a) + t*rgba_getb(b)),
int(ti*rgba_geta(a) + t*rgba_geta(b)));
// Convert RGBA to index
a = loop->getRgbMap()->mapColor(rgba_getr(a),
rgba_getg(a),
rgba_getb(a),
rgba_geta(a));
break;
}
}
loop->setPrimaryColor(a);
}
// Dynamic size and angle
int size = base::clamp(int(pt.size), int(Brush::kMinBrushSize), int(Brush::kMaxBrushSize));
int angle = base::clamp(int(pt.angle), -180, 180);
if ((brush->size() != size) ||
(brush->angle() != angle && m_origBrushType != kCircleBrushType) ||
(m_hasDynamicGradient && pt.gradient != m_lastGradientValue)) {
// TODO cache brushes
BrushRef newBrush = std::make_shared<Brush>(
m_origBrushType, size, angle);
// Dynamic gradient with dithering
bool prepareInk = false;
if (m_hasDynamicGradient &&
(m_dynamics.ditheringMatrix.rows() > 1 ||
m_dynamics.ditheringMatrix.cols() > 1)) {
convert_bitmap_brush_to_dithering_brush(
newBrush.get(),
loop->sprite()->pixelFormat(),
m_dynamics.ditheringMatrix,
pt.gradient,
m_primaryColor,
m_secondaryColor);
prepareInk = true;
}
m_lastGradientValue = pt.gradient;
loop->setBrush(newBrush);
brush = loop->getBrush();
if (prepareInk) {
// Prepare ink for the new brush
loop->getInk()->prepareInk(loop);
}
}
}
// TODO cache compressed images (or remove them completelly)
if (m_lastBrush != brush) {
m_lastBrush = brush;
m_compressedImage.reset(new CompressedImage(brush->image(),
brush->maskBitmap(),
false));
}
x += m_brush->bounds().x;
y += m_brush->bounds().y;
x += brush->bounds().x;
y += brush->bounds().y;
if (m_firstPoint) {
if ((m_brush->type() == kImageBrushType) &&
(m_brush->pattern() == BrushPattern::ALIGNED_TO_DST ||
m_brush->pattern() == BrushPattern::PAINT_BRUSH)) {
m_brush->setPatternOrigin(gfx::Point(x, y));
if ((brush->type() == kImageBrushType) &&
(brush->pattern() == BrushPattern::ALIGNED_TO_DST ||
brush->pattern() == BrushPattern::PAINT_BRUSH)) {
brush->setPatternOrigin(gfx::Point(x, y));
}
}
else {
if (m_brush->type() == kImageBrushType &&
m_brush->pattern() == BrushPattern::PAINT_BRUSH) {
m_brush->setPatternOrigin(gfx::Point(x, y));
if (brush->type() == kImageBrushType &&
brush->pattern() == BrushPattern::PAINT_BRUSH) {
brush->setPatternOrigin(gfx::Point(x, y));
}
}
if (int(loop->getTiledMode()) & int(TiledMode::X_AXIS)) {
int wrappedPatternOriginX = wrap_value(m_brush->patternOrigin().x, loop->sprite()->width()) % m_brush->bounds().w;
m_brush->setPatternOrigin(gfx::Point(wrappedPatternOriginX, m_brush->patternOrigin().y));
int wrappedPatternOriginX = wrap_value(brush->patternOrigin().x, loop->sprite()->width()) % brush->bounds().w;
brush->setPatternOrigin(gfx::Point(wrappedPatternOriginX, brush->patternOrigin().y));
x = wrap_value(x, loop->sprite()->width());
}
if (int(loop->getTiledMode()) & int(TiledMode::Y_AXIS)) {
int wrappedPatternOriginY = wrap_value(m_brush->patternOrigin().y, loop->sprite()->height()) % m_brush->bounds().h;
m_brush->setPatternOrigin(gfx::Point(m_brush->patternOrigin().x, wrappedPatternOriginY));
int wrappedPatternOriginY = wrap_value(brush->patternOrigin().y, loop->sprite()->height()) % brush->bounds().h;
brush->setPatternOrigin(gfx::Point(brush->patternOrigin().x, wrappedPatternOriginY));
y = wrap_value(y, loop->sprite()->height());
}
@ -107,19 +219,19 @@ class FloodFillPointShape : public PointShape {
public:
bool isFloodFill() override { return true; }
void transformPoint(ToolLoop* loop, int x, int y) override {
void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) override {
const doc::Image* srcImage = loop->getFloodFillSrcImage();
gfx::Point pt = wrap_point(loop->getTiledMode(),
gfx::Size(srcImage->width(),
srcImage->height()),
gfx::Point(x, y), true);
gfx::Point wpt = wrap_point(loop->getTiledMode(),
gfx::Size(srcImage->width(),
srcImage->height()),
pt.toPoint(), true);
doc::algorithm::floodfill(
srcImage,
(loop->useMask() ? loop->getMask(): nullptr),
pt.x, pt.y,
floodfillBounds(loop, pt.x, pt.y),
get_pixel(srcImage, pt.x, pt.y),
wpt.x, wpt.y,
floodfillBounds(loop, wpt.x, wpt.y),
get_pixel(srcImage, wpt.x, wpt.y),
loop->getTolerance(),
loop->getContiguous(),
loop->isPixelConnectivityEightConnected(),
@ -173,7 +285,7 @@ public:
m_subPointShape.preparePointShape(loop);
}
void transformPoint(ToolLoop* loop, int x, int y) override {
void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) override {
int spray_width = loop->getSprayWidth();
int spray_speed = loop->getSpraySpeed();
@ -203,9 +315,10 @@ public:
radius = rand() % fixmath::itofix(spray_width);
#endif
int u = fixmath::fixtoi(fixmath::fixmul(radius, fixmath::fixcos(angle)));
int v = fixmath::fixtoi(fixmath::fixmul(radius, fixmath::fixsin(angle)));
m_subPointShape.transformPoint(loop, x+u, y+v);
Stroke::Pt pt2(pt);
pt2.x += fixmath::fixtoi(fixmath::fixmul(radius, fixmath::fixcos(angle)));
pt2.y += fixmath::fixtoi(fixmath::fixmul(radius, fixmath::fixsin(angle)));
m_subPointShape.transformPoint(loop, pt2);
}
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2001-2015 David Capello
//
// This program is distributed under the terms of
@ -16,44 +16,46 @@ namespace tools {
void Stroke::reset()
{
m_points.clear();
m_pts.clear();
}
void Stroke::reset(int n, const gfx::Point& point)
void Stroke::reset(int n, const Pt& pt)
{
m_points.resize(n, point);
m_pts.resize(n, pt);
}
void Stroke::addPoint(const gfx::Point& point)
void Stroke::addPoint(const Pt& pt)
{
m_points.push_back(point);
m_pts.push_back(pt);
}
void Stroke::offset(const gfx::Point& delta)
{
for (auto& p : m_points)
p += delta;
for (auto& p : m_pts) {
p.x += delta.x;
p.y += delta.y;
}
}
void Stroke::erase(int index)
{
ASSERT(0 <= index && index < m_points.size());
if (0 <= index && index < m_points.size())
m_points.erase(m_points.begin()+index);
ASSERT(0 <= index && index < m_pts.size());
if (0 <= index && index < m_pts.size())
m_pts.erase(m_pts.begin()+index);
}
gfx::Rect Stroke::bounds() const
{
if (m_points.empty())
if (m_pts.empty())
return gfx::Rect();
gfx::Point
minpt(m_points[0]),
maxpt(m_points[0]);
minpt(m_pts[0].x, m_pts[0].y),
maxpt(m_pts[0].x, m_pts[0].y);
for (std::size_t c=1; c<m_points.size(); ++c) {
int x = m_points[c].x;
int y = m_points[c].y;
for (std::size_t c=1; c<m_pts.size(); ++c) {
int x = m_pts[c].x;
int y = m_pts[c].y;
if (minpt.x > x) minpt.x = x;
if (minpt.y > y) minpt.y = y;
if (maxpt.x < x) maxpt.x = x;
@ -65,5 +67,18 @@ gfx::Rect Stroke::bounds() const
maxpt.y - minpt.y + 1);
}
std::vector<int> Stroke::toXYInts() const
{
std::vector<int> output;
if (!empty()) {
output.reserve(2*size());
for (auto pt : m_pts) {
output.push_back(pt.x);
output.push_back(pt.y);
}
}
return output;
}
} // namespace tools
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2001-2015 David Capello
//
// This program is distributed under the terms of
@ -19,36 +19,49 @@ namespace app {
class Stroke {
public:
typedef std::vector<gfx::Point> Points;
typedef Points::const_iterator const_iterator;
struct Pt {
int x = 0;
int y = 0;
float size = 0.0f;
float angle = 0.0f;
float gradient = 0.0f;
Pt() { }
Pt(const gfx::Point& point) : x(point.x), y(point.y) { }
Pt(int x, int y) : x(x), y(y) { }
gfx::Point toPoint() const { return gfx::Point(x, y); }
bool operator==(const Pt& that) const { return x == that.x && y == that.y; }
bool operator!=(const Pt& that) const { return x != that.x || y != that.y; }
};
typedef std::vector<Pt> Pts;
typedef Pts::const_iterator const_iterator;
const_iterator begin() const { return m_points.begin(); }
const_iterator end() const { return m_points.end(); }
const_iterator begin() const { return m_pts.begin(); }
const_iterator end() const { return m_pts.end(); }
bool empty() const { return m_points.empty(); }
int size() const { return (int)m_points.size(); }
bool empty() const { return m_pts.empty(); }
int size() const { return (int)m_pts.size(); }
const gfx::Point& operator[](int i) const { return m_points[i]; }
gfx::Point& operator[](int i) { return m_points[i]; }
const Pt& operator[](int i) const { return m_pts[i]; }
Pt& operator[](int i) { return m_pts[i]; }
const gfx::Point& firstPoint() const {
ASSERT(!m_points.empty());
return m_points[0];
const Pt& firstPoint() const {
ASSERT(!m_pts.empty());
return m_pts[0];
}
const gfx::Point& lastPoint() const {
ASSERT(!m_points.empty());
return m_points[m_points.size()-1];
const Pt& lastPoint() const {
ASSERT(!m_pts.empty());
return m_pts[m_pts.size()-1];
}
// Clears the whole stroke.
void reset();
// Reset the stroke as "n" points in the given "point" position.
void reset(int n, const gfx::Point& point);
// Reset the stroke as "n" points in the given point position.
void reset(int n, const Pt& pt);
// Adds a new point to the stroke.
void addPoint(const gfx::Point& point);
void addPoint(const Pt& pt);
// Displaces all X,Y coordinates the given delta.
void offset(const gfx::Point& delta);
@ -59,8 +72,10 @@ namespace app {
// Returns the bounds of the stroke (minimum/maximum position).
gfx::Rect bounds() const;
std::vector<int> toXYInts() const;
public:
Points m_points;
Pts m_pts;
};
typedef std::vector<Stroke> Strokes;

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2015-2017 David Capello
//
// This program is distributed under the terms of
@ -38,10 +39,9 @@ void HorizontalSymmetry::generateStrokes(const Stroke& mainStroke, Strokes& stro
Stroke stroke2;
for (const auto& pt : mainStroke) {
stroke2.addPoint(
gfx::Point(
m_x - ((pt.x-brushCenter) - m_x + 1) - (brushSize - brushCenter - 1),
pt.y));
Stroke::Pt pt2 = pt;
pt2.x = m_x - ((pt.x-brushCenter) - m_x + 1) - (brushSize - brushCenter - 1);
stroke2.addPoint(pt2);
}
strokes.push_back(stroke2);
}
@ -64,10 +64,9 @@ void VerticalSymmetry::generateStrokes(const Stroke& mainStroke, Strokes& stroke
Stroke stroke2;
for (const auto& pt : mainStroke) {
stroke2.addPoint(
gfx::Point(
pt.x,
m_y - ((pt.y-brushCenter) - m_y + 1) - (brushSize - brushCenter - 1)));
Stroke::Pt pt2 = pt;
pt2.y = m_y - ((pt.y-brushCenter) - m_y + 1) - (brushSize - brushCenter - 1);
stroke2.addPoint(pt2);
}
strokes.push_back(stroke2);
}

View File

@ -103,12 +103,7 @@ void ToolLoopManager::pressButton(const Pointer& pointer)
return;
}
// Convert the screen point to a sprite point
Point spritePoint = pointer.point();
m_toolLoop->setSpeed(Point(0, 0));
m_oldPoint = spritePoint;
snapToGrid(spritePoint);
Stroke::Pt spritePoint = getSpriteStrokePt(pointer, true);
m_toolLoop->getController()->pressButton(m_toolLoop, m_stroke, spritePoint);
std::string statusText;
@ -138,9 +133,7 @@ bool ToolLoopManager::releaseButton(const Pointer& pointer)
if (isCanceled())
return false;
Point spritePoint = pointer.point();
snapToGrid(spritePoint);
Stroke::Pt spritePoint = getSpriteStrokePt(pointer, false);
bool res = m_toolLoop->getController()->releaseButton(m_stroke, spritePoint);
if (!res && (m_toolLoop->getTracePolicy() == TracePolicy::Last ||
@ -162,18 +155,7 @@ void ToolLoopManager::movement(const Pointer& pointer)
if (isCanceled())
return;
// Convert the screen point to a sprite point
Point spritePoint = pointer.point();
// Calculate the velocity (new sprite point - old sprite point)
Point velocity = (spritePoint - m_oldPoint);
m_toolLoop->setSpeed(velocity);
m_oldPoint = spritePoint;
snapToGrid(spritePoint);
// Control dynamic parameters through sensors
if (m_dynamics.isDynamic())
adjustBrushWithDynamics(pointer, velocity);
Stroke::Pt spritePoint = getSpriteStrokePt(pointer, false);
m_toolLoop->getController()->movement(m_toolLoop, m_stroke, spritePoint);
std::string statusText;
@ -283,16 +265,19 @@ void ToolLoopManager::doLoopStep(bool lastStep)
}
// Applies the grid settings to the specified sprite point.
void ToolLoopManager::snapToGrid(Point& point)
void ToolLoopManager::snapToGrid(Stroke::Pt& pt)
{
if (!m_toolLoop->getController()->canSnapToGrid() ||
!m_toolLoop->getSnapToGrid() ||
m_toolLoop->isSelectingTiles())
return;
gfx::Point point(pt.x, pt.y);
point = snap_to_grid(m_toolLoop->getGridBounds(), point,
PreferSnapTo::ClosestGridVertex);
point += m_toolLoop->getBrush()->center();
pt.x = point.x;
pt.y = point.y;
}
// Strokes are relative to sprite origin.
@ -379,11 +364,58 @@ void ToolLoopManager::calculateDirtyArea(const Strokes& strokes)
}
}
void ToolLoopManager::adjustBrushWithDynamics(const Pointer& pointer,
const Point& velocity)
Stroke::Pt ToolLoopManager::getSpriteStrokePt(const Pointer& pointer,
const bool firstPoint)
{
int size = m_brush0.size();
int angle = m_brush0.angle();
const base::tick_t t = base::current_tick();
const base::tick_t dt = t - m_lastPointerT;
m_lastPointerT = t;
// Convert the screen point to a sprite point
Stroke::Pt spritePoint = pointer.point();
spritePoint.size = m_brush0.size();
spritePoint.angle = m_brush0.angle();
// Calculate the velocity (new sprite point - old sprite point)
gfx::Point newVelocity;
if (firstPoint)
m_velocity = newVelocity = gfx::Point(0, 0);
else {
newVelocity.x = (spritePoint.x - m_oldPoint.x);
newVelocity.y = (spritePoint.y - m_oldPoint.y);
float a = base::clamp(float(dt) / 50.0f, 0.0f, 1.0f);
m_velocity.x = (1.0f-a)*m_velocity.x + a*newVelocity.x;
m_velocity.y = (1.0f-a)*m_velocity.y + a*newVelocity.y;
}
m_oldPoint.x = spritePoint.x;
m_oldPoint.y = spritePoint.y;
// Center the input to some grid point if needed
snapToGrid(spritePoint);
// Control dynamic parameters through sensors
if (useDynamics()) {
adjustPointWithDynamics(pointer, spritePoint);
}
// Inform the original velocity vector to the ToolLoop
m_toolLoop->setSpeed(newVelocity);
return spritePoint;
}
bool ToolLoopManager::useDynamics() const
{
return (m_dynamics.isDynamic() &&
!m_toolLoop->getFilled() &&
m_toolLoop->getController()->isFreehand());
}
void ToolLoopManager::adjustPointWithDynamics(const Pointer& pointer,
Stroke::Pt& pt)
{
int size = pt.size;
int angle = pt.angle;
// Pressure
bool hasP = (pointer.type() == Pointer::Type::Pen ||
@ -392,8 +424,8 @@ void ToolLoopManager::adjustBrushWithDynamics(const Pointer& pointer,
ASSERT(p >= 0.0f && p <= 1.0f);
// Velocity
float v = float(std::sqrt(velocity.x*velocity.x +
velocity.y*velocity.y)) / 32.0f; // TODO 16 should be configurable
float v = float(std::sqrt(m_velocity.x*m_velocity.x +
m_velocity.y*m_velocity.y)) / 16.0f; // TODO 16 should be configurable
v = base::clamp(v, 0.0f, 1.0f);
switch (m_dynamics.size) {
@ -414,17 +446,18 @@ void ToolLoopManager::adjustBrushWithDynamics(const Pointer& pointer,
break;
}
size = base::clamp(size, int(Brush::kMinBrushSize), int(Brush::kMaxBrushSize));
angle = base::clamp(angle, -180, 180);
Brush* currrentBrush = m_toolLoop->getBrush();
if (currrentBrush->size() != size ||
(currrentBrush->type() != kCircleBrushType &&
currrentBrush->angle() != angle)) {
m_toolLoop->setBrush(
std::make_shared<Brush>(m_brush0.type(), size, angle));
switch (m_dynamics.gradient) {
case DynamicSensor::Pressure:
if (hasP)
pt.gradient = p;
break;
case DynamicSensor::Velocity:
pt.gradient = v;
break;
}
pt.size = base::clamp(size, int(Brush::kMinBrushSize), int(Brush::kMaxBrushSize));
pt.angle = base::clamp(angle, -180, 180);
}
} // namespace tools

View File

@ -12,6 +12,7 @@
#include "app/tools/dynamics.h"
#include "app/tools/pointer.h"
#include "app/tools/stroke.h"
#include "base/time.h"
#include "doc/brush.h"
#include "gfx/point.h"
#include "gfx/region.h"
@ -73,9 +74,11 @@ public:
private:
void doLoopStep(bool lastStep);
void snapToGrid(gfx::Point& point);
void adjustBrushWithDynamics(const Pointer& pointer,
const gfx::Point& velocity);
void snapToGrid(Stroke::Pt& pt);
Stroke::Pt getSpriteStrokePt(const Pointer& pointer,
const bool firstPoint);
bool useDynamics() const;
void adjustPointWithDynamics(const Pointer& pointer, Stroke::Pt& pt);
void calculateDirtyArea(const Strokes& strokes);
@ -83,6 +86,8 @@ private:
Stroke m_stroke;
Pointer m_lastPointer;
gfx::Point m_oldPoint;
gfx::Point m_velocity;
base::tick_t m_lastPointerT;
gfx::Region m_dirtyArea;
gfx::Region m_nextDirtyArea;
doc::Brush m_brush0;

View File

@ -1878,6 +1878,10 @@ void ContextBar::updateForTool(tools::Tool* tool)
(tool->getInk(0)->withDitheringOptions() ||
tool->getInk(1)->withDitheringOptions());
// True if the brush supports dynamics
// TODO add support for dynamics in custom brushes in the future
const bool supportDynamics = (!hasImageBrush);
// Show/Hide fields
m_zoomButtons->setVisible(needZoomButtons);
m_brushBack->setVisible(supportOpacity && hasImageBrush && !withDithering);
@ -1891,6 +1895,7 @@ void ContextBar::updateForTool(tools::Tool* tool)
m_inkShades->setVisible(hasInkShades);
m_eyedropperField->setVisible(isEyedropper);
m_autoSelectLayer->setVisible(isMove);
m_dynamics->setVisible(isFreehand && supportDynamics);
m_freehandBox->setVisible(isFreehand && supportOpacity);
m_toleranceLabel->setVisible(hasTolerance);
m_tolerance->setVisible(hasTolerance);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -258,10 +258,12 @@ void BrushPreview::show(const gfx::Point& screenPos)
loop->getController()->prepareController(loop.get());
loop->getIntertwine()->prepareIntertwine();
loop->getPointShape()->preparePointShape(loop.get());
loop->getPointShape()->transformPoint(
loop.get(),
brushBounds.x-origBrushBounds.x,
brushBounds.y-origBrushBounds.y);
tools::Stroke::Pt pt(brushBounds.x-origBrushBounds.x,
brushBounds.y-origBrushBounds.y);
pt.size = brush->size();
pt.angle = brush->angle();
loop->getPointShape()->transformPoint(loop.get(), pt);
}
}

View File

@ -162,6 +162,18 @@ public:
m_tracePolicy = tools::TracePolicy::Accumulate;
break;
}
// Use overlap trace policy for dynamic gradient
auto dynamics = getDynamics();
if (dynamics.isDynamic() &&
dynamics.gradient != tools::DynamicSensor::Static &&
m_controller->isFreehand()) {
// Use overlap trace policy to accumulate changes of colors
// between stroke points.
//
// TODO this is connected with a condition in tools::PaintInk::prepareInk()
m_tracePolicy = tools::TracePolicy::Overlap;
}
}
// Symmetry mode
@ -371,7 +383,6 @@ public:
#endif
}
void onSliceRect(const gfx::Rect& bounds) override { }
};
@ -521,7 +532,7 @@ public:
if (m_saveLastPoint) {
m_tx(new cmd::SetLastPoint(
m_document,
getController()->getLastPoint()));
getController()->getLastPoint().toPoint()));
}
// Paint ink
@ -863,6 +874,11 @@ public:
void cancel() override { }
bool isCanceled() override { return true; }
tools::DynamicsOptions getDynamics() override {
// Preview without dynamics
return tools::DynamicsOptions();
}
};
tools::ToolLoop* create_tool_loop_preview(

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -42,6 +42,7 @@ namespace doc {
BrushPattern pattern() const { return m_pattern; }
gfx::Point patternOrigin() const { return m_patternOrigin; }
Image* patternImage() const { return m_patternImage.get(); }
const gfx::Rect& bounds() const { return m_bounds; }
const gfx::Point& center() const { return m_center; }
@ -58,6 +59,9 @@ namespace doc {
void setPatternOrigin(const gfx::Point& patternOrigin) {
m_patternOrigin = patternOrigin;
}
void setPatternImage(ImageRef& patternImage) {
m_patternImage = patternImage;
}
void setCenter(const gfx::Point& center);
private:
@ -74,6 +78,7 @@ namespace doc {
gfx::Point m_center;
BrushPattern m_pattern; // How the image should be replicated
gfx::Point m_patternOrigin; // From what position the brush was taken
ImageRef m_patternImage;
int m_gen;
// Extra data used for setImageColor()

View File

@ -1,4 +1,5 @@
// Aseprite Render Library
// Copyright (c) 2020 Igara Studio S.A.
// Copyright (c) 2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -8,6 +9,7 @@
#define RENDER_DITHERING_MATRIX_H_INCLUDED
#pragma once
#include <algorithm>
#include <vector>
namespace render {

View File

@ -1,5 +1,5 @@
// Aseprite Render Library
// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2019-2020 Igara Studio S.A.
// Copyright (c) 2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -224,4 +224,57 @@ void render_rgba_radial_gradient(
}
}
template<typename ImageTraits>
static void create_dithering_pattern_templ(
doc::Image* pattern,
const render::DitheringMatrix& matrix,
const float f,
const doc::color_t c0,
const doc::color_t c1)
{
const int w = pattern->width();
const int h = pattern->height();
doc::LockImageBits<ImageTraits> dstBits(pattern);
auto dst = dstBits.begin();
for (int y=0; y<h; ++y) {
for (int x=0; x<w; ++x, ++dst)
*dst = (f*(matrix.maxValue()+2) < matrix(y, x)+1 ? c0: c1);
}
}
void convert_bitmap_brush_to_dithering_brush(
doc::Brush* brush,
const doc::PixelFormat pixelFormat,
const render::DitheringMatrix& matrix,
const float f,
const doc::color_t c0,
const doc::color_t c1)
{
// Create a pattern
doc::ImageRef pattern(
doc::Image::create(pixelFormat,
matrix.cols(), matrix.rows()));
switch (pixelFormat) {
case doc::IMAGE_RGB:
create_dithering_pattern_templ<doc::RgbTraits>(
pattern.get(), matrix, f, c0, c1);
break;
case doc::IMAGE_GRAYSCALE:
create_dithering_pattern_templ<doc::GrayscaleTraits>(
pattern.get(), matrix, f, c0, c1);
break;
case doc::IMAGE_INDEXED:
create_dithering_pattern_templ<doc::IndexedTraits>(
pattern.get(), matrix, f, c0, c1);
break;
}
doc::ImageRef copy(doc::Image::createCopy(brush->image()));
brush->setImage(copy.get(), copy.get());
brush->setPatternImage(pattern);
brush->setPattern(doc::BrushPattern::PAINT_BRUSH);
}
} // namespace render

View File

@ -1,5 +1,5 @@
// Aseprite Render Library
// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2019-2020 Igara Studio S.A.
// Copyright (c) 2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -9,6 +9,7 @@
#define RENDER_GRADIENT_H_INCLUDED
#pragma once
#include "doc/brush.h"
#include "doc/color.h"
#include "gfx/point.h"
@ -53,6 +54,14 @@ void render_rgba_radial_gradient(
doc::color_t c1,
const render::DitheringMatrix& matrix);
void convert_bitmap_brush_to_dithering_brush(
doc::Brush* brush,
const doc::PixelFormat pixelFormat,
const render::DitheringMatrix& matrix,
const float f,
const doc::color_t c0,
const doc::color_t c1);
} // namespace render
#endif