Search code examples
qtfontsqpainter

How do I correctly render individual letters in different colors with alpha blending?


I want to draw text where each letter has a different solid color.

For text like this. I can't simply draw the text character by character, since, for example, fi and fa have different rendering for f. Some fonts have triplet rendering, where the middle character would depend on the font behind it, and the one in front of it.

I also want to blend the parts where the glyphs overlap in cursive fonts.

Here's what I've got so far:

#include <QtGui/QPainter>
#include <QtGui/QImage>
#include <QGuiApplication>

int main(int argc, char *argv[])
{
    QString text("Hello World");
    QGuiApplication a(argc, argv);
    QFont font("Dancing Script", 100);
    QFontMetrics metric(font);
    auto bbox = metric.boundingRect(text);
    QImage img(bbox.width(), bbox.height(), QImage::Format_ARGB32);
    QPainter painter;
    painter.begin(&img);
    painter.setFont(font);
    painter.setBrush(QBrush(QColor(0, 0, 0, 255)));
    painter.drawRect(0, 0, img.width(), img.height());
    painter.setPen(QColor(255, 255, 255, 255));
    QTextOption option;
    option.setWrapMode(QTextOption::NoWrap);
    painter.drawText(QRect(0, 0, bbox.width(), bbox.height()), text, option);
    painter.end();
    img.save("txt.png");
}

This renders the text correctly (white on black).

I need some way of getting the correct glyphs along the text and paint them individually with alpha set to 127 to paint them each in their own color and have them blend nicely.

Anyone can point me in the right direction? Do I need to create a QTextLayout and get the QGlyphRun? Do I need to create a QTextDocument and get a QTextFragment from which I take QGlyphRun? Do I then iterate over the position and respective index of each glyph and create a new QGlyphRun containing only a single position and index and give it to QPainter's drawGlyphRun? I'm not quite sure how this works...


Solution

  • Ok, I've figured it out:

    I need to create a QTextLayout, and create a line in that layout (no wrap). The glyphRuns I get from this layout are there per each letter of the text in the layout, so I just need to iterate over each letter, take that letter's glyph, make sure its position isn't that of the previous letter's position (in case it was combined with the previous letter), and draw it.

    Here's my working rainbow code:

    #include <QPainter>
    #include <QImage>
    #include <QGuiApplication>
    #include <QTextLayout>
    
    
    QColor get_color(float position, uint8_t alpha) {
        if (position < 0.2)
            return QColor(255 - 255 * position / 0.2,              0,                 255,                     alpha);
        if (position < 0.4)
            return QColor(              0,         255 * (position - 0.2) / 0.2,      255,                     alpha);
        if (position < 0.6)
            return QColor(              0,                  255,          255 - 255 * (position - 0.4) / 0.2,  alpha);
        if (position < 0.8)
            return QColor(255 * (position - 0.6) / 0.2,     255,                        0,                     alpha);
    
        return     QColor(             255,        255 - 255 * (position - 0.8) / 0.2,  0,                     alpha);
    
    }
    
    
    QImage draw_letters(const QString & text, const QFont & font) {
        QFontMetrics metric(font);
        auto bbox = metric.boundingRect(text);
        QImage img(bbox.width(), bbox.height(), QImage::Format_ARGB32);
        QPainter painter;
        painter.begin(&img);
        painter.setBrush(QBrush(QColor(0, 0, 0, 255)));
        painter.drawRect(0, 0, img.width()+1, img.height()+1);
        QTextLayout layout(text, font);
        QTextOption option;
        option.setWrapMode(QTextOption::NoWrap);
        layout.setTextOption(option);
        layout.beginLayout();
        auto line = layout.createLine();
        layout.endLayout();
        QPointF prev_position(-1, -1);
        int num_letters = text.length();
        for (int i = 0; i < num_letters; ++i) {
            auto glyph = layout.glyphRuns(i, 1)[0];
            painter.setPen(get_color(static_cast<float>(i) / (num_letters - 1), 255));
            if (prev_position != glyph.positions()[0])
                painter.drawGlyphRun(QPointF(0, 0), glyph);
            prev_position = glyph.positions()[0];
        }
        painter.end();
        return img;
    }
    
    
    int main(int argc, char *argv[])
    {
        QString text("I can see the rainbow!");
        QGuiApplication a(argc, argv);
        QFont font("Dancing Script", 100);
        draw_letters(text, font).save("txt.png");
        return 0;
    }
    

    Using colorful vertical bars with the rendered font as an alpha mask as per JarMan's suggestion could easily be achived by drawing rectangles with glyph.boundingRect() But this leads to a problem with letters such as W In my chosen font, since it overlaps the rectangle of the next letter - so using that method would draw that W in two colors.

    Also note, that I don't actually use alpha blending at all, it turns out it looks better when the next letter overwrites the previous one completely instead of alpha blending. A sort of Linear blending (with weights changing along the x axis) might be better, but that's out of the scope of this question.