1 /*
2  * maprenderer.cpp
3  * Copyright 2011, Thorbjørn Lindeijer <thorbjorn@lindeijer.nl>
4  *
5  * This file is part of libtiled.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions are met:
9  *
10  *    1. Redistributions of source code must retain the above copyright notice,
11  *       this list of conditions and the following disclaimer.
12  *
13  *    2. Redistributions in binary form must reproduce the above copyright
14  *       notice, this list of conditions and the following disclaimer in the
15  *       documentation and/or other materials provided with the distribution.
16  *
17  * THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR
18  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
19  * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
20  * EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
23  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
24  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
25  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
26  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28 
29 #include "maprenderer.h"
30 
31 #include "imagelayer.h"
32 #include "isometricrenderer.h"
33 #include "map.h"
34 #include "mapobject.h"
35 #include "objectgroup.h"
36 #include "orthogonalrenderer.h"
37 #include "staggeredrenderer.h"
38 #include "tile.h"
39 #include "tilelayer.h"
40 
41 #include <QPaintEngine>
42 #include <QPainter>
43 #include <QVector2D>
44 
45 #include "qtcompat_p.h"
46 
47 #include <cmath>
48 
49 using namespace Tiled;
50 
tinted(const QPixmap & pixmap,const QColor & color)51 static QPixmap tinted(const QPixmap &pixmap, const QColor &color)
52 {
53     if (!color.isValid() || color == QColor(255, 255, 255, 255))
54         return pixmap;
55 
56     QPixmap resultImage = pixmap;
57     QPainter painter(&resultImage);
58 
59     QColor fullOpacity = color;
60     fullOpacity.setAlpha(255);
61     // tint the final color (this will will mess up the alpha which we will fix
62     // in the next lines)
63     painter.setCompositionMode(QPainter::CompositionMode_Multiply);
64     painter.fillRect(resultImage.rect(), fullOpacity);
65 
66     // apply the original alpha to the final image
67     painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
68     painter.drawPixmap(0, 0, pixmap);
69 
70     // apply the alpha of the tint color so that we can use it to make the image
71     // transparent instead of just increasing or decreasing the tint effect
72     painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
73     painter.fillRect(resultImage.rect(), color);
74 
75     painter.end();
76 
77     return resultImage;
78 }
79 
~MapRenderer()80 MapRenderer::~MapRenderer()
81 {}
82 
boundingRect(const ImageLayer * imageLayer) const83 QRectF MapRenderer::boundingRect(const ImageLayer *imageLayer) const
84 {
85     return QRectF(QPointF(), imageLayer->image().size());
86 }
87 
pointShape(const QPointF & position) const88 QPainterPath MapRenderer::pointShape(const QPointF &position) const
89 {
90     QPainterPath path;
91 
92     const qreal radius = 10.0;
93     const qreal sweep = 235.0;
94     const qreal startAngle = 90.0 - sweep / 2;
95     QRectF rectangle(-radius, -radius, radius * 2, radius * 2);
96     path.moveTo(radius * cos(startAngle * M_PI / 180.0), -radius * sin(startAngle * M_PI / 180.0));
97     path.arcTo(rectangle, startAngle, sweep);
98     path.lineTo(0, 2 * radius);
99     path.closeSubpath();
100 
101     QPainterPath hole;
102     const qreal smallRadius = radius / 2.0;
103     hole.addEllipse(QRectF(-smallRadius, -smallRadius, smallRadius * 2, smallRadius * 2));
104     path = path.subtracted(hole);
105 
106     path.translate(pixelToScreenCoords(position) +
107                    QPointF(0, -2 * radius));
108 
109     return path;
110 }
111 
drawImageLayer(QPainter * painter,const ImageLayer * imageLayer,const QRectF & exposed) const112 void MapRenderer::drawImageLayer(QPainter *painter,
113                                  const ImageLayer *imageLayer,
114                                  const QRectF &exposed) const
115 {
116     Q_UNUSED(exposed)
117 
118     painter->drawPixmap(QPointF(), tinted(imageLayer->image(), imageLayer->effectiveTintColor()));
119 }
120 
drawPointObject(QPainter * painter,const QColor & color) const121 void MapRenderer::drawPointObject(QPainter *painter, const QColor &color) const
122 {
123     const qreal lineWidth = objectLineWidth();
124     const qreal scale = painterScale();
125     const qreal shadowDist = (lineWidth == 0 ? 1 : lineWidth) / scale;
126     const QPointF shadowOffset = QPointF(shadowDist * 0.5,
127                                          shadowDist * 0.5);
128 
129     QPen linePen(color, lineWidth, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
130     linePen.setCosmetic(true);
131     QPen shadowPen(linePen);
132     shadowPen.setColor(Qt::black);
133 
134     QColor brushColor = color;
135     brushColor.setAlpha(50);
136     const QBrush fillBrush(brushColor);
137 
138     painter->setPen(Qt::NoPen);
139     painter->setBrush(fillBrush);
140 
141     QPainterPath path;
142 
143     const qreal radius = 10.0;
144     const qreal sweep = 235.0;
145     const qreal startAngle = 90.0 - sweep / 2;
146     QRectF rectangle(-radius, -radius, radius * 2, radius * 2);
147     path.moveTo(radius * cos(startAngle * M_PI / 180.0), -radius * sin(startAngle * M_PI / 180.0));
148     path.arcTo(rectangle, startAngle, sweep);
149     path.lineTo(0, 2 * radius);
150     path.closeSubpath();
151 
152     painter->translate(0, -2 * radius);
153 
154     painter->setPen(shadowPen);
155     painter->setBrush(Qt::NoBrush);
156     painter->drawPath(path.translated(shadowOffset));
157 
158     painter->setPen(linePen);
159     painter->setBrush(fillBrush);
160     painter->drawPath(path);
161 
162     const QBrush opaqueBrush(color);
163     painter->setBrush(opaqueBrush);
164     const qreal smallRadius = radius / 3.0;
165     painter->drawEllipse(QRectF(-smallRadius, -smallRadius, smallRadius * 2, smallRadius * 2));
166 }
167 
pointInteractionShape(const MapObject * object) const168 QPainterPath MapRenderer::pointInteractionShape(const MapObject *object) const
169 {
170     Q_ASSERT(object->shape() == MapObject::Point);
171     QPainterPath path;
172     path.addRect(QRect(-10, -30, 20, 30));
173     path.translate(pixelToScreenCoords(object->position()));
174     return path;
175 }
176 
drawTileLayer(QPainter * painter,const TileLayer * layer,const QRectF & exposed) const177 void MapRenderer::drawTileLayer(QPainter *painter, const TileLayer *layer, const QRectF &exposed) const
178 {
179     const QSize tileSize = map()->tileSize();
180 
181     // Don't draw more than the bounding rectangle of the given layer,
182     // intersected with the exposed rectangle.
183     QRect rect = boundingRect(layer->bounds());
184     if (!exposed.isNull())
185         rect &= exposed.toAlignedRect();
186 
187     // Draw margins extend the rendered area on the opposite side. We subtract
188     // the grid size because this has already been taken into account by
189     // boundingRect.
190     QMargins drawMargins = layer->drawMargins();
191     drawMargins.setTop(drawMargins.top() - tileSize.height());
192     drawMargins.setRight(drawMargins.right() - tileSize.width());
193     rect.adjust(-drawMargins.right(),
194                 -drawMargins.bottom(),
195                 drawMargins.left(),
196                 drawMargins.top());
197 
198     CellRenderer renderer(painter, this, layer->effectiveTintColor());
199 
200     auto tileRenderFunction = [layer, &renderer, tileSize](QPoint tilePos, const QPointF &screenPos) {
201         const Cell &cell = layer->cellAt(tilePos - layer->position());
202         if (!cell.isEmpty()) {
203             const Tile *tile = cell.tile();
204             const QSize size = (tile && !tile->image().isNull()) ? tile->size() : tileSize;
205             renderer.render(cell, screenPos, size, CellRenderer::BottomLeft);
206         }
207     };
208 
209     drawTileLayer(tileRenderFunction, rect);
210 }
211 
setFlag(RenderFlag flag,bool enabled)212 void MapRenderer::setFlag(RenderFlag flag, bool enabled)
213 {
214 #if QT_VERSION >= 0x050700
215     mFlags.setFlag(flag, enabled);
216 #else
217     if (enabled)
218         mFlags |= flag;
219     else
220         mFlags &= ~flag;
221 #endif
222 }
223 
224 /**
225  * Converts a line running from \a start to \a end to a polygon which
226  * extends 5 pixels from the line in all directions.
227  */
lineToPolygon(const QPointF & start,const QPointF & end)228 QPolygonF MapRenderer::lineToPolygon(const QPointF &start, const QPointF &end)
229 {
230     QPointF direction = QVector2D(end - start).normalized().toPointF();
231     QPointF perpendicular(-direction.y(), direction.x());
232 
233     const qreal thickness = 5.0; // 5 pixels on each side
234     direction *= thickness;
235     perpendicular *= thickness;
236 
237     QPolygonF polygon(4);
238     polygon[0] = start + perpendicular - direction;
239     polygon[1] = start - perpendicular - direction;
240     polygon[2] = end - perpendicular + direction;
241     polygon[3] = end + perpendicular + direction;
242     return polygon;
243 }
244 
245 /**
246  * Returns a MapRenderer instance matching the orientation of the map.
247  */
create(const Map * map)248 std::unique_ptr<MapRenderer> MapRenderer::create(const Map *map)
249 {
250     switch (map->orientation()) {
251     case Map::Isometric:
252         return std::make_unique<IsometricRenderer>(map);
253     case Map::Staggered:
254         return std::make_unique<StaggeredRenderer>(map);
255     case Map::Hexagonal:
256         return std::make_unique<HexagonalRenderer>(map);
257     default:
258         return std::make_unique<OrthogonalRenderer>(map);
259     }
260 }
261 
setupGridPens(const QPaintDevice * device,QColor color,QPen & gridPen,QPen & majorGridPen)262 void MapRenderer::setupGridPens(const QPaintDevice *device, QColor color,
263                                 QPen &gridPen, QPen &majorGridPen)
264 {
265     const qreal devicePixelRatio = device->devicePixelRatioF();
266 
267 #ifdef Q_OS_MAC
268     const qreal dpiScale = 1.0;
269 #else
270     const qreal dpiScale = device->logicalDpiX() / 96.0;
271 #endif
272 
273     const qreal dashLength = std::ceil(2.0 * dpiScale);
274 
275     color.setAlpha(96);
276 
277     gridPen = QPen(color, 1.0 * devicePixelRatio);
278     gridPen.setCosmetic(true);
279     gridPen.setDashPattern({dashLength, dashLength});
280 
281     color.setAlpha(192);
282 
283     majorGridPen = gridPen;
284     majorGridPen.setColor(color);
285 }
286 
287 
renderMissingImageMarker(QPainter & painter,const QRectF & rect)288 static void renderMissingImageMarker(QPainter &painter, const QRectF &rect)
289 {
290     QRectF r { rect.adjusted(0.5, 0.5, -0.5, -0.5) };
291     QPen pen { Qt::red, 1 };
292     pen.setCapStyle(Qt::FlatCap);
293     pen.setJoinStyle(Qt::MiterJoin);
294 
295     painter.save();
296     painter.fillRect(r, QColor(0, 0, 0, 128));
297     painter.setRenderHint(QPainter::Antialiasing);
298     painter.setPen(pen);
299     painter.drawRect(r);
300     painter.drawLine(r.topLeft(), r.bottomRight());
301     painter.drawLine(r.topRight(), r.bottomLeft());
302     painter.restore();
303 }
304 
hasOpenGLEngine(const QPainter * painter)305 static bool hasOpenGLEngine(const QPainter *painter)
306 {
307     const QPaintEngine::Type type = painter->paintEngine()->type();
308     return (type == QPaintEngine::OpenGL ||
309             type == QPaintEngine::OpenGL2);
310 }
311 
CellRenderer(QPainter * painter,const MapRenderer * renderer,const QColor & tintColor)312 CellRenderer::CellRenderer(QPainter *painter, const MapRenderer *renderer, const QColor &tintColor)
313     : mPainter(painter)
314     , mRenderer(renderer)
315     , mTile(nullptr)
316     , mIsOpenGL(hasOpenGLEngine(painter))
317     , mTintColor(tintColor)
318 {
319 }
320 
321 /**
322  * Renders a \a cell with the given \a origin at \a pos, taking into account
323  * the flipping and tile offset.
324  *
325  * For performance reasons, the actual drawing is delayed until a different
326  * kind of tile has to be drawn. For this reason it is necessary to call
327  * flush when finished doing drawCell calls. This function is also called by
328  * the destructor so usually an explicit call is not needed.
329  *
330  * This call expects `painter.translate(pos)` to correspond to the Origin point.
331  */
render(const Cell & cell,const QPointF & screenPos,const QSizeF & size,Origin origin)332 void CellRenderer::render(const Cell &cell, const QPointF &screenPos, const QSizeF &size, Origin origin)
333 {
334     const Tile *tile = cell.tile();
335 
336     if (tile)
337         tile = tile->currentFrameTile();
338 
339     if (!tile || tile->image().isNull()) {
340         QRectF target { screenPos, size };
341 
342         if (origin == BottomLeft)
343             target.translate(0.0, -size.height());
344 
345         renderMissingImageMarker(*mPainter, target);
346         return;
347     }
348 
349     // The USHRT_MAX limit is rather arbitrary but avoids a crash in
350     // drawPixmapFragments for a large number of fragments.
351     if (mTile != tile || mFragments.size() == USHRT_MAX)
352         flush();
353 
354     const QPixmap &image = tile->image();
355     const QSizeF imageSize = image.size();
356     if (imageSize.isEmpty())
357         return;
358 
359     const QSizeF scale(size.width() / imageSize.width(), size.height() / imageSize.height());
360     const QPoint offset = tile->offset();
361     const QPointF sizeHalf = QPointF(size.width() / 2, size.height() / 2);
362 
363     bool flippedHorizontally = cell.flippedHorizontally();
364     bool flippedVertically = cell.flippedVertically();
365 
366     QPainter::PixmapFragment fragment;
367     // Calculate the position as if the origin is TopLeft, and correct it later.
368     fragment.x = screenPos.x() + (offset.x() * scale.width()) + sizeHalf.x();
369     fragment.y = screenPos.y() + (offset.y() * scale.height()) + sizeHalf.y();
370     fragment.sourceLeft = 0;
371     fragment.sourceTop = 0;
372     fragment.width = imageSize.width();
373     fragment.height = imageSize.height();
374     fragment.scaleX = flippedHorizontally ? -1 : 1;
375     fragment.scaleY = flippedVertically ? -1 : 1;
376     fragment.rotation = 0;
377     fragment.opacity = 1;
378 
379     // Correct the position if the origin is BottomLeft.
380     if (origin == BottomLeft)
381         fragment.y -= size.height();
382 
383     if (mRenderer->cellType() == MapRenderer::HexagonalCells) {
384 
385         if (cell.flippedAntiDiagonally())
386             fragment.rotation += 60;
387 
388         if (cell.rotatedHexagonal120())
389             fragment.rotation += 120;
390 
391     } else if (cell.flippedAntiDiagonally()) {
392         fragment.rotation = 90;
393 
394         flippedHorizontally = flippedVertically;
395         flippedVertically = !cell.flippedHorizontally();
396 
397         // Compensate for the swap of image dimensions
398         const qreal halfDiff = sizeHalf.y() - sizeHalf.x();
399         fragment.y += halfDiff;
400         fragment.x += halfDiff;
401     }
402 
403     fragment.scaleX = scale.width() * (flippedHorizontally ? -1 : 1);
404     fragment.scaleY = scale.height() * (flippedVertically ? -1 : 1);
405 
406     if (mIsOpenGL || (fragment.scaleX > 0 && fragment.scaleY > 0)) {
407         mTile = tile;
408         mFragments.append(fragment);
409         return;
410     }
411 
412     // The Raster paint engine as of Qt 4.8.4 / 5.0.2 does not support
413     // drawing fragments with a negative scaling factor.
414 
415     flush(); // make sure we drew all tiles so far
416 
417     const QTransform oldTransform = mPainter->transform();
418     QTransform transform = oldTransform;
419     transform.translate(fragment.x, fragment.y);
420     transform.rotate(fragment.rotation);
421     transform.scale(fragment.scaleX, fragment.scaleY);
422 
423     const QRectF target(fragment.width * -0.5, fragment.height * -0.5,
424                         fragment.width, fragment.height);
425     const QRectF source(0, 0, fragment.width, fragment.height);
426 
427     mPainter->setTransform(transform);
428     mPainter->drawPixmap(target, tinted(image, mTintColor), source);
429     mPainter->setTransform(oldTransform);
430 
431     // A bit of a hack to still draw tile collision shapes when requested
432     if (mRenderer->flags().testFlag(ShowTileCollisionShapes)
433             && tile->objectGroup()
434             && !tile->objectGroup()->objects().isEmpty()) {
435         mTile = tile;
436         mFragments.append(fragment);
437         paintTileCollisionShapes();
438         mTile = nullptr;
439         mFragments.resize(0);
440     }
441 }
442 
443 /**
444  * Renders any remaining cells.
445  */
flush()446 void CellRenderer::flush()
447 {
448     if (!mTile)
449         return;
450 
451     mPainter->drawPixmapFragments(mFragments.constData(),
452                                   mFragments.size(),
453                                   tinted(mTile->image(), mTintColor));
454 
455     if (mRenderer->flags().testFlag(ShowTileCollisionShapes)
456             && mTile->objectGroup()
457             && !mTile->objectGroup()->objects().isEmpty()) {
458         paintTileCollisionShapes();
459     }
460 
461     mTile = nullptr;
462     mFragments.resize(0);
463 }
464 
465 /**
466  * Returns a transform that rotates by \a rotation degrees around the given
467  * \a position.
468  */
rotateAt(const QPointF & position,qreal rotation)469 static QTransform rotateAt(const QPointF &position, qreal rotation)
470 {
471     QTransform transform;
472     transform.translate(position.x(), position.y());
473     transform.rotate(rotation);
474     transform.translate(-position.x(), -position.y());
475     return transform;
476 }
477 
paintTileCollisionShapes()478 void CellRenderer::paintTileCollisionShapes()
479 {
480     const Tileset *tileset = mTile->tileset();
481     const bool isIsometric = tileset->orientation() == Tileset::Isometric;
482     Map::Parameters mapParameters;
483     mapParameters.orientation = isIsometric ? Map::Isometric : Map::Orthogonal;
484     mapParameters.tileWidth = tileset->gridSize().width();
485     mapParameters.tileHeight = tileset->gridSize().height();
486     const Map map(mapParameters);
487     const auto renderer = MapRenderer::create(&map);
488 
489     const qreal lineWidth = mRenderer->objectLineWidth();
490     const qreal shadowDist = (lineWidth == 0 ? 1 : lineWidth) / mRenderer->painterScale();
491     const QPointF shadowOffset = QPointF(shadowDist * 0.5, shadowDist * 0.5);
492 
493     QPen shadowPen(Qt::black);
494     shadowPen.setCosmetic(true);
495     shadowPen.setJoinStyle(Qt::RoundJoin);
496     shadowPen.setCapStyle(Qt::RoundCap);
497     shadowPen.setWidthF(lineWidth);
498     shadowPen.setStyle(Qt::DotLine);
499 
500     mPainter->setRenderHint(QPainter::Antialiasing);
501 
502     for (const auto &fragment : qAsConst(mFragments)) {
503         QTransform tileTransform;
504         tileTransform.translate(fragment.x, fragment.y);
505         tileTransform.rotate(fragment.rotation);
506         tileTransform.scale(fragment.scaleX, fragment.scaleY);
507         tileTransform.translate(-fragment.width * 0.5, -fragment.height * 0.5);
508 
509         if (isIsometric)
510             tileTransform.translate(0, fragment.height - tileset->gridSize().height());
511 
512         for (MapObject *object : mTile->objectGroup()->objects()) {
513             QColor penColor = object->effectiveColor();
514             QColor brushColor = penColor;
515             brushColor.setAlpha(50);
516             QPen colorPen(shadowPen);
517             colorPen.setColor(penColor);
518 
519             mPainter->setPen(colorPen);
520             mPainter->setBrush(brushColor);
521 
522             auto transform = rotateAt(renderer->pixelToScreenCoords(object->position()),
523                                       object->rotation());
524             transform *= tileTransform;
525 
526             const auto shape = transform.map(renderer->shape(object));
527 
528             mPainter->strokePath(shape.translated(shadowOffset), shadowPen);
529 
530             if (object->shape() == MapObject::Polyline)
531                 mPainter->strokePath(shape, colorPen);
532             else
533                 mPainter->drawPath(shape);
534         }
535     }
536 }
537