Commit 6da12242 authored by Juan Luis Boya García's avatar Juan Luis Boya García Committed by Dmitry Kazakov
Browse files

[FEATURE] Brush stabilizer patch by Juan Luis Boya García

This commit adds to calligra a new stabilizer feature as an alternative
to the current "Weigthed smoothing".

This stabilizer is very simple, calculating an arithmetic mean of the
last N values (position, pressure and tilt) each millisecond and drawing
a line from the previous point to there.

This takes into account both the length of the strokes and the speed
they are made with.

Fast and small movements are considered erratic and are likely to be
ignored yielding a mostly straight line.

On the other hand, slower and bigger movements are assumed to be
deliberated and will follow the shape the user draws. The slower and
bigger, the more accurate.

This process makes the strokes draw 'slowly'. The user sees how the line
pursues their cursor. The effect is harder or softer depending on the N
chosen (sample size), which is user-customizable.

Starting and end points are forced by the algorithm to be drawn exactly
where the user began and ended the line.
parent 5a113d46
......@@ -22,13 +22,12 @@
#include <QCheckBox>
#include <QComboBox>
#include <QButtonGroup>
#include <klocale.h>
#include "kis_cursor.h"
#include "kis_config.h"
#include "kis_slider_spin_box.h"
#include <KoGroupButton.h>
#define MAXIMUM_SMOOTHNESS_DISTANCE 1000.0 // 0..1000.0 == weight in gui
#define MAXIMUM_MAGNETISM 1000
......@@ -40,6 +39,7 @@ KisToolBrush::KisToolBrush(KoCanvasBase * canvas)
i18nc("(qtundo-format)", "Brush"))
{
setObjectName("tool_brush");
connect(this, SIGNAL(smoothingTypeChanged()), this, SLOT(resetCursorStyle()));
}
KisToolBrush::~KisToolBrush()
......@@ -84,12 +84,19 @@ void KisToolBrush::slotSetSmoothingType(int index)
m_chkUseScalableDistance->setEnabled(false);
break;
case 2:
default:
m_smoothingOptions.setSmoothingType(KisSmoothingOptions::WEIGHTED_SMOOTHING);
m_sliderSmoothnessDistance->setEnabled(true);
m_sliderTailAggressiveness->setEnabled(true);
m_chkSmoothPressure->setEnabled(true);
m_chkUseScalableDistance->setEnabled(true);
break;
case 3:
default:
m_smoothingOptions.setSmoothingType(KisSmoothingOptions::STABILIZER);
m_sliderSmoothnessDistance->setEnabled(true);
m_sliderTailAggressiveness->setEnabled(false);
m_chkSmoothPressure->setEnabled(false);
m_chkUseScalableDistance->setEnabled(false);
}
emit smoothingTypeChanged();
}
......@@ -121,6 +128,22 @@ bool KisToolBrush::useScalableDistance() const
return m_smoothingOptions.useScalableDistance();
}
void KisToolBrush::resetCursorStyle()
{
KisConfig cfg;
enumCursorStyle cursorStyle = cfg.cursorStyle();
// When the stabilizer is in use, we avoid using the brush outline cursor,
// because it would hide the real position of the cursor to the user,
// yielding unexpected results.
if (m_smoothingOptions.smoothingType() == KisSmoothingOptions::STABILIZER
&& cursorStyle == CURSOR_STYLE_OUTLINE) {
useCursor(KisCursor::roundCursor());
} else {
KisToolFreehand::resetCursorStyle();
}
}
void KisToolBrush::setUseScalableDistance(bool value)
{
m_smoothingOptions.setUseScalableDistance(value);
......@@ -139,38 +162,14 @@ QWidget * KisToolBrush::createOptionWidget()
optionsWidget->layout()->addWidget(specialSpacer);
// Line smoothing configuration
QWidget* smoothingWidget = new QWidget(optionsWidget);
QHBoxLayout* smoothingLayout = new QHBoxLayout(smoothingWidget);
smoothingLayout->setSpacing(0);
m_buttonGroup = new QButtonGroup(this);
m_buttonGroup->setExclusive(true);
connect(m_buttonGroup, SIGNAL(buttonClicked(int)), this, SLOT(slotSetSmoothingType(int)));
KoGroupButton * button = new KoGroupButton(optionsWidget);
button->setGroupPosition(KoGroupButton::GroupLeft);
button->setText(i18nc("No smoothing enabled", "No"));
button->setAutoRaise(true);
button->setCheckable(true);
smoothingLayout->addWidget(button);
m_buttonGroup->addButton(button, KisSmoothingOptions::NO_SMOOTHING);
button = new KoGroupButton(optionsWidget);
button->setGroupPosition(KoGroupButton::GroupCenter);
button->setText(i18nc("Basic smoothing enabled", "Basic"));
button->setAutoRaise(true);
button->setCheckable(true);
smoothingLayout->addWidget(button);
m_buttonGroup->addButton(button, KisSmoothingOptions::SIMPLE_SMOOTHING);
button = new KoGroupButton(optionsWidget);
button->setGroupPosition(KoGroupButton::GroupRight);
button->setText(i18nc("Weighted smoothing enabled", "Weighted"));
button->setAutoRaise(true);
button->setCheckable(true);
smoothingLayout->addWidget(button);
m_buttonGroup->addButton(button, KisSmoothingOptions::WEIGHTED_SMOOTHING);
addOptionWidgetOption(smoothingWidget, new QLabel(i18nc("Smoothing of the brush", "Smoothing:")));
m_cmbSmoothingType = new QComboBox(optionsWidget);
m_cmbSmoothingType->addItems(QStringList()
<< i18n("No Smoothing")
<< i18n("Basic Smoothing")
<< i18n("Weighted Smoothing")
<< i18n("Stabilizer"));
connect(m_cmbSmoothingType, SIGNAL(currentIndexChanged(int)), this, SLOT(slotSetSmoothingType(int)));
addOptionWidgetOption(m_cmbSmoothingType);
m_sliderSmoothnessDistance = new KisDoubleSliderSpinBox(optionsWidget);
m_sliderSmoothnessDistance->setRange(3.0, MAXIMUM_SMOOTHNESS_DISTANCE, 1);
......@@ -196,8 +195,8 @@ QWidget * KisToolBrush::createOptionWidget()
connect(m_chkUseScalableDistance, SIGNAL(toggled(bool)), this, SLOT(setUseScalableDistance(bool)));
addOptionWidgetOption(m_chkUseScalableDistance, new QLabel(i18n("Scalable Distance")));
m_buttonGroup->button((int)m_smoothingOptions.smoothingType())->setChecked(true);
slotSetSmoothingType((int)m_smoothingOptions.smoothingType());
m_cmbSmoothingType->setCurrentIndex((int)m_smoothingOptions.smoothingType());
// Drawing assistant configuration
m_chkAssistant = new QCheckBox(i18n("Assistant:"), optionsWidget);
......
......@@ -55,6 +55,9 @@ public:
int smoothingType() const;
bool useScalableDistance() const;
protected slots:
virtual void resetCursorStyle();
public slots:
void slotSetSmoothnessDistance(qreal distance);
void slotSetMagnetism(int magnetism);
......@@ -72,6 +75,7 @@ Q_SIGNALS:
private:
QGridLayout *m_optionLayout;
QComboBox *m_cmbSmoothingType;
QCheckBox *m_chkAssistant;
KisSliderSpinBox *m_sliderMagnetism;
......@@ -79,7 +83,6 @@ private:
KisDoubleSliderSpinBox *m_sliderTailAggressiveness;
QCheckBox *m_chkSmoothPressure;
QCheckBox *m_chkUseScalableDistance;
QButtonGroup * m_buttonGroup;
};
......
......@@ -27,7 +27,8 @@ public:
enum SmoothingType {
NO_SMOOTHING = 0,
SIMPLE_SMOOTHING,
WEIGHTED_SMOOTHING
WEIGHTED_SMOOTHING,
STABILIZER
};
public:
......
......@@ -72,7 +72,6 @@ protected:
virtual bool wantsAutoScroll() const;
void activate(ToolActivation activation, const QSet<KoShape*> &shapes);
void deactivate();
void resetCursorStyle();
virtual void initStroke(KoPointerEvent *event);
virtual void doStroke(KoPointerEvent *event);
......@@ -89,6 +88,7 @@ protected:
protected slots:
virtual void resetCursorStyle();
void setAssistant(bool assistant);
private:
......
......@@ -19,6 +19,7 @@
#include "kis_tool_freehand_helper.h"
#include <QTimer>
#include <QQueue>
#include <klocale.h>
......@@ -109,6 +110,11 @@ struct KisToolFreehandHelper::Private
QList<qreal> distanceHistory;
PositionHistory lastOutlinePos;
// Stabilizer data
QQueue<KisPaintInformation> stabilizerDeque;
KisPaintInformation stabilizerLastPaintInfo;
QTimer stabilizerPollTimer;
};
......@@ -123,6 +129,8 @@ KisToolFreehandHelper::KisToolFreehandHelper(KisPaintingInformationBuilder *info
connect(&m_d->strokeTimeoutTimer, SIGNAL(timeout()), SLOT(finishStroke()));
connect(&m_d->airbrushingTimer, SIGNAL(timeout()), SLOT(doAirbrushing()));
connect(&m_d->stabilizerPollTimer, SIGNAL(timeout()), SLOT(stabilizerPollAndPaint()));
}
KisToolFreehandHelper::~KisToolFreehandHelper()
......@@ -209,6 +217,10 @@ void KisToolFreehandHelper::initPaint(KoPointerEvent *event,
m_d->airbrushingTimer.setInterval(m_d->resources->airbrushingRate());
m_d->airbrushingTimer.start();
}
if (m_d->smoothingOptions.smoothingType() == KisSmoothingOptions::STABILIZER) {
stabilizerStart(m_d->previousPaintInformation);
}
}
void KisToolFreehandHelper::paintBezierSegment(KisPaintInformation pi1, KisPaintInformation pi2,
......@@ -446,11 +458,15 @@ void KisToolFreehandHelper::paint(KoPointerEvent *event)
m_d->olderPaintInformation = m_d->previousPaintInformation;
m_d->strokeTimeoutTimer.start(100);
}
else {
else if (m_d->smoothingOptions.smoothingType() == KisSmoothingOptions::NO_SMOOTHING){
paintLine(m_d->painterInfos, m_d->previousPaintInformation, info);
}
m_d->previousPaintInformation = info;
if (m_d->smoothingOptions.smoothingType() == KisSmoothingOptions::STABILIZER) {
m_d->stabilizerLastPaintInfo = info;
} else {
m_d->previousPaintInformation = info;
}
if(m_d->airbrushingTimer.isActive()) {
m_d->airbrushingTimer.start();
......@@ -470,6 +486,10 @@ void KisToolFreehandHelper::endPaint()
m_d->airbrushingTimer.stop();
}
if (m_d->smoothingOptions.smoothingType() == KisSmoothingOptions::STABILIZER) {
stabilizerEnd();
}
/**
* There might be some timer events still pending, so
* we should cancel them. Use this flag for the purpose.
......@@ -485,6 +505,91 @@ void KisToolFreehandHelper::endPaint()
}
}
void KisToolFreehandHelper::stabilizerStart(KisPaintInformation firstPaintInfo)
{
// FIXME: Ugly hack, this is no a "distance" in any way
int sampleSize = m_d->smoothingOptions.smoothnessDistance();
assert(sampleSize > 0);
// Fill the deque with the current value repeated until filling the sample
m_d->stabilizerDeque.clear();
for (int i = sampleSize; i > 0; i--) {
m_d->stabilizerDeque.enqueue(firstPaintInfo);
}
m_d->stabilizerLastPaintInfo = firstPaintInfo;
// Poll and draw each millisecond
m_d->stabilizerPollTimer.setInterval(1);
m_d->stabilizerPollTimer.start();
}
void KisToolFreehandHelper::stabilizerPoll()
{
// Remove the oldest entry
m_d->stabilizerDeque.dequeue();
// Add a new entry with the last paint info (position and pressure)
m_d->stabilizerDeque.enqueue(m_d->stabilizerLastPaintInfo);
}
void KisToolFreehandHelper::stabilizerPaint()
{
// Get the average position and pressure in the deque
qreal x = 0.0,
y = 0.0,
pressure = 0.0,
xTilt = 0.0,
yTilt = 0.0;
foreach (KisPaintInformation info, m_d->stabilizerDeque) {
x += info.pos().x();
y += info.pos().y();
pressure += info.pressure();
xTilt += info.xTilt();
yTilt += info.yTilt();
}
x /= m_d->stabilizerDeque.size();
y /= m_d->stabilizerDeque.size();
pressure /= m_d->stabilizerDeque.size();
xTilt /= m_d->stabilizerDeque.size();
yTilt /= m_d->stabilizerDeque.size();
// Draw with these params
KisPaintInformation newInfo = m_d->stabilizerLastPaintInfo;
newInfo.setPos(QPointF(x, y));
newInfo.setPressure(pressure);
paintLine(m_d->painterInfos, m_d->previousPaintInformation, newInfo);
m_d->previousPaintInformation = newInfo;
}
void KisToolFreehandHelper::stabilizerPollAndPaint()
{
// Update the deque and draw a line to the new average
stabilizerPoll();
stabilizerPaint();
}
void KisToolFreehandHelper::stabilizerEnd()
{
// FIXME: Ugly hack, this is no a "distance" in any way
int sampleSize = m_d->smoothingOptions.smoothnessDistance();
assert(sampleSize > 0);
// Stop the timer
m_d->stabilizerPollTimer.stop();
// Finish the line
for (int i = sampleSize; i > 0; i--) {
// In each iteration we add the latest paint info and delete the oldest
// After `sampleSize` iterations the deque will be filled with the latest
// value and we will have reached the end point.
stabilizerPoll();
stabilizerPaint();
}
}
const KisPaintOp* KisToolFreehandHelper::currentPaintOp() const
{
return !m_d->painterInfos.isEmpty() ? m_d->painterInfos.first()->painter->paintOp() : 0;
......
......@@ -106,10 +106,16 @@ private:
void paintBezierSegment(KisPaintInformation pi1, KisPaintInformation pi2,
QPointF tangent1, QPointF tangent2);
void stabilizerStart(KisPaintInformation firstPaintInfo);
void stabilizerEnd();
void stabilizerPoll();
void stabilizerPaint();
private slots:
void finishStroke();
void doAirbrushing();
void stabilizerPollAndPaint();
private:
struct Private;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment