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 }