Finally a decent brush!

I’ve finally discovered how to do a decent brush in Krita. Turns out that the most time-intensive bit was the redrawing of the picture. This happened a lot in the in-betweening code, that painted a line between the previous and the current mouse position if Krita couldn’t keep up with the mouse.

So, now I update only once per mouse-move event, and all is pretty:

It’s not as good as the Gimp’s brush, and circles tend to come out a bit angular, but I am pretty chuffed with this result anyway.

Learning C++ and achieving a decent brush

I am finally being developing the feeling that I am starting to begin achieving the first step towards a modicum of confidence in my ability to achieve a moderate competence in C++. That’s to say, last night I spent a few hours hacking the KisToolBrush class for Krita. I want to achieve what nearly all paint applications manage to achieve: to draw a beautiful, antialiased line that accurately follows the mouse or stylus and is painted in the right colour, gradient or pattern using the correct brush. And I don’t seem able to figure out how to do that.

However, in the process of trying various approaches, I noticed that I was producing new code, instead of cut & paste jobs, and code that was working after only two compiles (the first to fix the typoes and add any semicolons that had shimmied off, the second to actually compile all the code). And when I say working, I mean that the code did what I wanted it to do. Always excepting my naive implementation of Bresenham’s line drawing algorithm — I hadn’t understood that I needed to implement it four times, one for ever major direction. Anyway, keying in code and seeing it compile and work feels good, even though I still have trouble with the *&->.! pointers and references, and especially with const.

Brushes

A propos of brushes, I have tried the following approaches:

Create a small layer that contains a colored version of the brush shape, and bitBlt it to the target layer on every mouse move event. This left huge gaps where Krita was too busy to allow Qt to push fresh mouse events. It seems that this is a standard feature of GUI toolkits.

Ditto, but copy the old in-betweening code from the early days of Krita. This uses a rather complex KisVector vector class and does a lot of computing to calculate where to place the points in between the positions we get from mouse move events. This was really, really slow; slow enough that when you tried to paint a circle with any celerity, you’d get a straight line from the point where you pressed the mouse button to the point where you released the mouse button.

See the first attempt, but now don’t paint until the mouse button is released. Show the path using a simple QPainter based line, and collect all mouse positions along the way. On mouse-up, fill the path with the brush. This is what I was doing yesterday evening; I still need to do the brush drawing on mouse-up. I suspect that this will also be too slow to be useful.

Mooching code

Of course, when I couldn’t find a way myself, I took a look really hard at other OSS paint applications to see how they manage… I have found the following application that could, or could not teach me the tricks of the trade:

    • Perico, by Peter Jodda. This application has the advantage that it is written using Qt, just like Krita, and that it is well-commented. However, his backend is fairly simple, doesn’t use tiles, and his paint tool doesn’t use brush masks. He paints a simple ellipse on every mouse move event, and inbetweens using an algorithm that I am going to copy tonight.
    • The Gimp. Now nearing version 2.0 (development started in 1994 or 1995, first using Motif, then using a purpose-built toolkit, GTK), the Gimp is everything that Krita still isn’t. And Krita is now in its fifth year of development. I rather suspect that GDK, the underlying layer between the GTK and X11, does a lot of useful work for paint applications. The Gimp takes the whole event loop, picks the most useful and likely events, returns the other events back to the GDK event loop and starts processing the events, in between updating cursor shapes and things. This is complex code, full of special cases and optimizations that I don’t understand, but the most important clues appear to be in the display/gimpdisplayshell-callbacks.c file. To be investigated more fully.
    • GSumi. This application is so weird I just don’t get anything about it…
    • KolourPaint. This uses QPainter/QPaintDevice for its core, and as such is fast, but not really suitable as an example. As you can see from the image above, I already can use QPainter, and it’s not enough.
    • Wet Dreams. This is an example I am going to copy in any case, but interestingly, there doesn’t appear to be any special connect-the-dots code, as you can see from the screenshot:

Showing the code

Oh, well, I knew I wasn’t competent when I started doing this: it’s all a learning exercise… Here’s the source, with all appropriate ifdefs:


