Commit cab95c71 authored by Dmitry Kazakov's avatar Dmitry Kazakov

[FEATURE] Implemented Wash mode for the liquify transform tool

There is a fundamental difference between Wash mode and BuildUp mode of
the liquify brush. When you paint on the same place with in BuildUp
(default) mode it adds deformations infinitely, until you hardly see the
original piece. When you use Wash mode, all deformations are limited
by the Amount value, that is they will not exceed this level. You can also
control how fast the deformations will rush to the maximum level by
adjusting 'Flow' parameter, which is available only in this mode.

CCMAIL:kimageshop@kde.org
parent c33c596f
......@@ -44,10 +44,23 @@ struct KisLiquifyTransformWorker::Private
struct MapIndexesOp;
template <class ProcessOp>
void processTransformedPixelsBuildUp(ProcessOp op,
const QPointF &base,
qreal sigma);
template <class ProcessOp>
void processTransformedPixelsWash(ProcessOp op,
const QPointF &base,
qreal sigma,
qreal flow);
template <class ProcessOp>
void processTransformedPixels(ProcessOp op,
const QPointF &base,
qreal sigma);
qreal sigma,
bool useWashMode,
qreal flow);
};
KisLiquifyTransformWorker::KisLiquifyTransformWorker(const QRect &srcBounds,
......@@ -159,9 +172,9 @@ void KisLiquifyTransformWorker::undoPoints(const QPointF &base,
template <class ProcessOp>
void KisLiquifyTransformWorker::Private::
processTransformedPixels(ProcessOp op,
const QPointF &base,
qreal sigma)
processTransformedPixelsBuildUp(ProcessOp op,
const QPointF &base,
qreal sigma)
{
const qreal maxDist = ProcessOp::maxDistCoeff * sigma;
QRectF clipRect(base.x() - maxDist, base.y() - maxDist,
......@@ -182,6 +195,55 @@ processTransformedPixels(ProcessOp op,
}
}
template <class ProcessOp>
void KisLiquifyTransformWorker::Private::
processTransformedPixelsWash(ProcessOp op,
const QPointF &base,
qreal sigma,
qreal flow)
{
const qreal maxDist = ProcessOp::maxDistCoeff * sigma;
QRectF clipRect(base.x() - maxDist, base.y() - maxDist,
2 * maxDist, 2 * maxDist);
QVector<QPointF>::iterator it = transformedPoints.begin();
QVector<QPointF>::iterator end = transformedPoints.end();
QVector<QPointF>::iterator refIt = originalPoints.begin();
KIS_ASSERT_RECOVER_RETURN(originalPoints.size() ==
transformedPoints.size());
for (; it != end; ++it, ++refIt) {
if (!clipRect.contains(*it)) continue;
QPointF diff = *refIt - base;
qreal dist = KisAlgebra2D::norm(diff);
if (dist > maxDist) continue;
const qreal lambda = exp(-0.5 * pow2(dist / sigma));
QPointF dstPt = op(*refIt, base, diff, lambda);
if (kisDistance(dstPt, *refIt) > kisDistance(*it, *refIt)) {
*it = (1.0 - flow) * (*it) + flow * dstPt;
}
}
}
template <class ProcessOp>
void KisLiquifyTransformWorker::Private::
processTransformedPixels(ProcessOp op,
const QPointF &base,
qreal sigma,
bool useWashMode,
qreal flow)
{
if (useWashMode) {
processTransformedPixelsWash(op, base, sigma, flow);
} else {
processTransformedPixelsBuildUp(op, base, sigma);
}
}
struct TranslateOp
{
TranslateOp(const QPointF &offset) : m_offset(offset) {}
......@@ -248,26 +310,32 @@ struct RotateOp
void KisLiquifyTransformWorker::translatePoints(const QPointF &base,
const QPointF &offset,
qreal sigma)
qreal sigma,
bool useWashMode,
qreal flow)
{
TranslateOp op(offset);
m_d->processTransformedPixels(op, base, sigma);
m_d->processTransformedPixels(op, base, sigma, useWashMode, flow);
}
void KisLiquifyTransformWorker::scalePoints(const QPointF &base,
qreal scale,
qreal sigma)
qreal sigma,
bool useWashMode,
qreal flow)
{
ScaleOp op(scale);
m_d->processTransformedPixels(op, base, sigma);
m_d->processTransformedPixels(op, base, sigma, useWashMode, flow);
}
void KisLiquifyTransformWorker::rotatePoints(const QPointF &base,
qreal angle,
qreal sigma)
qreal sigma,
bool useWashMode,
qreal flow)
{
RotateOp op(angle);
m_d->processTransformedPixels(op, base, sigma);
m_d->processTransformedPixels(op, base, sigma, useWashMode, flow);
}
struct KisLiquifyTransformWorker::Private::MapIndexesOp {
......
......@@ -45,15 +45,21 @@ public:
void translatePoints(const QPointF &base,
const QPointF &offset,
qreal sigma);
qreal sigma,
bool useWashMode,
qreal flow);
void scalePoints(const QPointF &base,
qreal scale,
qreal sigma);
qreal sigma,
bool useWashMode,
qreal flow);
void rotatePoints(const QPointF &base,
qreal angle,
qreal sigma);
qreal sigma,
bool useWashMode,
qreal flow);
void undoPoints(const QPointF &base,
qreal amount,
......
......@@ -148,11 +148,11 @@ void KisLiquifyTransformWorkerTest::testPoints()
QBENCHMARK_ONCE {
worker.translatePoints(QPointF(100,100),
QPointF(50, 0),
50);
50, false, 0.2);
worker.scalePoints(QPointF(400,100),
0.9,
50);
50, false, 0.2);
worker.undoPoints(QPointF(400,100),
1.0,
......@@ -160,15 +160,15 @@ void KisLiquifyTransformWorkerTest::testPoints()
worker.scalePoints(QPointF(400,300),
0.5,
50);
50, false, 0.2);
worker.scalePoints(QPointF(100,300),
-0.5,
30);
30, false, 0.2);
worker.rotatePoints(QPointF(100,500),
M_PI / 4,
50);
50, false, 0.2);
}
worker.run(dev);
......@@ -198,7 +198,7 @@ void KisLiquifyTransformWorkerTest::testPointsQImage()
worker.translatePoints(QPointF(100,100),
QPointF(50, 0),
50);
50, false, 0.2);
QRect rc = dev->exactBounds();
dev->setX(50);
......
......@@ -121,32 +121,33 @@ KisSpacingInformation KisLiquifyPaintop::paintAt(const KisPaintInformation &pi)
pi.pressure() * reverseCoeff * m_d->props.amount():
reverseCoeff * m_d->props.amount();
const bool useWashMode = m_d->props.useWashMode();
const qreal flow = m_d->props.flow();
switch (m_d->props.mode()) {
case KisLiquifyProperties::MOVE: {
const qreal offsetLength = size * amount;
m_d->worker->translatePoints(pi.pos(),
pi.drawingDirectionVector() * offsetLength,
size);
size, useWashMode, flow);
break;
}
case KisLiquifyProperties::SCALE:
m_d->worker->scalePoints(pi.pos(),
amount,
size);
size, useWashMode, flow);
break;
case KisLiquifyProperties::ROTATE:
m_d->worker->rotatePoints(pi.pos(),
2.0 * M_PI * amount,
size);
size, useWashMode, flow);
break;
case KisLiquifyProperties::OFFSET: {
const qreal offsetLength = size * amount;
m_d->worker->translatePoints(pi.pos(),
KisAlgebra2D::rightUnitNormal(pi.drawingDirectionVector()) * offsetLength,
size);
size, useWashMode, flow);
break;
}
case KisLiquifyProperties::UNDO:
......
......@@ -61,6 +61,9 @@ void KisLiquifyProperties::saveMode() const
cfg.writeEntry("sizeHasPressure", m_sizeHasPressure);
cfg.writeEntry("amountHasPressure", m_amountHasPressure);
cfg.writeEntry("reverseDirection", m_reverseDirection);
cfg.writeEntry("useWashMode", m_useWashMode);
cfg.writeEntry("flow", m_flow);
}
void KisLiquifyProperties::loadMode()
......@@ -74,4 +77,6 @@ void KisLiquifyProperties::loadMode()
m_sizeHasPressure = cfg.readEntry("sizeHasPressure", m_sizeHasPressure);
m_amountHasPressure = cfg.readEntry("amountHasPressure", m_amountHasPressure);
m_reverseDirection = cfg.readEntry("reverseDirection", m_reverseDirection);
m_useWashMode = cfg.readEntry("useWashMode", m_useWashMode);
m_flow = cfg.readEntry("flow", m_flow);
}
......@@ -39,7 +39,9 @@ public:
m_spacing(0.2),
m_sizeHasPressure(false),
m_amountHasPressure(false),
m_reverseDirection(false)
m_reverseDirection(false),
m_useWashMode(false),
m_flow(0.2)
{
}
......@@ -100,6 +102,20 @@ public:
m_reverseDirection = value;
}
bool useWashMode() const {
return m_useWashMode;
}
void setUseWashMode(bool value) {
m_useWashMode = value;
}
qreal flow() const {
return m_flow;
}
void setFlow(qreal value) {
m_flow = value;
}
void saveMode() const;
void loadMode();
......@@ -111,6 +127,9 @@ private:
bool m_sizeHasPressure;
bool m_amountHasPressure;
bool m_reverseDirection;
bool m_useWashMode;
qreal m_flow;
};
#endif /* __KIS_LIQUIFY_PROPERTIES_H */
......@@ -144,6 +144,15 @@ KisToolTransformConfigWidget::KisToolTransformConfigWidget(TransformTransactionP
connect(liquifyAmountSlider, SIGNAL(valueChanged(qreal)), this, SLOT(liquifyAmountChanged(qreal)));
liquifyAmountSlider->setToolTip(i18nc("@info:tooltip", "Amount of the deformation you get"));
liquifyFlowSlider->setRange(0.0, 1.0, 2);
liquifyFlowSlider->setValue(1.0);
connect(liquifyFlowSlider, SIGNAL(valueChanged(qreal)), this, SLOT(liquifyFlowChanged(qreal)));
liquifyFlowSlider->setToolTip(i18nc("@info:tooltip", "When in non-buildup mode, shows how fast the deformation limit is reached."));
liquifyBuildUpBox->setChecked(true);
connect(liquifyBuildUpBox, SIGNAL(toggled(bool)), this, SLOT(liquifyBuildUpChanged(bool)));
liquifyBuildUpBox->setToolTip(i18nc("@info:tooltip", "Switch between Build Up and Wash mode of painting. Build Up mode adds deformations one on top of the other without any limits. Wash mode gradually deforms the piece to the selected deformation level."));
liquifySpacingSlider->setRange(0.0, 3.0, 2);
liquifySizeSlider->setExponentRatio(3);
liquifySpacingSlider->setSingleStep(0.01);
......@@ -265,8 +274,12 @@ void KisToolTransformConfigWidget::updateLiquifyControls()
KisLiquifyProperties *props =
config->liquifyProperties();
const bool useWashMode = props->useWashMode();
liquifySizeSlider->setValue(props->size());
liquifyAmountSlider->setValue(props->amount());
liquifyFlowSlider->setValue(props->flow());
liquifyBuildUpBox->setChecked(!useWashMode);
liquifySpacingSlider->setValue(props->spacing());
liquifySizePressureBox->setChecked(props->sizeHasPressure());
liquifyAmountPressureBox->setChecked(props->amountHasPressure());
......@@ -279,7 +292,14 @@ void KisToolTransformConfigWidget::updateLiquifyControls()
bool canInverseDirection =
mode != KisLiquifyProperties::UNDO;
bool canUseWashMode = mode != KisLiquifyProperties::UNDO;
liquifyReverseDirectionChk->setEnabled(canInverseDirection);
liquifyFlowSlider->setEnabled(canUseWashMode && useWashMode);
liquifyBuildUpBox->setEnabled(canUseWashMode);
const qreal maxAmount = canUseWashMode ? 5.0 : 1.0;
liquifyAmountSlider->setRange(0.0, maxAmount, 2);
unblockUiSlots();
}
......@@ -330,6 +350,33 @@ void KisToolTransformConfigWidget::liquifyAmountChanged(qreal value)
notifyConfigChanged();
}
void KisToolTransformConfigWidget::liquifyFlowChanged(qreal value)
{
if (m_uiSlotsBlocked) return;
ToolTransformArgs *config = m_transaction->currentConfig();
KisLiquifyProperties *props =
config->liquifyProperties();
props->setFlow(value);
notifyConfigChanged();
}
void KisToolTransformConfigWidget::liquifyBuildUpChanged(bool value)
{
if (m_uiSlotsBlocked) return;
ToolTransformArgs *config = m_transaction->currentConfig();
KisLiquifyProperties *props =
config->liquifyProperties();
props->setUseWashMode(!value);
notifyConfigChanged();
// we need to enable/disable flow slider
updateLiquifyControls();
}
void KisToolTransformConfigWidget::liquifySpacingChanged(qreal value)
{
if (m_uiSlotsBlocked) return;
......
......@@ -90,6 +90,8 @@ public slots:
void liquifySizeChanged(qreal value);
void liquifyAmountChanged(qreal value);
void liquifyFlowChanged(qreal value);
void liquifyBuildUpChanged(bool value);
void liquifySpacingChanged(qreal value);
void liquifySizePressureChanged(bool value);
void liquifyAmountPressureChanged(bool value);
......
......@@ -1054,7 +1054,7 @@ big!</string>
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
<string>buttonGroup</string>
</attribute>
</widget>
</item>
......@@ -1120,7 +1120,7 @@ big!</string>
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
<string>buttonGroup</string>
</attribute>
</widget>
</item>
......@@ -1373,17 +1373,17 @@ big!</string>
</property>
</widget>
</item>
<item row="2" column="0">
<item row="3" column="0">
<widget class="QLabel" name="lblSpacing">
<property name="text">
<string>Spacing:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<item row="3" column="1">
<widget class="KisDoubleSliderSpinBox" name="liquifySpacingSlider" native="true"/>
</item>
<item row="3" column="1">
<item row="4" column="1">
<widget class="QPushButton" name="liquifyReverseDirectionChk">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
......@@ -1399,6 +1399,23 @@ big!</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lblFlow">
<property name="text">
<string>Flow:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="KisDoubleSliderSpinBox" name="liquifyFlowSlider" native="true"/>
</item>
<item row="2" column="2">
<widget class="QCheckBox" name="liquifyBuildUpBox">
<property name="text">
<string>Build Up</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
......
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