aboutsummaryrefslogtreecommitdiffstats
path: root/src/avatar.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/avatar.cpp')
-rw-r--r--src/avatar.cpp78
1 files changed, 78 insertions, 0 deletions
diff --git a/src/avatar.cpp b/src/avatar.cpp
index 0e2b25f..7aaa6eb 100644
--- a/src/avatar.cpp
+++ b/src/avatar.cpp
@@ -19,6 +19,8 @@
#include "avatar.h"
#include <QCryptographicHash>
+#include <QLinearGradient>
+#include <QLineF>
#include <QPainter>
#include <QPainterPath>
@@ -97,4 +99,80 @@ QColor colourFor(const QString &address)
return QColor::fromHsl(hue, 110, 95);
}
+QPixmap pixmapFor(const QString &seed, const QString &initials, Fill fill,
+ int side, const QFont &font)
+{
+ QPixmap pixmap(side, side);
+ pixmap.fill(Qt::transparent);
+
+ const QByteArray digest =
+ QCryptographicHash::hash(seed.toUtf8(), QCryptographicHash::Md5);
+ const QColor base = colourFor(seed);
+
+ QPainter painter(&pixmap);
+ painter.setRenderHint(QPainter::Antialiasing, true);
+
+ // The squircle. A rounded rect at ~30% of the side reads as one without
+ // needing a superellipse, and clipping to it means neither fill has to
+ // know the shape.
+ QPainterPath squircle;
+ squircle.addRoundedRect(QRectF(0, 0, side, side), side * 0.3, side * 0.3);
+ painter.setClipPath(squircle);
+
+ if (fill == Fill::Identicon) {
+ // A 5x5 grid, mirrored about the vertical axis, so only the left
+ // three columns come from the hash: 15 cells, one bit each, which is
+ // two bytes of the digest. Symmetry is what makes the shape read as a
+ // deliberate mark rather than as noise.
+ painter.fillRect(QRect(0, 0, side, side), base.darker(220));
+ const qreal cell = qreal(side) / 5.0;
+ for (int col = 0; col < 3; ++col) {
+ for (int row = 0; row < 5; ++row) {
+ const int bit = col * 5 + row;
+ const bool on =
+ (static_cast<quint8>(digest.at(bit / 8)) >> (bit % 8)) & 1;
+ if (!on)
+ continue;
+ painter.fillRect(QRectF(col * cell, row * cell, cell, cell),
+ base);
+ const int mirrored = 4 - col;
+ painter.fillRect(
+ QRectF(mirrored * cell, row * cell, cell, cell), base);
+ }
+ }
+ // The veil. Without it the initials sit on whatever the pattern
+ // happens to do behind them, which is the classic legibility failure
+ // this fill invites. Tune the opacity against the real font before
+ // calling it done.
+ painter.fillRect(QRect(0, 0, side, side), QColor(0, 0, 0, 77));
+ } else {
+ // Two related hues split at an angle, both from the hash. The field
+ // behind the letters stays large and flat, which is the whole reason
+ // this fill exists beside the identicon.
+ const int angle = static_cast<quint8>(digest.at(1)) * 360 / 256;
+ QLineF axis = QLineF::fromPolar(side, angle);
+ axis.translate(side / 2.0, side / 2.0);
+ QLinearGradient gradient(axis.p2(), axis.p1());
+ gradient.setColorAt(0.0, base);
+ gradient.setColorAt(0.499, base);
+ gradient.setColorAt(0.5, base.darker(135));
+ gradient.setColorAt(1.0, base.darker(135));
+ painter.fillRect(QRect(0, 0, side, side), gradient);
+ }
+
+ // The letters. White with a soft shadow rather than a computed contrast
+ // colour: the fills are generated at a fixed lightness precisely so one
+ // choice works for all of them.
+ QFont letters = font;
+ letters.setBold(true);
+ letters.setPixelSize(qMax(8, int(side * 0.36)));
+ painter.setFont(letters);
+ painter.setPen(QColor(0, 0, 0, 120));
+ painter.drawText(QRect(1, 1, side, side), Qt::AlignCenter, initials);
+ painter.setPen(Qt::white);
+ painter.drawText(QRect(0, 0, side, side), Qt::AlignCenter, initials);
+
+ return pixmap;
+}
+
} // namespace Avatar