How to implement transparent pencil tool in Qt? [pic link updated]
-
I'm currently making a Painter like program. And I want to implement a painting tool, pencil, can draw in transparent color, such as rgb(255,0,0,100).
I've tried to use QPainter and draw things in a QImage and then draw on a QWidget.
Using QPainter::drawLine with QWidget::mouseMoveEvent is nice when the color is totally opacity one. However, if I use a transparent color, the point that both belongs to the last line and current line, will drawn twice(see http://pic.yupoo.com/liuyanghejerry/C4nibREy/medish.jpg
And I also tried using QPainter:drawPath and QPainter::drawLines, but none of them are right for this. They won't double draw the "last point" between two lines, but can't draw twice even when user intend to do so( saying you're drawing "8" and the cross should be drawn twice).
So, maybe impossible, is there a way to solve this? Implement a basic pencil tool?
-
-
Your basic, fundamental mistake is you are using the pen line drawing method. This is not how painting applications are done, at least not professionally. The line drawing method is for drawing vector like graphics, and you are limited to regular solid pen and dashes and stuff like that.
For a painting application you'd want to be able to use different brushes, sort of a black and white stencil you colorize with your draw color, so that when you drag the mouse, what is left behind is imprints of that stencil in the specified color.
Of course, you can have a solid brush like that, used by the line drawing method, you simply generate a circle on your stencil paint device. You can also create soft brushes by generating a circle with a gradient. And last but most certainly not least, you can load custom stencils to use as brushes too, have brush dynamics like rotation, random position and all that good stuff.
The problem is the developers of Qt have not really anticipated this usage scenario, so you will pretty much have to do everything yourself, calculate the segments between the last and current position of the cursor, draw a line and interpolate and draw your stencil on it based on your "spacing" setting.
The workflow is the following:
1 - generate a circle for solid brush, or a gradient circle for soft brush, or import an image for custom brush
2 - use the black and white data from the stencil to create the ALPHA channel of your brush, the rest of the channels you fill pixel by pixel with the color you want to draw in, so in the end you get an imprint of the stencil in the desired color
3 - since Qt polls events rather slowly and there is NO way to make it faster you have to interpolate between the last and current point, basically you create a QLine, passing last and current position to the constructor, you find the length of that line, divide it by your brush spacing and use drawPixmap method along the line to draw the stroke at the spaced locationsYou can also emulate OIL PAINTING by randomizing the fill color, to create not a solid but a little noisy stencil, which will imitate the individual hairs of a real brush. You can have brush dynamics in between interpolating the stroke line by adding per-defiend variations in either the rotation of the stencil or the position it is being drawn at.
Here are some code snippets that might get you started with generating the brush, the rest is easy, the snippets will get you through step 1 and 2, you will only have to figure step 3 for yourself, but it is quite easy.
@ QFile *stencilInput; // file for input, assumes a SQUARE RAW 8 bit grayscale image, no JPG no GIF, no size/format header, just 8 bit values in the file
char *brushPrototype; // raw brush prototype
uchar *brushData; // raw brush datastencilInput = new QFile("c:/raw2.raw"); // open raw file stencilInput->open(QIODevice::ReadOnly); QDataStream in; in.setDevice(stencilInput); int size = stencilInput->size(); // set size to the length of the raw file brushPrototype = new char[size]; // create the brush prototype array in.readRawData(brushPrototype, size); // read the file into the prototype brushData = new uchar[size]; // create the uchar array you need to construct QImage for (int i = 0; i < size; ++i) brushData[i] = (uchar)brushPrototype[i]; // copy the char to the uchar array QImage test(brushData, 100, 100, QImage::Format_Indexed8); // create QImage from the brush data array /// 100x100 was my raw file, for any file size just use the square root of the size variable provided it is SQUARE QImage test2(100, 100, QImage::Format_ARGB32);
QVector<QRgb> vectorColors(256); // create a color table for the image
for (int c = 0; c < 256; c++)
vectorColors[c] = qRgb(c, c, c);test.setColorTable(vectorColors); // set the color table to the image for (int iX = 0; iX < 100; ++iX) // fill all pixels with 255 0 0 (red) with random variations for OIL PAINT effect // use your color of choice and remove random stuff for solid color // the fourth parameter of setPixel, the qAbs thingie, is the ALPHA, use that to make your brush transparent by multiplying by opacity 0 to 1
// those 100s of both loops should also be quare root of size of file for files with dimensions different from 100x100 pixels
{
for (int iY = 0; iY < 100; ++iY)
{
test2.setPixel(iX, iY, qRgba(255 - (qrand() % 100), 0 + (qrand() % 100), 0 + (qrand() % 100), qAbs((int)test.pixel(iX, iY)-255)));
}
}
// final convertions of the stencil and color brush
QPixmap testPixmap = QPixmap::fromImage(test2);
QPixmap testPixmap2 = QPixmap::fromImage(test);// in a paint event you can test out both pixmaps
QPainter painter(this); painter.drawPixmap(150, 50, 100, 100, testPixmap); painter.drawPixmap(50, 50, 100, 100, testPixmap2);
// don't forget to delete all dynamically allocated objects with no parents
delete [] brushPrototype;
delete [] brushData;
delete stencilInput;@You can easily change from RAW files to directly creating QImage from image formats and copy pixels in the same manner, generating solid and soft round brushes is even easier.
If you do everything right you will have an output like that:
!http://i47.tinypic.com/21cwcqa.png(brushes)!
And strokes like those:
!http://i44.tinypic.com/35a1w5t.png(a)!
!http://i39.tinypic.com/2642m2e.png(a)!You can also layer 2 different brushes, this stroke is created with a layered brush, the back brush has rotation and position dynamics:
!http://i41.tinypic.com/34hcewg.png(a)! -
[quote author="Eddy" date="1339839416"]Fixed the link[/quote]
Sorry, I've updated the picture.
[quote author="utcenter" date="1339841603"]Your basic, fundamental mistake is you are using the pen line drawing method...[/quote]
This is what I want! Sorry for my late. I checked "Notify me via email when someone posts in this thread" but it didn't.
I also tried to read the source code of GIMP, but it's too complicated for me. While your post is a very nice start point for me. Thanks!!
-
You use the Qt provided pencil drawing method - you are limited to and by the way it is implemented. Ergo your issue with transparency stacking. Re-implementing it will probably harder than creating your own drawing method based on my post, which is WAY MORE FLEXIBLE, perfectly able to emulate pen drawing plus all the other options. As I said, this is how drawing applications actually work, other than that, good luck finding the draw method in any large and complex painting application, god knows I tried, but eventually it took me less to reinvent the wheel than to find the ways other did it.
-
This is quite useful. But when I tried to set opacity, it comes out weird:
!http://i46.tinypic.com/20r3hpi.jpg(preview)! -
Hehe, looks like you discovered accidental art, looking at the resulting pattern. It is either an issue of color indexing or you need to add some more parenthesis to make sure you get a value in the range of 0-255 before you multiply it by the transparency and set it as alpha value.
-
[quote author="utcenter" date="1340915293"]Hehe, looks like you discovered accidental art, looking at the resulting pattern. It is either an issue of color indexing or you need to add some more parenthesis to make sure you get a value in the range of 0-255 before you multiply it by the transparency and set it as alpha value. [/quote]
But the art also makes me puzzled...
I replaced your way of read alpha value from this:
@qAbs((int)test.pixel(iX, iY)-255)@
to this:
@(255-qGray(test.pixel(iX, iY)))*0.3; // 0.3 is the transparency@
Now everything woks fine^^
Thanks again!