/*
 *  Copyright (c) 2003 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 

#include "kis_vec.h"
#include "kis_painter.h"
#include "kis_selection.h"
#include "kis_doc.h"
#include "kis_view.h"
#include "kis_tool_brush.h"
#include "kis_tool_paint.h"
#include "kis_layer.h"
#include "kis_alpha_mask.h"
#include "kis_cursor.h"

KisToolBrush::KisToolBrush()
        : super(),
          m_mode( HOVER ),
	  m_hotSpotX ( 0 ),
	  m_hotSpotY ( 0 ),
	  m_brushWidth ( 0 ),
	  m_brushHeight ( 0 ),
	  m_spacing ( 1 ),
	  m_dragDist ( 0 ),
	  m_usePattern ( false ),
	  m_useGradient ( false )
{
	setCursor(KisCursor::crossCursor());

        m_painter = 0;
	m_dab = 0;
	m_points = 0;
}

KisToolBrush::~KisToolBrush()
{
	if (m_points) delete m_points;
}

void KisToolBrush::update(KisCanvasSubject *subject)
{
	m_subject = subject;
	super::update(m_subject);
}

void KisToolBrush::paint(QPainter& gc)
{
 	if (m_mode == PAINT)
 		paintLine(gc, QRect());
}

void KisToolBrush::paint(QPainter& gc, const QRect& rc)
{
 	if (m_mode == PAINT)
 		paintLine(gc, rc);
}


void KisToolBrush::mousePress(QMouseEvent *e)
{
        if (!m_subject) return;

        if (!m_subject->currentBrush()) return;

        if (e->button() == QMouseEvent::LeftButton) {
		kdDebug() << "mouse press button:" << e->button() << "\n"; m_mode = PAINT; initPaint(); // Remember the startposition of the stroke #if 0 m_dragStart = e -> pos();
		m_dragDist = 0;
#endif
		if (m_points) delete m_points;

		m_points = new QPointArray(1);
		
		m_points -> setPoint(0, translateImageXYtoViewPort( e->pos()));
		
                paint(e->pos(), 128, 0, 0);
         }
}


void KisToolBrush::mouseRelease(QMouseEvent* e)
{
	if (e->button() == QMouseEvent::LeftButton && m_mode == PAINT) {
		endPaint();
        }
}


#if 1
void KisToolBrush::mouseMove(QMouseEvent *e) {
	if (m_mode == PAINT) {
		Q_INT32 s = m_points -> size();
		m_points -> resize(s + 1);
		m_points -> setPoint( s, translateImageXYtoViewPort( e->pos()));
		paintLine( s - 1 );
		return;
	}
}
#endif

#if 0
void KisToolBrush::mouseMove(QMouseEvent *e)
{
	// XXX: Funny, this: the mouse button of a mouse-move event is always 0; this problably means
	// I should be checking the status of every button here.
	// XXX: Even if I accept all events, playing around with the stylus gives two or three spurious
	// mouse-move events if I lift the stylus from the pad.
	if (m_mode == PAINT) {
  		QPoint pos = e -> pos();
#if 0
		paint(pos, 128, 0, 0);
		return;
#endif
		KisVector end(pos.x(), pos.y());
		KisVector start(m_dragStart.x(), m_dragStart.y());
		KisVector dragVec = end - start;
		float savedDist = m_dragDist;
		float newDist = dragVec.length();
		float dist = savedDist + newDist;

		if (static_cast(dist) < m_spacing) { m_dragDist += newDist; m_dragStart = pos; return; } m_dragDist = 0; #if 0 dragVec.normalize(); // XX: enabling this gives a link error, so copied the relevant code below. #endif double length, ilength; double x, y, z; x = dragVec.x(); y = dragVec.y(); z = dragVec.z(); length = x * x + y * y + z * z; length = sqrt (length); if (length) { ilength = 1/length; x *= ilength; y *= ilength; z *= ilength; } dragVec.setX(x); dragVec.setY(y); dragVec.setZ(z); KisVector step = start; while (dist >= m_spacing) {
			if (savedDist > 0) {
				step += dragVec * (m_spacing - savedDist);
				savedDist -= m_spacing;
			}
			else {
				step += dragVec * m_spacing;
			}
			QPoint p(qRound(step.x()), qRound(step.y()));
			paint(p, 128, 0, 0);
			kdDebug() << "paint: (" << p.x() << "," << p.y() << ")\n"; dist -= m_spacing; } if (dist > 0)
			m_dragDist = dist;

		m_dragStart = pos;
         }
}
#endif

void KisToolBrush::tabletEvent(QTabletEvent *e)
{
         if (e->device() == QTabletEvent::Stylus) {
		 if (!m_subject) {
			 e->accept();
			 return;
		 }

		 if (!m_subject->currentBrush()) {
			 e->accept();
			 return;
		 }

		 Q_INT32 pressure = e -> pressure();

		 if (pressure < 5 && m_mode == PAINT_STYLUS) { endPaint(); } else if (pressure >= 5 && m_mode == HOVER) {
			 m_mode = PAINT_STYLUS;
			 initPaint();
			 paint(e->pos(), e->pressure(), e->xTilt(), e->yTilt());
		 }
		 else if (pressure >= 5 && m_mode == PAINT_STYLUS) {
			 kdDebug() << "Tablet: painting " << e->pos().x() << ", " << e -> pos().y() << "\n"; paint(e->pos(), e->pressure(), e->xTilt(), e->yTilt());
		 }
         }
	 e->accept();
}


void KisToolBrush::initPaint()
{
	// Create painter
	KisImageSP currentImage = m_subject -> currentImg();
	KisPaintDeviceSP device;
	if (currentImage && (device = currentImage -> activeDevice())) {
		if (m_painter)
			delete m_painter;
		m_painter = new KisPainter( device );
		m_painter->beginTransaction("brush");
	}

	// Retrieve and cache brush data. XXX: this is not ideal, since it is
	// done for every stroke, even if the brush and colour have not changed.
	// So, more work is done than is necessary.
	KisBrush *brush = m_subject -> currentBrush();
	KisAlphaMask *mask = brush -> mask();
	m_brushWidth = mask -> width();
	m_brushHeight = mask -> height();

	m_hotSpot = brush -> hotSpot();
	m_hotSpotX = m_hotSpot.x();
	m_hotSpotY = m_hotSpot.y();

	m_spacing = brush -> spacing();
	if (m_spacing <= 0) { m_spacing = brush -> width();
	}
	
	// Set the cursor -- ideally. this should be a pixmap created from the brush,
	// now that X11 can handle colored cursors.

#if 0
	// Setting cursors has no effect until the tool is selected again; this
	// should be fixed.
	setCursor(KisCursor::brushCursor());
#endif


	// Create dab
	m_dab = new KisLayer(mask -> width(),
			     mask -> height(),
			     currentImage -> imgType(),
			     "dab");
        m_dab -> opacity(OPACITY_TRANSPARENT);
	for (int y = 0; y < mask -> height(); y++) {
		for (int x = 0; x < mask -> width(); x++) {
                        m_dab -> setPixel(x, y, m_subject -> fgColor(), mask -> alphaAt(x, y));
                }
        }
}

void KisToolBrush::endPaint() 
{
	m_mode = HOVER;
	KisImageSP currentImage = m_subject -> currentImg();
	KisPaintDeviceSP device;
	if (currentImage && (device = currentImage -> activeDevice())) {
		KisUndoAdapter *adapter = currentImage -> undoAdapter();
		if (adapter && m_painter) {
			// If painting in mouse release, make sure painter
			// is destructed or end()ed
			adapter -> addCommand(m_painter->endTransaction());
		}
		delete m_painter;
		m_painter = 0;
		m_dab = 0; // XXX: No need to delete m_dab because shared pointer?
	}
}

void KisToolBrush::paint(const QPoint & pos,
                         const Q_INT32 /*pressure*/,
                         const Q_INT32 /*xTilt*/,
                         const Q_INT32 /*yTilt*/)
{
#if 0
        kdDebug() << "paint: " << pos.x() << ", " << pos.y() << endl; #endif Q_INT32 x = pos.x() - m_hotSpotX; Q_INT32 y = pos.y() - m_hotSpotY; KisImageSP currentImage = m_subject -> currentImg();

        if (!currentImage) return;

        // Blit the temporary KisPaintDevice onto the current layer
        KisPaintDeviceSP device = currentImage -> activeDevice();
        if (device) {
                m_painter->bitBlt( x,  y,  COMPOSITE_NORMAL, m_dab.data() );
        }
        currentImage -> notify(x,
			       y,
			       m_dab -> width(),
			       m_dab -> height());
}


