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