Fix interpolation between stroke points with line brush (fix #728)

Related to #245, and there are still problems with gaps using new
dynamic angle parameters.
This commit is contained in:
David Capello 2020-04-23 11:30:36 -03:00
parent 3b370c2ff5
commit d1843fcf55
6 changed files with 175 additions and 43 deletions

View File

@ -16,8 +16,11 @@
#include "app/tools/stroke.h"
#include "app/tools/symmetry.h"
#include "app/tools/tool_loop.h"
#include "base/pi.h"
#include "doc/algo.h"
#include <cmath>
namespace app {
namespace tools {
@ -101,26 +104,60 @@ void Intertwine::doPointshapeHline(int x1, int y, int x2, ToolLoop* loop)
}
// static
void Intertwine::doPointshapeLine(int x1, int y1, int x2, int y2, ToolLoop* loop)
void Intertwine::doPointshapeLineWithoutDynamics(int x1, int y1, int x2, int y2, ToolLoop* loop)
{
doc::AlgoLineWithAlgoPixel algo = getLineAlgo(loop);
algo(x1, y1, x2, y2, (void*)loop, (AlgoPixel)doPointshapePoint);
Stroke::Pt a(x1, y1);
Stroke::Pt b(x2, y2);
a.size = b.size = loop->getBrush()->size();
a.angle = b.angle = loop->getBrush()->angle();
doPointshapeLine(a, b, loop);
}
void Intertwine::doPointshapeLine(const Stroke::Pt& a,
const Stroke::Pt& b, ToolLoop* loop)
{
doc::AlgoLineWithAlgoPixel algo = getLineAlgo(loop, a, b);
LineData lineData(loop, a, b);
algo(a.x, a.y, b.x, b.y, (void*)&lineData, (AlgoPixel)doPointshapePointDynamics);
}
// static
doc::AlgoLineWithAlgoPixel Intertwine::getLineAlgo(ToolLoop* loop)
doc::AlgoLineWithAlgoPixel Intertwine::getLineAlgo(ToolLoop* loop,
const Stroke::Pt& a,
const Stroke::Pt& b)
{
bool needsFixForLineBrush = false;
if (loop->getBrush()->type() == kLineBrushType) {
if ((a.angle != 0.0f || b.angle != 0.0f) &&
(a.angle != b.angle)) {
needsFixForLineBrush = true;
}
else {
int angle = a.angle;
int p = SGN(b.x - a.x);
int q = SGN(a.y - b.y);
float rF = std::cos(PI * angle / 180);
float sF = std::sin(PI * angle / 180);
int r = SGN(rF);
int s = SGN(sF);
needsFixForLineBrush = ((p == q && r != s) ||
(p != q && r == s));
}
}
if (// When "Snap Angle" in being used or...
(int(loop->getModifiers()) & int(ToolLoopModifiers::kSquareAspect)) ||
// "Snap to Grid" is enabled
(loop->getController()->canSnapToGrid() && loop->getSnapToGrid())) {
// We prefer the perfect pixel lines that matches grid tiles
return algo_line_perfect;
return (needsFixForLineBrush ? algo_line_perfect_with_fix_for_line_brush:
algo_line_perfect);
}
else {
// In other case we use the regular algorithm that is useful to
// draw continuous lines/strokes.
return algo_line_continuous;
return (needsFixForLineBrush ? algo_line_continuous_with_fix_for_line_brush:
algo_line_continuous);
}
}

View File

@ -48,9 +48,14 @@ namespace app {
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);
// TODO We should remove this function and always use dynamics
static void doPointshapeLineWithoutDynamics(int x1, int y1, int x2, int y2, ToolLoop* loop);
static void doPointshapeLine(const Stroke::Pt& a,
const Stroke::Pt& b, ToolLoop* loop);
static doc::AlgoLineWithAlgoPixel getLineAlgo(ToolLoop* loop);
static doc::AlgoLineWithAlgoPixel getLineAlgo(ToolLoop* loop,
const Stroke::Pt& a,
const Stroke::Pt& b);
};
} // namespace tools

View File