void KisToolBrush::setup(KActionCollection *collection)
{
        KToggleAction *toggle;
        toggle = new KToggleAction(i18n("&Brush"),
				"handdrawn", 0, this,
                                   SLOT(activate()), collection,
                                   "tool_brush");
        toggle -> setExclusiveGroup("tools");
}


void KisToolBrush::paintLine(Q_INT32 s)
{
	if (m_subject) {
		KisCanvasControllerInterface *controller = m_subject -> canvasController();
		QWidget *canvas = controller -> canvas();
		QPainter gc(canvas);
		QRect rc;

		paintLine(gc, rc, s);
	}
}

void KisToolBrush::paintLine(QPainter& gc, const QRect&, Q_INT32 s)
{
	if (m_subject) {

		RasterOp op = gc.rasterOp();
		QPen old = gc.pen();
		QPen pen( Qt::SolidLine );

		gc.setRasterOp(Qt::NotROP);
		gc.setPen(pen);

		gc.drawPolyline(*m_points, s);

		gc.setRasterOp(op);
		gc.setPen(old);
	}
}

QPoint KisToolBrush::translateImageXYtoViewPort(const QPoint& p) {
		KisCanvasControllerInterface *controller = m_subject -> canvasController();
		Q_ASSERT(controller);
		QPoint p2;
		p2 = controller -> viewToWindow(p);

		p2.setX(p2.x() - controller -> horzValue());
		p2.setY(p2.y() - controller -> vertValue());


		p2 *= m_subject -> zoomFactor();

		return p2;
}