File indexing completed on 2025-02-02 04:22:20

0001 /*
0002  *  SPDX-FileCopyrightText: 2010 Lukáš Tvrdý <lukast.dev@gmail.com>
0003  *  SPDX-FileCopyrightText: 2010 Ricardo Cabello <hello@mrdoob.com>
0004  *
0005  *  SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "kis_sketch_paintop.h"
0009 #include "kis_sketch_paintop_settings.h"
0010 
0011 #include <cmath>
0012 #include <QRect>
0013 
0014 #include <KoColor.h>
0015 #include <KoColorSpace.h>
0016 
0017 #include <kis_image.h>
0018 #include <kis_debug.h>
0019 
0020 #include <kis_global.h>
0021 #include <kis_paint_device.h>
0022 #include <kis_painter.h>
0023 #include <kis_types.h>
0024 #include <kis_paintop_plugin_utils.h>
0025 #include <brushengine/kis_paintop.h>
0026 #include <brushengine/kis_paint_information.h>
0027 #include <kis_fixed_paint_device.h>
0028 
0029 #include <kis_dab_cache.h>
0030 #include "kis_lod_transform.h"
0031 #include <KoResourceLoadResult.h>
0032 
0033 
0034 #include <QtGlobal>
0035 
0036 /*
0037 * Based on Harmony project https://github.com/mrdoob/harmony/
0038 */
0039 // chrome : diff 0.2, sketchy : 0.3, fur: 0.5
0040 // fur : distance / thresholdDistance
0041 
0042 // shaded: opacity per line :/
0043 // ((1 - (d / 1000)) * 0.1 * BRUSH_PRESSURE), offset == 0
0044 // chrome: color per line :/
0045 //this.context.strokeStyle = "rgba(" + Math.floor(Math.random() * COLOR[0]) + ", " + Math.floor(Math.random() * COLOR[1]) + ", " + Math.floor(Math.random() * COLOR[2]) + ", " + 0.1 * BRUSH_PRESSURE + " )";
0046 
0047 // long fur
0048 // from: count + offset * -random
0049 // to: i point - (offset * -random)  + random * 2
0050 // probability distance / thresholdDistance
0051 
0052 // shaded: probability : paint always - 0.0 density
0053 
0054 KisSketchPaintOp::KisSketchPaintOp(const KisPaintOpSettingsSP settings, KisPainter *painter, KisNodeSP node, KisImageSP image)
0055     : KisPaintOp(painter)
0056     , m_opacityOption(settings.data())
0057     , m_sizeOption(settings.data())
0058     , m_rotationOption(settings.data())
0059     , m_rateOption(settings.data())
0060     , m_densityOption(settings.data())
0061     , m_lineWidthOption(settings.data())
0062     , m_offsetScaleOption(settings.data())
0063 {
0064     Q_UNUSED(image);
0065     Q_UNUSED(node);
0066 
0067     m_airbrushOption.read(settings.data());
0068     m_sketchProperties.read(settings.data());
0069     m_brushOption.readOptionSetting(settings, settings->resourcesInterface(), settings->canvasResourcesInterface());
0070 
0071     m_brush = m_brushOption.brush();
0072     m_dabCache = new KisDabCache(m_brush);
0073 
0074     m_painter = 0;
0075     m_count = 0;
0076 }
0077 
0078 KisSketchPaintOp::~KisSketchPaintOp()
0079 {
0080     delete m_painter;
0081     delete m_dabCache;
0082 }
0083 
0084 QList<KoResourceLoadResult> KisSketchPaintOp::prepareLinkedResources(const KisPaintOpSettingsSP settings, KisResourcesInterfaceSP resourcesInterface)
0085 {
0086     KisBrushOptionProperties brushOption;
0087     return brushOption.prepareLinkedResources(settings, resourcesInterface);
0088 }
0089 
0090 void KisSketchPaintOp::drawConnection(const QPointF& start, const QPointF& end, double lineWidth)
0091 {
0092     //Both drawWuLine() and the drawDDALine produce nicer 1px lines than the drawLine()
0093     if (m_sketchProperties.antiAliasing) {
0094         if (lineWidth == 1.0) {
0095             m_painter->drawWuLine(start, end);
0096         }
0097         else {
0098             m_painter->drawLine(start, end, lineWidth, true);
0099         }
0100     }
0101     else {
0102         if (lineWidth == 1.0) {
0103             m_painter->drawDDALine(start, end);
0104         }
0105         else {
0106             m_painter->drawLine(start, end, lineWidth, false);
0107         }
0108     }
0109 }
0110 
0111 void KisSketchPaintOp::updateBrushMask(const KisPaintInformation& info, qreal scale, qreal rotation)
0112 {
0113     QRect dstRect;
0114     m_maskDab = m_dabCache->fetchDab(m_dab->colorSpace(),
0115                                      painter()->paintColor(),
0116                                      info.pos(),
0117                                      KisDabShape(scale, 1.0, rotation),
0118                                      info, 1.0,
0119                                      &dstRect);
0120 
0121     m_brushBoundingBox = dstRect;
0122     m_hotSpot = QPointF(0.5 * m_brushBoundingBox.width(),
0123                         0.5 * m_brushBoundingBox.height());
0124 }
0125 
0126 void KisSketchPaintOp::paintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2,
0127                                  KisDistanceInformation *currentDistance)
0128 {
0129     // Use superclass behavior for lines of zero length. Otherwise, airbrushing can happen faster
0130     // than it is supposed to.
0131     if (pi1.pos() == pi2.pos()) {
0132         KisPaintOp::paintLine(pi1, pi2, currentDistance);
0133     }
0134     else {
0135         doPaintLine(pi1, pi2);
0136     }
0137 }
0138 void KisSketchPaintOp::doPaintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2)
0139 {
0140     if (!m_brush || !painter()) return;
0141 
0142     if (!m_dab) {
0143         m_dab = source()->createCompositionSourceDevice();
0144         m_painter = new KisPainter(m_dab);
0145         m_painter->setPaintColor(painter()->paintColor());
0146     }
0147     else {
0148         m_dab->clear();
0149     }
0150 
0151     QPointF prevMouse = pi1.pos();
0152     QPointF mousePosition = pi2.pos();
0153     m_points.append(mousePosition);
0154 
0155 
0156     const qreal lodAdditionalScale = KisLodTransform::lodToScale(painter()->device());
0157     const qreal scale = lodAdditionalScale * m_sizeOption.apply(pi2);
0158     if ((scale * m_brush->width()) <= 0.01 || (scale * m_brush->height()) <= 0.01) return;
0159 
0160     const qreal currentLineWidth = qMax(0.9, lodAdditionalScale * m_lineWidthOption.apply(pi2) * m_sketchProperties.lineWidth);
0161 
0162     const qreal currentOffsetScale = m_offsetScaleOption.apply(pi2) * m_sketchProperties.offset * 0.01;
0163     const double rotation = m_rotationOption.apply(pi2);
0164     const double currentProbability = m_densityOption.apply(pi2) * m_sketchProperties.probability;
0165 
0166     // shaded: does not draw this line, chrome does, fur does
0167     if (m_sketchProperties.makeConnection) {
0168         drawConnection(prevMouse, mousePosition, currentLineWidth);
0169     }
0170 
0171 
0172     qreal thresholdDistance = 0.0;
0173 
0174     // update the mask for simple mode only once
0175     // determine the radius
0176     if (m_count == 0 && m_sketchProperties.simpleMode) {
0177         updateBrushMask(pi2, 1.0, 0.0);
0178         //m_radius = qMax(m_maskDab->bounds().width(),m_maskDab->bounds().height()) * 0.5;
0179         m_radius = 0.5 * qMax(m_brush->width(), m_brush->height());
0180     }
0181 
0182     if (!m_sketchProperties.simpleMode) {
0183         updateBrushMask(pi2, scale, rotation);
0184         m_radius = qMax(m_maskDab->bounds().width(), m_maskDab->bounds().height()) * 0.5;
0185         thresholdDistance = pow(m_radius, 2);
0186     }
0187 
0188     if (m_sketchProperties.simpleMode) {
0189         // update the radius according scale in simple mode
0190         thresholdDistance = pow(m_radius * scale, 2);
0191     }
0192 
0193     // determine density
0194     const qreal density = thresholdDistance * currentProbability;
0195 
0196     // probability behaviour
0197     qreal probability = 1.0 - currentProbability;
0198 
0199     QColor painterColor = painter()->paintColor().toQColor();
0200     QColor randomColor;
0201     KoColor color(m_dab->colorSpace());
0202 
0203     int w = m_maskDab->bounds().width();
0204     quint8 opacityU8 = 0;
0205     quint8 * pixel;
0206     qreal distance;
0207     QPoint  positionInMask;
0208     QPointF diff;
0209 
0210     int size = m_points.size();
0211     // MAIN LOOP
0212     for (int i = 0; i < size; i++) {
0213         diff = m_points.at(i) - mousePosition;
0214         distance = diff.x() * diff.x() + diff.y() * diff.y();
0215 
0216         // circle test
0217         bool makeConnection = false;
0218         if (m_sketchProperties.simpleMode) {
0219             if (distance < thresholdDistance) {
0220                 makeConnection = true;
0221             }
0222             // mask test
0223         }
0224         else {
0225             if (m_brushBoundingBox.contains(m_points.at(i))) {
0226                 positionInMask = (diff + m_hotSpot).toPoint();
0227                 uint pos = ((positionInMask.y() * w + positionInMask.x()) * m_maskDab->pixelSize());
0228                 if (pos < m_maskDab->allocatedPixels() * m_maskDab->pixelSize()) {
0229                     pixel = m_maskDab->data() + pos;
0230                     opacityU8 = m_maskDab->colorSpace()->opacityU8(pixel);
0231                     if (opacityU8 != 0) {
0232                         makeConnection = true;
0233                     }
0234                 }
0235             }
0236 
0237         }
0238 
0239         if (!makeConnection) {
0240             // check next point
0241             continue;
0242         }
0243 
0244         if (m_sketchProperties.distanceDensity) {
0245             probability =  distance / density;
0246         }
0247 
0248         KisRandomSourceSP randomSource = pi2.randomSource();
0249 
0250         // density check
0251         if (randomSource->generateNormalized() >= probability) {
0252             QPointF offsetPt = diff * currentOffsetScale;
0253 
0254             if (m_sketchProperties.randomRGB) {
0255                 /**
0256                  * Since the order of calculation of function
0257                  * parameters is not defined by C++ standard, we
0258                  * should generate values in an external code snippet
0259                  * which has a definite order of execution.
0260                  */
0261                 qreal r1 = randomSource->generateNormalized();
0262                 qreal r2 = randomSource->generateNormalized();
0263                 qreal r3 = randomSource->generateNormalized();
0264 
0265                 // some color transformation per line goes here
0266                 randomColor.setRgbF(r1 * painterColor.redF(),
0267                                     r2 * painterColor.greenF(),
0268                                     r3 * painterColor.blueF());
0269                 color.fromQColor(randomColor);
0270                 m_painter->setPaintColor(color);
0271             }
0272 
0273             // distance based opacity
0274             quint8 opacity = OPACITY_OPAQUE_U8;
0275             if (m_sketchProperties.distanceOpacity) {
0276                 opacity *= qRound((1.0 - (distance / thresholdDistance)));
0277             }
0278 
0279             if (m_sketchProperties.randomOpacity) {
0280                 opacity *= randomSource->generateNormalized();
0281             }
0282 
0283             m_painter->setOpacity(opacity);
0284 
0285             if (m_sketchProperties.magnetify) {
0286                 drawConnection(mousePosition + offsetPt, m_points.at(i) - offsetPt, currentLineWidth);
0287             }
0288             else {
0289                 drawConnection(mousePosition + offsetPt, mousePosition - offsetPt, currentLineWidth);
0290             }
0291 
0292 
0293 
0294         }
0295     }// end of MAIN LOOP
0296 
0297     m_count++;
0298 
0299     QRect rc = m_dab->extent();
0300     quint8 origOpacity = m_opacityOption.apply(painter(), pi2);
0301 
0302     painter()->bitBlt(rc.x(), rc.y(), m_dab, rc.x(), rc.y(), rc.width(), rc.height());
0303     painter()->renderMirrorMask(rc, m_dab);
0304     painter()->setOpacity(origOpacity);
0305 }
0306 
0307 
0308 
0309 KisSpacingInformation KisSketchPaintOp::paintAt(const KisPaintInformation& info)
0310 {
0311     doPaintLine(info, info);
0312     return updateSpacingImpl(info);
0313 }
0314 
0315 KisSpacingInformation KisSketchPaintOp::updateSpacingImpl(const KisPaintInformation &info) const
0316 {
0317     return KisPaintOpPluginUtils::effectiveSpacing(0.0, 0.0, true, 0.0, false, 0.0, false, 0.0,
0318                                                    KisLodTransform::lodToScale(painter()->device()),
0319                                                    &m_airbrushOption, nullptr, info);
0320 }
0321 
0322 KisTimingInformation KisSketchPaintOp::updateTimingImpl(const KisPaintInformation &info) const
0323 {
0324     return KisPaintOpPluginUtils::effectiveTiming(&m_airbrushOption, &m_rateOption, info);
0325 }