File indexing completed on 2024-05-19 04:07:51

0001 /*
0002     SPDX-FileCopyrightText: 2009, 2010, 2011 Stefan Majewsky <majewsky@gmx.net>
0003     SPDX-FileCopyrightText: 2010 Johannes Löhnert <loehnert.kde@gmx.de>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "piecevisuals.h"
0009 #include "mathtricks.h"
0010 
0011 #include <cmath>
0012 #include "palapeli_debug.h"
0013 #include <QImage>
0014 #include <QPainter>
0015 
0016 //BEGIN shadow blur algorithm
0017 
0018 static void blur(QImage& image, const QRect& rect, int radius)
0019 {
0020     const int tab[] = { 14, 10, 8, 6, 5, 5, 4, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2 };
0021     const int alpha = (radius < 1)  ? 16 : (radius > 17) ? 1 : tab[radius - 1];
0022 
0023     const int r1 = rect.top();
0024     const int r2 = rect.bottom();
0025     const int c1 = rect.left();
0026     const int c2 = rect.right();
0027 
0028     int bpl = image.bytesPerLine();
0029     int alphaChannel;
0030     unsigned char* p;
0031 
0032     for (int col = c1; col <= c2; col++) {
0033         p = image.scanLine(r1) + col * 4 + 3; //+3 to access alpha channel
0034         alphaChannel = *p << 4;
0035 
0036         p += bpl;
0037         for (int j = r1; j < r2; j++, p += bpl)
0038             *p = (alphaChannel += ((*p << 4) - alphaChannel) * alpha / 16) >> 4;
0039     }
0040 
0041     for (int row = r1; row <= r2; row++) {
0042         p = image.scanLine(row) + c1 * 4 + 3;
0043         alphaChannel = *p << 4;
0044 
0045         p += 4;
0046         for (int j = c1; j < c2; j++, p += 4)
0047             *p = (alphaChannel += ((*p << 4) - alphaChannel) * alpha / 16) >> 4;
0048     }
0049 
0050     for (int col = c1; col <= c2; col++) {
0051         p = image.scanLine(r2) + col * 4 + 3;
0052         alphaChannel = *p << 4;
0053 
0054         p -= bpl;
0055         for (int j = r1; j < r2; j++, p -= bpl)
0056                 *p = (alphaChannel += ((*p << 4) - alphaChannel) * alpha / 16) >> 4;
0057     }
0058 
0059     for (int row = r1; row <= r2; row++) {
0060         p = image.scanLine(row) + c2 * 4 + 3;
0061             alphaChannel = *p << 4;
0062 
0063         p -= 4;
0064         for (int j = c1; j < c2; j++, p -= 4)
0065                 *p = (alphaChannel += ((*p << 4) - alphaChannel) * alpha / 16) >> 4;
0066     }
0067 }
0068 
0069 static QImage makePixmapMonochrome(const QImage& image, const QColor& color)
0070 {
0071     QImage baseImage(image.size(), QImage::Format_ARGB32_Premultiplied);
0072     baseImage.fill(color.rgba());
0073 
0074     QImage monoImage(image);
0075     QPainter px(&monoImage);
0076     px.setCompositionMode(QPainter::CompositionMode_SourceAtop);
0077     px.drawImage(0, 0, baseImage);
0078     px.end();
0079     return monoImage;
0080 }
0081 
0082 static QImage createShadow(const QImage& source, int radius)
0083 {
0084     QImage shadowImage(QSize(source.width() + 2 * radius, source.height() + 2 * radius), QImage::Format_ARGB32_Premultiplied);
0085     shadowImage.fill(0x00000000); //transparent
0086     QPainter px(&shadowImage);
0087     px.drawImage(QPoint(radius, radius), makePixmapMonochrome(source, Qt::black));
0088     px.end();
0089 
0090     blur(shadowImage, QRect(QPoint(), shadowImage.size()), radius / 3);
0091     // IDW TODO - Shadow and highlight are hard to see on Apple. Can they
0092     //            be made more conspicuous? How are they on Linux?
0093     // IDW test. NO divisor ==> "tablets", / 2); ==> a bit bigger than / 3);
0094     return shadowImage;
0095 }
0096 
0097 //END shadow blur algorithm
0098 
0099 //BEGIN edge beveling algorithms
0100 
0101 // See piecevisuals.h for some explanations about BevelMap.
0102 Palapeli::BevelMap Palapeli::calculateBevelMap(const QImage& source, int radius)
0103 {
0104     const qreal strength_scale = 0.2;
0105     // in multiples of radius
0106     const qreal outline_width = 0.07;
0107     const qreal outline_darken_scale = 2;
0108 
0109     const QImage img = source.convertToFormat(QImage::Format_ARGB32);
0110     const int width = img.width();
0111     const int height = img.height();
0112 
0113     // prepare the folding kernels (access order: x * radius + y)
0114     QList<qreal> kernel_bevel(radius * radius, 0.0);
0115     QList<qreal> kernel_outline(radius * radius, 0.0);
0116     for (int n_xf = 0, n_fIndex = 0; n_xf < radius; ++n_xf)
0117     {
0118         for (int n_yf = 0; n_yf < radius; ++n_yf, ++n_fIndex)
0119         {
0120             const qreal xfold = n_xf + 0.5;
0121             const qreal yfold = n_yf + 0.5;
0122             const qreal len = Palapeli::fastnorm(xfold, yfold);
0123             if (len > radius) continue;
0124             // 3/pi/radius: normalization so that sum over all elements is (roughly) 1/r
0125             // (1 - len/radius)^2 : actual blur falloff function
0126             qreal t = (1 - len/radius);
0127             kernel_bevel[n_fIndex] = 3.0 / (M_PI*radius) * (t*t);
0128             // outline kernel
0129             if (len >= radius*outline_width + 0.5) continue;
0130             if (len <= radius*outline_width - 0.5)
0131             {
0132                 kernel_outline[n_fIndex] = 1.0 / (M_PI*radius);
0133             }
0134             else
0135             {
0136                 // roughly estimate coverage of that pixel
0137                 kernel_outline[n_fIndex] = (radius*outline_width + 0.5 - len) / (M_PI*radius);
0138             }
0139         }
0140     }
0141 
0142     // stores the blurred "fallof" of the transparency at each point.
0143     QList<QPointF> vmap(width*height);
0144     // stores how much pixel shall be darkened (outline effect)
0145     QList<qreal> darkening(width*height);
0146     // cache scanLine pointers (these are used very often from now on)
0147     QList<QRgb*> scanLinePointers(height);
0148     for (int ny = 0; ny < height; ++ny)
0149         scanLinePointers[ny] = (QRgb*) img.scanLine(ny);
0150 
0151     // iterate over img's pixel, finding the alpha edges
0152     // we imagine the image padded into a 1px wide transparent border,
0153     // thus iteration starts at -1.
0154     for (int ny = -1; ny < height; ++ny)
0155     {
0156         for (int nx = -1; nx < width; ++nx)
0157         {
0158             // for each edge, we add a blurred vector "blob" (the folding kernel_bevel) into the target vector at the right spot.
0159             // find alpha values
0160             // The conditions in here are a condensed version of img.rect().contains(nx(+1),ny(+1)).
0161             const int a1 = (nx == -1 || ny == -1) ? 0 : qAlpha(scanLinePointers[ny][nx]);
0162             const int a2 = (nx == width-1 || ny == -1) ? 0 : qAlpha(scanLinePointers[ny][nx+1]);
0163             const int a3 = (nx == -1 || ny == height-1) ? 0 : qAlpha(scanLinePointers[ny+1][nx]);
0164             const int a4 = (nx == width-1 || ny == height-1) ? 0 : qAlpha(scanLinePointers[ny+1][nx+1]);
0165             // find difference
0166             const int dx = (a2 + a4) - (a1 + a3);
0167             const int dy = (a3 + a4) - (a1 + a2);
0168             if (dx == 0 && dy == 0)
0169                 continue;
0170             const int diff = Palapeli::fastnorm(dx, dy);
0171 
0172             // iterate over the folding kernel_bevel
0173             for (int n_xf = 0, n_fIndex = 0; n_xf < radius; ++n_xf)
0174             {
0175                 //break statement in second loop might break n_fIndex
0176                 n_fIndex = n_xf * radius;
0177                 for (int n_yf = 0; n_yf < radius; ++n_yf, ++n_fIndex)
0178                 {
0179                     const qreal bevelfactor = kernel_bevel[n_fIndex];
0180                     if (bevelfactor == 0.0)
0181                         // only 0 will follow. break n_yf iteration and proceed with next n_xf.
0182                         break;
0183                     const qreal darkenfactor = diff * kernel_outline[n_fIndex];
0184                     for (int quadrant=0; quadrant<4; ++quadrant) {
0185                         // the coordinates of the point we write to
0186                         const int n_xtick = (quadrant%2 == 0) ? nx - n_xf : nx + 1 + n_xf;
0187                         const int n_ytick = (quadrant/2 == 0) ? ny - n_yf : ny + 1 + n_yf;
0188                         //if (image.rect().contains(n_xtick, n_ytick))
0189                         if (n_xtick>=0 && n_ytick>=0 && n_xtick < width && n_ytick < height)
0190                         {
0191                             const int n_tickIndex = n_xtick + n_ytick * width;
0192                             vmap[n_tickIndex] += QPointF(bevelfactor*dx, bevelfactor*dy);
0193                             darkening[n_tickIndex] += darkenfactor;
0194                         }
0195                     }
0196                 }
0197             } // end of iteration over folding kernel
0198         }
0199     } // end of iteration over source
0200 
0201     // re-encode the result into polar coordinates / RLE zero runs as described in piecevisuals.h.
0202     // While at it, we wipe out all elements where source alpha is zero.
0203     Palapeli::BevelMap result(width*height);
0204     int last_nonzero = -1;
0205     int strength;
0206 
0207     for (int y = 0, idx = 0; y < height; ++y)
0208     {
0209         for (int x = 0; x < width; ++x, ++idx)
0210         {
0211             const QRgb srccolor = scanLinePointers[y][x];
0212             if (qAlpha(srccolor) == 0)
0213             {
0214                 vmap[idx] = QPointF(0.0, 0.0);
0215                 strength = 0;
0216             }
0217             else
0218             {
0219                 strength = Palapeli::fastnorm(vmap[idx]) * strength_scale;
0220                 if (strength>255) strength=255;
0221             }
0222             if (strength>0) {
0223                 if (last_nonzero < idx-1)
0224                 {
0225                     result[last_nonzero+1].strength = 0;
0226                     // orig_argb stores run length
0227                     result[last_nonzero+1].orig_argb = idx - last_nonzero - 1;
0228                 }
0229                 last_nonzero = idx;
0230                 result[idx].strength = strength;
0231                 const qreal angle = qAtan2(-vmap[idx].y(), vmap[idx].x());
0232                 result[idx].angle = int(angle * 256 / (2 * M_PI) + 512) % 256;
0233                 QColor color = QColor::fromRgba(srccolor);
0234                 if (darkening[idx] > 0) color = color.darker(100 + darkening[idx]*outline_darken_scale);
0235                 result[idx].orig_argb = color.rgba();
0236             }
0237         }
0238     }
0239     // encode last zero run if present
0240     if (last_nonzero < width*height-1)
0241     {
0242         result[last_nonzero+1].strength = 0;
0243         // orig_argb stores run length
0244         result[last_nonzero+1].orig_argb = width*height - last_nonzero - 1;
0245     }
0246     return result;
0247 }
0248 
0249 // source: unbeveled image
0250 // bevelmap: must belong to the pixmap
0251 // angle: rotation angle of source, 0=unrotated, +x = ccw
0252 QImage Palapeli::applyBevelMap(const QImage &source, const Palapeli::BevelMap& bevelmap, qreal angle)
0253 {
0254     // first, prevent mem corruption
0255     if (source.width() * source.height() != bevelmap.size())
0256     {
0257         qCWarning(PALAPELI_LOG) << "ApplyBevelMap: returning unbeveled source since bevelmap has wrong size";
0258         return source;
0259     }
0260 
0261     QImage result = source.convertToFormat(QImage::Format_ARGB32);
0262     const int width = result.width();
0263     const int height = result.height();
0264     const int size = width * height;
0265     // in degrees
0266     const qreal lighting_angle = 120;
0267 
0268     // convert into 256-unit circle
0269     angle = (lighting_angle - angle) * 256 / 360;
0270 
0271     QRgb* data = (QRgb*) result.bits();
0272     for (int idx = 0; idx < size; ++idx)
0273     {
0274         const Palapeli::BevelPoint bevelpoint = bevelmap[idx];
0275         if (bevelpoint.strength == 0)
0276         {
0277             // zero run, skip to next nonzero element
0278             idx += int(bevelpoint.orig_argb) - 1;
0279             continue;
0280         }
0281 
0282         QColor newcolor = QColor::fromRgba(bevelpoint.orig_argb);
0283         const int effect = bevelpoint.strength * Palapeli::hexcos(bevelpoint.angle - angle);
0284         if (effect < 0)
0285             newcolor = newcolor.lighter(100 - effect);
0286         else
0287             newcolor = newcolor.darker(100 + effect);
0288         data[idx] = newcolor.rgba();
0289     }
0290     return result;
0291 }
0292 //END edge beveling algorithms
0293 
0294 Palapeli::PieceVisuals Palapeli::createShadow(const Palapeli::PieceVisuals& pieceVisuals, const QSize& shadowSizeHint)
0295 {
0296     const QSize shadowSizeHintUse = shadowSizeHint.isEmpty() ? pieceVisuals.size() : shadowSizeHint;
0297     const int shadowRadius = qMin<qreal>(0.15 * (shadowSizeHintUse.width() + shadowSizeHintUse.height()), 50);
0298     return Palapeli::PieceVisuals(
0299         createShadow(pieceVisuals.image(), shadowRadius),
0300         pieceVisuals.offset() - QPoint(shadowRadius, shadowRadius)
0301     );
0302 }
0303 
0304 Palapeli::PieceVisuals Palapeli::changeShadowColor(const Palapeli::PieceVisuals& shadowVisuals, const QColor& color)
0305 {
0306     return Palapeli::PieceVisuals(
0307         makePixmapMonochrome(shadowVisuals.image(), color),
0308         shadowVisuals.offset()
0309     );
0310 }
0311 
0312 Palapeli::PieceVisuals Palapeli::mergeVisuals(const QList<Palapeli::PieceVisuals>& visuals)
0313 {
0314     //determine geometry of combined pixmap, and hold a vote on which representation to use
0315     int imageCount = 0, pixmapCount = 0;
0316     QRect combinedGeometry;
0317     for (const Palapeli::PieceVisuals& sample : visuals)
0318     {
0319         QRect sampleGeometry(sample.offset(), sample.size());
0320         if (combinedGeometry.isNull())
0321             combinedGeometry = sampleGeometry;
0322         else
0323             combinedGeometry |= sampleGeometry;
0324         //NOTE: bool-int cast always returns 0 or 1.
0325         imageCount += (int) sample.hasImage();
0326         pixmapCount += (int) sample.hasPixmap();
0327     }
0328     const QPoint combinedOffset = combinedGeometry.topLeft();
0329     //combine pixmaps
0330     if (pixmapCount > imageCount)
0331     {
0332         QPixmap combinedPixmap(combinedGeometry.size());
0333         combinedPixmap.fill(Qt::transparent);
0334         QPainter painter(&combinedPixmap);
0335         for (const Palapeli::PieceVisuals& sample : visuals)
0336             painter.drawPixmap(sample.offset() - combinedOffset, sample.pixmap());
0337         painter.end();
0338         return Palapeli::PieceVisuals(combinedPixmap, combinedOffset);
0339     }
0340     else
0341     {
0342         QImage combinedImage(combinedGeometry.size(), QImage::Format_ARGB32_Premultiplied);
0343         combinedImage.fill(0x00000000); // == Qt::transparent
0344         QPainter painter(&combinedImage);
0345         for (const Palapeli::PieceVisuals& sample : visuals)
0346             painter.drawImage(sample.offset() - combinedOffset, sample.image());
0347         painter.end();
0348         return Palapeli::PieceVisuals(combinedImage, combinedOffset);
0349     }
0350 }
0351 
0352 Palapeli::BevelMap Palapeli::mergeBevelMaps(const QList<Palapeli::PieceVisuals>& visuals, const QList<Palapeli::BevelMap>& bevelMaps)
0353 {
0354     //determine geometry of combined pixmap
0355     QRect combinedGeometry;
0356     for (const Palapeli::PieceVisuals& sample : visuals)
0357     {
0358         QRect sampleGeometry(sample.offset(), sample.size());
0359         if (combinedGeometry.isNull())
0360             combinedGeometry = sampleGeometry;
0361         else
0362             combinedGeometry |= sampleGeometry;
0363     }
0364     const QPoint combinedOffset = combinedGeometry.topLeft();
0365     const int combinedWidth = combinedGeometry.width();
0366     const int combinedHeight = combinedGeometry.height();
0367 
0368     Palapeli::BevelMap result(combinedWidth*combinedHeight);
0369     for (int i=0; i<visuals.size(); i++)
0370     {
0371         int srcwidth = visuals[i].size().width();
0372         QPoint offset = visuals[i].offset() - combinedOffset;
0373         for (int srcidx=0; srcidx<bevelMaps[i].size(); srcidx++)
0374         {
0375             if (bevelMaps[i][srcidx].strength==0)
0376             {
0377                 // zero run, skip
0378                 srcidx += int(bevelMaps[i][srcidx].orig_argb);
0379                 if (srcidx > bevelMaps[i].size())
0380                     break;
0381             }
0382             QPoint dst = QPoint(srcidx%srcwidth, srcidx/srcwidth);
0383             dst += offset;
0384             if (dst.x() >= 0 && dst.y() >= 0 && dst.x() < combinedWidth && dst.y() < combinedHeight)
0385             {
0386                 int dstidx = dst.x() + dst.y() * combinedWidth;
0387                 if (result[dstidx].strength == 0)
0388                 {
0389                     result[dstidx] = bevelMaps[i][srcidx];
0390                 }
0391                 else
0392                 {
0393                     int a1 = qAlpha(result[dstidx].orig_argb);
0394                     int a2 = qAlpha(bevelMaps[i][srcidx].orig_argb);
0395                     // pixel with most opacity wins
0396                     if (a2 > a1) result[dstidx] = bevelMaps[i][srcidx];
0397                     QColor color = QColor::fromRgba(result[dstidx].orig_argb);
0398                     // sets alpha channel to 255
0399                     result[dstidx].orig_argb = color.rgb();
0400                     
0401                     // reduce strength if alpha difference is low, to avoid aliasing at the edges.
0402                     int strength = result[dstidx].strength;
0403                     strength = strength * (a1-a2) / 255;
0404                     if (strength==0)
0405                         strength=1;
0406                     else
0407                         strength = (strength<0) ? -strength: strength;
0408                     result[dstidx].strength = strength;
0409                 }
0410             }
0411         }
0412     }
0413 
0414     // Run-length encode the zero runs
0415     int last_nonzero = -1;
0416     for (int dstidx=0; dstidx<result.size(); dstidx++)
0417     {
0418         if (result[dstidx].strength!=0)
0419         {
0420             if (last_nonzero < dstidx-1)
0421                 result[last_nonzero+1].orig_argb = dstidx - last_nonzero - 1;
0422             last_nonzero = dstidx;
0423         }
0424     }
0425     if (last_nonzero < result.size()-1)
0426         result[last_nonzero+1].orig_argb = result.size() - last_nonzero - 1;
0427     
0428     return result;
0429 }