Commit 7ec11f56 authored by Halla Rempt's avatar Halla Rempt
Browse files

Implement line smoothing in Krita

REVIEW:108049
BUG:281267
parent d9eaa063
......@@ -111,7 +111,7 @@ public:
/// Number of ms since the beginning of the stroke
int currentTime() const;
void toXML(QDomDocument&, QDomElement&) const;
static KisPaintInformation fromXML(const QDomElement&);
......
......@@ -24,9 +24,23 @@
* to be passed for the next call.
*/
struct KisDistanceInformation {
KisDistanceInformation() : distance(0), spacing(0) {}
KisDistanceInformation(double _distance, double _spacing) : distance(_distance), spacing(_spacing) {}
void clear() { distance = 0; spacing = 0;}
KisDistanceInformation()
: distance(0)
, spacing(0)
{}
KisDistanceInformation(double _distance, double _spacing)
: distance(_distance)
, spacing(_spacing)
{}
void clear()
{
distance = 0;
spacing = 0;
}
double distance;
double spacing;
};
......
......@@ -30,10 +30,12 @@ KisDynamicSensorSpeed::KisDynamicSensorSpeed() : KisDynamicSensor(SpeedId)
}
qreal KisDynamicSensorSpeed::value(const KisPaintInformation& info) {
int dt = qMax(1, info.currentTime() - m_lastTime); // make sure dt > 1
int deltaTime = qMax(1, info.currentTime() - m_lastTime); // make sure deltaTime > 1
m_lastTime = info.currentTime();
double currentMove = info.movement().norm() / dt;
m_speed = qMin(1.0, (m_speed * 0.9 + currentMove * 0.1)); // average it to get nicer result, at the price of being less mathematically correct, but we quicly reach a situation where dt = 1 and currentMove = 1
double currentMove = info.movement().norm() / deltaTime;
// Average it to get nicer result, at the price of being less mathematically correct,
// but we quickly reach a situation where dt = 1 and currentMove = 1
m_speed = qMin(1.0, (m_speed * 0.9 + currentMove * 0.1));
return m_speed;
}
......
......@@ -21,19 +21,22 @@
#include "kis_tool_brush.h"
#include <QCheckBox>
#include <QComboBox>
#include <klocale.h>
#include "kis_cursor.h"
#include "kis_slider_spin_box.h"
#define MAXIMUM_SMOOTHNESS 1000
#define MAXIMUM_SMOOTHNESS_QUALITY 100 // 0..100
#define MAXIMUM_SMOOTHNESS_FACTOR 1000.0 // 0..1000.0 == weight in gui
#define MAXIMUM_MAGNETISM 1000
KisToolBrush::KisToolBrush(KoCanvasBase * canvas)
: KisToolFreehand(canvas, KisCursor::load("tool_freehand_cursor.png", 5, 5), i18nc("(qtundo-format)", "Brush"))
: KisToolFreehand(canvas,
KisCursor::load("tool_freehand_cursor.png", 5, 5),
i18nc("(qtundo-format)", "Brush"))
{
setObjectName("tool_brush");
}
......@@ -42,9 +45,35 @@ KisToolBrush::~KisToolBrush()
{
}
void KisToolBrush::slotSetSmoothness(int smoothness)
void KisToolBrush::slotSetSmoothingType(int index)
{
m_smoothness = smoothness / (double)MAXIMUM_SMOOTHNESS;
switch (index) {
case 0:
m_smoothingOptions.smoothingType = KisSmoothingOptions::NO_SMOOTHING;
m_sliderSmoothnessFactor->setEnabled(false);
m_sliderSmoothnessQuality->setEnabled(false);
break;
case 1:
m_smoothingOptions.smoothingType = KisSmoothingOptions::SIMPLE_SMOOTHING;
m_sliderSmoothnessFactor->setEnabled(false);
m_sliderSmoothnessQuality->setEnabled(false);
break;
case 2:
default:
m_smoothingOptions.smoothingType = KisSmoothingOptions::WEIGHTED_SMOOTHING;
m_sliderSmoothnessFactor->setEnabled(true);
m_sliderSmoothnessQuality->setEnabled(true);
}
}
void KisToolBrush::slotSetSmoothnessQuality(int quality)
{
m_smoothingOptions.smoothnessQuality = quality;
}
void KisToolBrush::slotSetSmoothnessFactor(qreal factor)
{
m_smoothingOptions.smoothnessFactor = factor;
}
void KisToolBrush::slotSetMagnetism(int magnetism)
......@@ -54,23 +83,30 @@ void KisToolBrush::slotSetMagnetism(int magnetism)
QWidget * KisToolBrush::createOptionWidget()
{
QWidget * optionWidget = KisToolFreehand::createOptionWidget();
optionWidget->setObjectName(toolId() + "option widget");
m_chkSmooth = new QCheckBox(i18nc("smooth out the curves while drawing", "Smoothness:"), optionWidget);
m_chkSmooth->setObjectName("chkSmooth");
m_chkSmooth->setChecked(m_smooth);
connect(m_chkSmooth, SIGNAL(toggled(bool)), this, SLOT(setSmooth(bool)));
// Line smoothing configuration
m_cmbSmoothingType = new QComboBox(optionWidget);
m_cmbSmoothingType->addItems(QStringList() << i18n("No Smoothing") << i18n("Basic Smoothing") << i18n("Weighted Smoothing"));
m_cmbSmoothingType->setCurrentIndex(2);
connect(m_cmbSmoothingType, SIGNAL(currentIndexChanged(int)), this, SLOT(slotSetSmoothingType(int)));
addOptionWidgetOption(m_cmbSmoothingType);
m_sliderSmoothnessQuality = new KisSliderSpinBox(optionWidget);
m_sliderSmoothnessQuality->setRange(1, MAXIMUM_SMOOTHNESS_QUALITY);
m_sliderSmoothnessQuality->setEnabled(true);
connect(m_sliderSmoothnessQuality, SIGNAL(valueChanged(int)), SLOT(slotSetSmoothnessQuality(int)));
m_sliderSmoothnessQuality->setValue(m_smoothingOptions.smoothnessQuality);
addOptionWidgetOption(m_sliderSmoothnessQuality, new QLabel(i18n("Quality:")));
m_sliderSmoothness = new KisSliderSpinBox(optionWidget);
m_sliderSmoothness->setRange(0, MAXIMUM_SMOOTHNESS);
m_sliderSmoothness->setEnabled(true);
connect(m_chkSmooth, SIGNAL(toggled(bool)), m_sliderSmoothness, SLOT(setEnabled(bool)));
connect(m_sliderSmoothness, SIGNAL(valueChanged(int)), SLOT(slotSetSmoothness(int)));
m_sliderSmoothness->setValue(m_smoothness * MAXIMUM_SMOOTHNESS);
m_sliderSmoothnessFactor = new KisDoubleSliderSpinBox(optionWidget);
m_sliderSmoothnessFactor->setRange(3.0, MAXIMUM_SMOOTHNESS_FACTOR, 1);
m_sliderSmoothnessFactor->setEnabled(true);
connect(m_sliderSmoothnessFactor, SIGNAL(valueChanged(qreal)), SLOT(slotSetSmoothnessFactor(qreal)));
m_sliderSmoothnessFactor->setValue(m_smoothingOptions.smoothnessFactor);
addOptionWidgetOption(m_sliderSmoothness, m_chkSmooth);
addOptionWidgetOption(m_sliderSmoothnessFactor, new QLabel(i18n("Weight:")));
// Drawing assistant configuration
m_chkAssistant = new QCheckBox(i18n("Assistant:"), optionWidget);
......@@ -78,7 +114,7 @@ QWidget * KisToolBrush::createOptionWidget()
connect(m_chkAssistant, SIGNAL(toggled(bool)), this, SLOT(setAssistant(bool)));
m_sliderMagnetism = new KisSliderSpinBox(optionWidget);
m_sliderMagnetism->setToolTip(i18n("Assistant Magnetism"));
m_sliderMagnetism->setRange(0, MAXIMUM_SMOOTHNESS);
m_sliderMagnetism->setRange(0, MAXIMUM_MAGNETISM);
m_sliderMagnetism->setEnabled(false);
connect(m_chkAssistant, SIGNAL(toggled(bool)), m_sliderMagnetism, SLOT(setEnabled(bool)));
m_sliderMagnetism->setValue(m_magnetism * MAXIMUM_MAGNETISM);
......
......@@ -32,6 +32,7 @@ class QGridLayout;
class KoCanvasBase;
class KisSliderSpinBox;
class KisDoubleSliderSpinBox;
class KisToolBrush : public KisToolFreehand
{
......@@ -44,15 +45,19 @@ public:
QWidget * createOptionWidget();
private slots:
void slotSetSmoothness(int smoothness);
void slotSetSmoothnessQuality(int quality);
void slotSetSmoothnessFactor(qreal factor);
void slotSetMagnetism(int magnetism);
void slotSetSmoothingType(int index);
private:
QGridLayout *m_optionLayout;
QCheckBox *m_chkSmooth;
QComboBox *m_cmbSmoothingType;
QCheckBox *m_chkAssistant;
KisSliderSpinBox *m_sliderMagnetism;
KisSliderSpinBox *m_sliderSmoothness;
KisDoubleSliderSpinBox *m_sliderSmoothnessFactor;
KisSliderSpinBox *m_sliderSmoothnessQuality;
};
......
......@@ -147,6 +147,7 @@ set(kritaui_LIB_SRCS
tool/kis_tool_polyline_base.cpp
tool/kis_color_picker_utils.cpp
tool/kis_resources_snapshot.cpp
tool/kis_smoothing_options.cpp
tool/strokes/freehand_stroke.cpp
tool/strokes/kis_painter_based_stroke_strategy.cpp
widgets/kis_channelflags_widget.cpp
......
/*
* Copyright (c) 2012 Boudewijn Rempt <boud@valdyas.org>
*
* 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "kis_smoothing_options.h"
KisSmoothingOptions::KisSmoothingOptions()
: smoothingType(WEIGHTED_SMOOTHING)
, smoothnessFactor(50.0)
, smoothnessQuality(20)
{
}
/*
* Copyright (c) 2012 Boudewijn Rempt <boud@valdyas.org>
*
* 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef KIS_SMOOTHING_OPTIONS_H
#define KIS_SMOOTHING_OPTIONS_H
#include <qglobal.h>
struct KisSmoothingOptions
{
enum SmoothingType {
NO_SMOOTHING = 0,
SIMPLE_SMOOTHING,
WEIGHTED_SMOOTHING
};
KisSmoothingOptions();
SmoothingType smoothingType;
qreal smoothnessFactor;
int smoothnessQuality;
};
#endif // KIS_SMOOTHING_OPTIONS_H
......@@ -71,9 +71,7 @@ KisToolFreehand::KisToolFreehand(KoCanvasBase * canvas, const QCursor & cursor,
{
m_explicitShowOutline = false;
m_smooth = true;
m_assistant = false;
m_smoothness = 1.0;
m_magnetism = 1.0;
setSupportOutline(true);
......@@ -160,7 +158,7 @@ void KisToolFreehand::initStroke(KoPointerEvent *event)
{
setCurrentNodeLocked(true);
m_helper->setSmoothness(m_smooth, m_smoothness);
m_helper->setSmoothness(m_smoothingOptions);
m_helper->initPaint(event, canvas()->resourceManager(),
image(),
image().data(),
......@@ -336,11 +334,6 @@ bool KisToolFreehand::wantsAutoScroll() const
return false;
}
void KisToolFreehand::setSmooth(bool smooth)
{
m_smooth = smooth;
}
void KisToolFreehand::setAssistant(bool assistant)
{
m_assistant = assistant;
......
......@@ -25,6 +25,7 @@
#include "kis_resources_snapshot.h"
#include "kis_paintop_settings.h"
#include "kis_distance_information.h"
#include "kis_smoothing_options.h"
#include "krita_export.h"
......@@ -83,7 +84,6 @@ protected:
protected slots:
void setSmooth(bool smooth);
void setAssistant(bool assistant);
private:
......@@ -114,8 +114,8 @@ private slots:
void hideOutline();
protected:
bool m_smooth;
double m_smoothness;
KisSmoothingOptions m_smoothingOptions;
bool m_assistant;
double m_magnetism;
......
......@@ -30,7 +30,10 @@
#include "kis_recording_adapter.h"
#include "kis_image.h"
#include "kis_painter.h"
#include "kis_smoothing_options.h"
#include <math.h>
#include <qnumeric.h> // for qIsNaN
struct KisToolFreehandHelper::Private
{
......@@ -41,7 +44,6 @@ struct KisToolFreehandHelper::Private
bool haveTangent;
QPointF previousTangent;
bool hasPaintAtLeastOnce;
QTime strokeTime;
......@@ -54,10 +56,12 @@ struct KisToolFreehandHelper::Private
KisPaintInformation previousPaintInformation;
KisPaintInformation olderPaintInformation;
bool smooth;
qreal smoothness;
KisSmoothingOptions smoothingOptions;
QTimer airbrushingTimer;
QList<KisPaintInformation> history;
QList<qreal> velocityHistory;
};
......@@ -68,9 +72,6 @@ KisToolFreehandHelper::KisToolFreehandHelper(KisPaintingInformationBuilder *info
m_d->infoBuilder = infoBuilder;
m_d->recordingAdapter = recordingAdapter;
m_d->smooth = true;
m_d->smoothness = 1.0;
m_d->strokeTimeoutTimer.setSingleShot(true);
connect(&m_d->strokeTimeoutTimer, SIGNAL(timeout()), SLOT(finishStroke()));
......@@ -82,10 +83,9 @@ KisToolFreehandHelper::~KisToolFreehandHelper()
delete m_d;
}
void KisToolFreehandHelper::setSmoothness(bool smooth, qreal smoothness)
void KisToolFreehandHelper::setSmoothness(const KisSmoothingOptions &smoothingOptions)
{
m_d->smooth = smooth;
m_d->smoothness = smoothness;
m_d->smoothingOptions = smoothingOptions;
}
void KisToolFreehandHelper::initPaint(KoPointerEvent *event,
......@@ -99,7 +99,6 @@ void KisToolFreehandHelper::initPaint(KoPointerEvent *event,
m_d->strokesFacade = strokesFacade;
m_d->haveTangent = false;
m_d->previousTangent = QPointF();
......@@ -123,14 +122,18 @@ void KisToolFreehandHelper::initPaint(KoPointerEvent *event,
}
KisStrokeStrategy *stroke =
new FreehandStrokeStrategy(indirectPainting,
m_d->resources, m_d->painterInfos, i18n("Freehand Stroke"));
new FreehandStrokeStrategy(indirectPainting,
m_d->resources, m_d->painterInfos, i18n("Freehand Stroke"));
m_d->strokeId = m_d->strokesFacade->startStroke(stroke);
m_d->previousPaintInformation =
m_d->infoBuilder->startStroke(event, m_d->strokeTime.elapsed());
m_d->infoBuilder->startStroke(event, m_d->strokeTime.elapsed());
m_d->history.clear();
m_d->history.append(m_d->previousPaintInformation);
m_d->velocityHistory.clear();
m_d->velocityHistory.append(std::numeric_limits<qreal>::signaling_NaN());
if(m_d->resources->needsAirbrushing()) {
m_d->airbrushingTimer.setInterval(m_d->resources->airbrushingRate());
m_d->airbrushingTimer.start();
......@@ -140,19 +143,93 @@ void KisToolFreehandHelper::initPaint(KoPointerEvent *event,
void KisToolFreehandHelper::paint(KoPointerEvent *event)
{
KisPaintInformation info =
m_d->infoBuilder->continueStroke(event,
m_d->previousPaintInformation.pos(),
m_d->strokeTime.elapsed());
m_d->infoBuilder->continueStroke(event,
m_d->previousPaintInformation.pos(),
m_d->strokeTime.elapsed());
// Smooth the coordinates out using the history and the velocity. See
// https://bugs.kde.org/show_bug.cgi?id=281267 and http://www24.atwiki.jp/sigetch_2007/pages/17.html.
// This is also implemented in gimp, which is where I cribbed the code from.
if (m_d->smoothingOptions.smoothingType == KisSmoothingOptions::WEIGHTED_SMOOTHING
&& m_d->smoothingOptions.smoothnessQuality > 1
&& m_d->smoothingOptions.smoothnessFactor > 3.0) {
m_d->history.append(info);
m_d->velocityHistory.append(std::numeric_limits<qreal>::signaling_NaN()); // Fake velocity!
qreal x = 0.0;
qreal y = 0.0;
if (m_d->history.size() > 3) {
int length = qMin(m_d->smoothingOptions.smoothnessQuality, m_d->history.size());
int minIndex = m_d->history.size() - length;
qreal gaussianWeight = 0.0;
qreal gaussianWeight2 = m_d->smoothingOptions.smoothnessFactor * m_d->smoothingOptions.smoothnessFactor;
qreal velocitySum = 0.0;
qreal scaleSum = 0.0;
if (gaussianWeight2 != 0.0) {
gaussianWeight = 1 / (sqrt(2 * M_PI) * m_d->smoothingOptions.smoothnessFactor);
}
Q_ASSERT(m_d->history.size() == m_d->velocityHistory.size());
for (int i = m_d->history.size() - 1; i >= minIndex; i--) {
qreal rate = 0.0;
const KisPaintInformation nextInfo = m_d->history.at(i);
double velocity = m_d->velocityHistory.at(i);
if (qIsNaN(velocity)) {
int previousTime = nextInfo.currentTime();
if (i > 0) {
previousTime = m_d->history.at(i - 1).currentTime();
}
int deltaTime = qMax(1, nextInfo.currentTime() - previousTime); // make sure deltaTime > 1
velocity = info.movement().norm() / deltaTime;
m_d->velocityHistory[i] = velocity;
}
if (gaussianWeight2 != 0.0) {
velocitySum += velocity * 100;
rate = gaussianWeight * exp(-velocitySum * velocitySum / (2 * gaussianWeight2));
}
scaleSum += rate;
x += rate * nextInfo.pos().x();
y += rate * nextInfo.pos().y();
}
if (scaleSum != 0.0) {
x /= scaleSum;
y /= scaleSum;
}
if ((x != 0.0 && y != 0.0) || (x == info.pos().x() && y == info.pos().y())) {
m_d->history.last().setPos(QPointF(x, y));
info.setPos(QPointF(x, y));
}
}
}
if (m_d->smooth) {
if (m_d->smoothingOptions.smoothingType == KisSmoothingOptions::SIMPLE_SMOOTHING
|| m_d->smoothingOptions.smoothingType == KisSmoothingOptions::WEIGHTED_SMOOTHING)
{
// Now paint between the coordinates, using the bezier curve interpolation
if (!m_d->haveTangent) {
m_d->haveTangent = true;
// XXX: 3.0 is a magic number I don't know anything about
// 1.0 was the old default value for smoothness, and anything lower than that
// gave horrible results, so remove that setting.
m_d->previousTangent =
(info.pos() - m_d->previousPaintInformation.pos()) * m_d->smoothness /
(3.0 * (info.currentTime() - m_d->previousPaintInformation.currentTime()));
(info.pos() - m_d->previousPaintInformation.pos()) /
(3.0 * (info.currentTime() - m_d->previousPaintInformation.currentTime()));
} else {
QPointF newTangent = (info.pos() - m_d->olderPaintInformation.pos()) * m_d->smoothness /
(3.0 * (info.currentTime() - m_d->olderPaintInformation.currentTime()));
QPointF newTangent = (info.pos() - m_d->olderPaintInformation.pos()) /
(3.0 * (info.currentTime() - m_d->olderPaintInformation.currentTime()));
qreal scaleFactor = (m_d->previousPaintInformation.currentTime() - m_d->olderPaintInformation.currentTime());
QPointF control1 = m_d->olderPaintInformation.pos() + m_d->previousTangent * scaleFactor;
QPointF control2 = m_d->previousPaintInformation.pos() - newTangent * scaleFactor;
......@@ -165,10 +242,13 @@ void KisToolFreehandHelper::paint(KoPointerEvent *event)
}
m_d->olderPaintInformation = m_d->previousPaintInformation;
m_d->strokeTimeoutTimer.start(100);
} else {
}
else {
paintLine(m_d->painterInfos, m_d->previousPaintInformation, info);
}
m_d->previousPaintInformation = info;
if(m_d->airbrushingTimer.isActive()) {
......@@ -180,7 +260,7 @@ void KisToolFreehandHelper::endPaint()
{
if (!m_d->hasPaintAtLeastOnce) {
paintAt(m_d->painterInfos, m_d->previousPaintInformation);
} else if (m_d->smooth) {
} else if (m_d->smoothingOptions.smoothingType != KisSmoothingOptions::NO_SMOOTHING) {
finishStroke();
}
m_d->strokeTimeoutTimer.stop();
......@@ -215,7 +295,7 @@ void KisToolFreehandHelper::finishStroke()
if(m_d->haveTangent) {
m_d->haveTangent = false;
QPointF newTangent = (m_d->previousPaintInformation.pos() - m_d->olderPaintInformation.pos()) * m_d->smoothness / 3.0;
QPointF newTangent = (m_d->previousPaintInformation.pos() - m_d->olderPaintInformation.pos()) / 3.0;
qreal scaleFactor = (m_d->previousPaintInformation.currentTime() - m_d->olderPaintInformation.currentTime());
QPointF control1 = m_d->olderPaintInformation.pos() + m_d->previousTangent * scaleFactor;
QPointF control2 = m_d->previousPaintInformation.pos() - newTangent;
......@@ -239,8 +319,8 @@ void KisToolFreehandHelper::paintAt(PainterInfo *painterInfo,
{
m_d->hasPaintAtLeastOnce = true;
m_d->strokesFacade->addJob(m_d->strokeId,
new FreehandStrokeStrategy::Data(m_d->resources->currentNode(),
painterInfo, pi));
new FreehandStrokeStrategy::Data(m_d->resources->currentNode(),
painterInfo, pi));
if(m_d->recordingAdapter) {
m_d->recordingAdapter->addPoint(pi);
......@@ -253,8 +333,8 @@ void KisToolFreehandHelper::paintLine(PainterInfo *painterInfo,
{
m_d->hasPaintAtLeastOnce = true;
m_d->strokesFacade->addJob(m_d->strokeId,
new FreehandStrokeStrategy::Data(m_d->resources->currentNode(),
painterInfo, pi1, pi2));
new FreehandStrokeStrategy::Data(m_d->resources->currentNode(),
painterInfo, pi1, pi2));
if(m_d->recordingAdapter) {
m_d->recordingAdapter->addLine(pi1, pi2);
......@@ -269,9 +349,9 @@ void KisToolFreehandHelper::paintBezierCurve(PainterInfo *painterInfo,
{
m_d->hasPaintAtLeastOnce = true;
m_d->strokesFacade->addJob(m_d->strokeId,
new FreehandStrokeStrategy::Data(m_d->resources->currentNode(),
painterInfo,
pi1, control1, control2, pi2));
new FreehandStrokeStrategy::Data(m_d->resources->currentNode(),
painterInfo,
pi1, control1, control2, pi2));
if(m_d->recordingAdapter) {
m_d->recordingAdapter->addCurve(pi1, control1, control2, pi2);
......
......@@ -35,21 +35,23 @@ class KisStrokesFacade;
class KisPostExecutionUndoAdapter;
class KisPaintOp;
class KisPainter;
class KisSmoothingOptions;
class KRITAUI_EXPORT KisToolFreehandHelper : public QObject
{
Q_OBJECT
protected:
typedef FreehandStrokeStrategy::PainterInfo PainterInfo;
public:
KisToolFreehandHelper(KisPaintingInformationBuilder *infoBuilder,