@ -115,8 +115,8 @@ public:
}
else {
Stroke pts;
doc::AlgoLineWithAlgoPixel lineAlgo = getLineAlgo(loop);
for (int c=0; c+1<stroke.size(); ++c) {
auto lineAlgo = getLineAlgo(loop, stroke[c], stroke[c+1]);
LineData2 lineData(loop, stroke[c], stroke[c+1], pts);
lineAlgo(stroke[c].x, stroke[c].y,
stroke[c+1].x, stroke[c+1].y,
@ -143,9 +143,7 @@ public:
// contour tool, with brush type = kImageBrush with alpha content and
// with not Pixel Perfect pencil mode.
if (loop->getFilled() && !loop->getController()->isFreehand()) {
doPointshapeLine(stroke[stroke.size()-1].x,
stroke[stroke.size()-1].y,
stroke[0].x, stroke[0].y, loop);
doPointshapeLine(stroke[stroke.size()-1], stroke[0], loop);
}
}
m_firstStroke = false;
@ -191,6 +189,7 @@ public:
}
else if (stroke.size() >= 2) {
for (int c=0; c+1<stroke.size(); ++c) {
// TODO fix this with strokes and dynamics
int x1 = stroke[c].x;
int y1 = stroke[c].y;
int x2 = stroke[c+1].x;
@ -202,8 +201,8 @@ public:
const double angle = loop->getController()->getShapeAngle();
if (ABS(angle) < 0.001) {
doPointshapeLine(x1, y1, x2, y1, loop);
doPointshapeLine(x1, y2, x2, y2, loop);
doPointshapeLineWithoutDynamics(x1, y1, x2, y1, loop);
doPointshapeLineWithoutDynamics(x1, y2, x2, y2, loop);
for (y=y1; y<=y2; y++) {
doPointshapePoint(x1, y, loop);
@ -214,11 +213,9 @@ public:
Stroke p = rotateRectangle(x1, y1, x2, y2, angle);
int n = p.size();
for (int i=0; i+1<n; ++i) {
doPointshapeLine(p[i].x, p[i].y,
p[i+1].x, p[i+1].y, loop);
doPointshapeLine(p[i], p[i+1], loop);
}
doPointshapeLine(p[n-1].x, p[n-1].y,
p[0].x, p[0].y, loop);
doPointshapeLine(p[n-1], p[0], loop);
}
}
}
@ -243,7 +240,7 @@ public:
const double angle = loop->getController()->getShapeAngle();
if (ABS(angle) < 0.001) {
for (y=y1; y<=y2; y++)
doPointshapeLine(x1, y, x2, y, loop);
doPointshapeLineWithoutDynamics(x1, y, x2, y, loop);
}
else {
Stroke p = rotateRectangle(x1, y1, x2, y2, angle);
@ -398,25 +395,24 @@ public:
for (int c=0; c<stroke.size(); c += 4) {
if (stroke.size()-c == 1) {
doPointshapePoint(stroke[c].x, stroke[c].y, loop);
doPointshapeStrokePt(stroke[c], loop);
}
else if (stroke.size()-c == 2) {
doPointshapeLine(stroke[c].x, stroke[c].y,
stroke[c+1].x, stroke[c+1].y, loop);
doPointshapeLine(stroke[c], stroke[c+1], loop);
}
else if (stroke.size()-c == 3) {
algo_spline(stroke[c ].x, stroke[c ].y,
stroke[c+1].x, stroke[c+1].y,
stroke[c+1].x, stroke[c+1].y,
stroke[c+2].x, stroke[c+2].y, loop,
(AlgoLine)doPointshapeLine);
(AlgoLine)doPointshapeLineWithoutDynamics);
}
else {
algo_spline(stroke[c ].x, stroke[c ].y,
stroke[c+1].x, stroke[c+1].y,
stroke[c+2].x, stroke[c+2].y,
stroke[c+3].x, stroke[c+3].y, loop,
(AlgoLine)doPointshapeLine);
(AlgoLine)doPointshapeLineWithoutDynamics);
}
}
}
@ -465,8 +461,9 @@ public:
}
else {
for (int c=0; c+1<stroke.size(); ++c) {
auto lineAlgo = getLineAlgo(loop, stroke[c], stroke[c+1]);
LineData2 lineData(loop, stroke[c], stroke[c+1], m_pts);
algo_line_continuous(
lineAlgo(
stroke[c].x,
stroke[c].y,
stroke[c+1].x,
@ -476,17 +473,27 @@ public:
}
}
for (int 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) {
m_pts.erase(c);
// For line brush type, the pixel-perfect will create gaps so we
// avoid removing points
if (loop->getBrush()->type() != kLineBrushType ||
(loop->getDynamics().angle == tools::DynamicSensor::Static &&
(loop->getBrush()->angle() == 0.0f ||
loop->getBrush()->angle() == 90.0f ||
loop->getBrush()->angle() == 180.0f))) {
for (int 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) {
m_pts.erase(c);
}
}
}
for (int c=0; c<m_pts.size(); ++c) {
// We must ignore to print the first point of the line after
// a joinStroke pass with a retained "Last" trace policy
// (i.e. the user confirms draw a line while he is holding

View File

@ -101,6 +101,7 @@ protected:
doc::color_t m_bgColor;
doc::color_t m_primaryColor;
doc::color_t m_secondaryColor;
tools::DynamicsOptions m_dynamics;
public:
ToolLoopBase(Editor* editor, Site site,
@ -144,6 +145,11 @@ public:
, m_primaryColor(button == tools::ToolLoop::Left ? m_fgColor: m_bgColor)
, m_secondaryColor(button == tools::ToolLoop::Left ? m_bgColor: m_fgColor)
{
#ifdef ENABLE_UI // TODO add dynamics support when UI is not enabled
if (m_controller->isFreehand())
m_dynamics = App::instance()->contextBar()->getDynamics();
#endif
if (m_tracePolicy == tools::TracePolicy::Accumulate ||
m_tracePolicy == tools::TracePolicy::AccumulateUpdateLast) {
tools::ToolBox* toolbox = App::instance()->toolBox();
@ -164,9 +170,8 @@ public:
}
// Use overlap trace policy for dynamic gradient
auto dynamics = getDynamics();
if (dynamics.isDynamic() &&
dynamics.gradient != tools::DynamicSensor::Static &&
if (m_dynamics.isDynamic() &&
m_dynamics.gradient != tools::DynamicSensor::Static &&
m_controller->isFreehand()) {
// Use overlap trace policy to accumulate changes of colors
// between stroke points.
@ -376,11 +381,7 @@ public:
}
tools::DynamicsOptions getDynamics() override {
#ifdef ENABLE_UI // TODO add support when UI is not enabled
return App::instance()->contextBar()->getDynamics();
#else
return tools::DynamicsOptions();
#endif
return m_dynamics;
}
void onSliceRect(const gfx::Rect& bounds) override { }

View File

@ -65,6 +65,54 @@ void algo_line_perfect(int x1, int y1, int x2, int y2, void* data, AlgoPixel pro
}
}
// Special version of the perfect line algorithm specially done for
// kLineBrushType so the whole line looks continuous without holes.
//
// TOOD in a future we should convert lines into scanlines and render
// scanlines instead of drawing the brush on each pixel, that
// would fix all cases
void algo_line_perfect_with_fix_for_line_brush(int x1, int y1, int x2, int y2, void* data, AlgoPixel proc)
{
bool yaxis;
if (ABS(y2-y1) > ABS(x2-x1)) {
std::swap(x1, y1);
std::swap(x2, y2);
yaxis = true;
}
else
yaxis = false;
const int w = ABS(x2-x1)+1;
const int h = ABS(y2-y1)+1;
const int dx = SGN(x2-x1);
const int dy = SGN(y2-y1);
int e = 0;
int y = y1;
x2 += dx;
for (int x=x1; x!=x2; x+=dx) {
if (yaxis)
proc(y, x, data);
else
proc(x, y, data);
e += h;
if (e >= w) {
y += dy;
e -= w;
if (x+dx != x2) {
if (yaxis)
proc(y, x, data);
else
proc(x, y, data);
}
}
}
}
// Line code based on Alois Zingl work released under the
// MIT license http://members.chello.at/easyfilter/bresenham.html
void algo_line_continuous(int x0, int y0, int x1, int y1, void* data, AlgoPixel proc)
@ -91,6 +139,38 @@ void algo_line_continuous(int x0, int y0, int x1, int y1, void* data, AlgoPixel
}
}
// Special version of the continuous line algorithm specially done for
// kLineBrushType so the whole line looks continuous without holes.
void algo_line_continuous_with_fix_for_line_brush(int x0, int y0, int x1, int y1, void* data, AlgoPixel proc)
{
int dx = ABS(x1-x0), sx = (x0 < x1 ? 1: -1);
int dy = -ABS(y1-y0), sy = (y0 < y1 ? 1: -1);
int err = dx+dy, e2; // error value e_xy
bool x_changed;
for (;;) {
x_changed = false;
proc(x0, y0, data);
e2 = 2*err;
if (e2 >= dy) { // e_xy+e_x > 0
if (x0 == x1)
break;
err += dy;
x0 += sx;
x_changed = true;
}
if (e2 <= dx) { // e_xy+e_y < 0
if (y0 == y1)
break;
err += dx;
if (x_changed)
proc(x0, y0, data);
y0 += sy;
}
}
}
// Ellipse code based on Alois Zingl work released under the MIT
// license http://members.chello.at/easyfilter/bresenham.html
//

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2018-2019 Igara Studio S.A.
// Copyright (C) 2018-2020 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -26,6 +26,7 @@ namespace doc {
//
// Related to: https://github.com/aseprite/aseprite/issues/1395
void algo_line_perfect(int x1, int y1, int x2, int y2, void* data, AlgoPixel proc);
void algo_line_perfect_with_fix_for_line_brush(int x1, int y1, int x2, int y2, void *data, AlgoPixel proc);
// Useful to create continuous lines (you can draw from one point to
// another, and continue from that point to another in the same
@ -35,6 +36,7 @@ namespace doc {
// https://community.aseprite.org/t/1045
// https://github.com/aseprite/aseprite/issues/1894
void algo_line_continuous(int x1, int y1, int x2, int y2, void *data, AlgoPixel proc);
void algo_line_continuous_with_fix_for_line_brush(int x1, int y1, int x2, int y2, void *data, AlgoPixel proc);
void algo_ellipse(int x1, int y1, int x2, int y2, void *data, AlgoPixel proc);
void algo_ellipsefill(int x1, int y1, int x2, int y2, void *data, AlgoHLine proc);