blob: 9960845565f0b3ddf49b39f29c8436ba79a2e8d6 [file] [log] [blame]
epoger@google.comec3ed6a2011-07-28 14:26:00 +00001/*
2 * Copyright 2011 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
Mike Reedebfce6d2016-12-12 10:02:12 -05007
Mike Kleinc0bd9f92019-04-23 12:05:21 -05008#include "gm/gm.h"
9#include "include/core/SkCanvas.h"
Ben Wagner7fde8e12019-05-01 17:28:53 -040010#include "include/core/SkClipOp.h"
11#include "include/core/SkColor.h"
Mike Kleinc0bd9f92019-04-23 12:05:21 -050012#include "include/core/SkFont.h"
Ben Wagner7fde8e12019-05-01 17:28:53 -040013#include "include/core/SkFontTypes.h"
14#include "include/core/SkPaint.h"
Mike Kleinc0bd9f92019-04-23 12:05:21 -050015#include "include/core/SkPath.h"
Ben Wagner7fde8e12019-05-01 17:28:53 -040016#include "include/core/SkRect.h"
17#include "include/core/SkScalar.h"
18#include "include/core/SkSize.h"
19#include "include/core/SkString.h"
20#include "include/core/SkTypeface.h"
21#include "include/core/SkTypes.h"
Michael Ludwig49203842020-06-02 17:27:07 -040022#include "include/effects/SkGradientShader.h"
Ben Wagner7fde8e12019-05-01 17:28:53 -040023#include "src/core/SkClipOpPriv.h"
Mike Reed121c2af2020-03-10 14:02:56 -040024#include "tools/Resources.h"
Mike Kleinc0bd9f92019-04-23 12:05:21 -050025#include "tools/ToolUtils.h"
bsalomon@google.com807cec42011-03-10 19:20:15 +000026
Ben Wagner7fde8e12019-05-01 17:28:53 -040027#include <string.h>
28
bsalomon@google.com807cec42011-03-10 19:20:15 +000029namespace skiagm {
30
mtkleindbfd7ab2016-09-01 11:24:54 -070031constexpr SkColor gPathColor = SK_ColorBLACK;
32constexpr SkColor gClipAColor = SK_ColorBLUE;
33constexpr SkColor gClipBColor = SK_ColorRED;
reed@google.coma8f60f22011-12-08 16:18:29 +000034
bsalomon@google.com807cec42011-03-10 19:20:15 +000035class ComplexClipGM : public GM {
36public:
bsalomon6ae83cf2014-12-17 14:38:49 -080037 ComplexClipGM(bool aaclip, bool saveLayer, bool invertDraw)
robertphillips@google.com50a69a02012-07-12 13:48:46 +000038 : fDoAAClip(aaclip)
bsalomon6ae83cf2014-12-17 14:38:49 -080039 , fDoSaveLayer(saveLayer)
40 , fInvertDraw(invertDraw) {
caryclarkceb9f3b2015-06-12 10:00:11 -070041 this->setBGColor(0xFFDEDFDE);
bsalomon@google.com807cec42011-03-10 19:20:15 +000042 }
43
44protected:
Mike Reedbc414ed2018-08-16 22:49:55 -040045 SkString onShortName() override {
reed@google.coma8f60f22011-12-08 16:18:29 +000046 SkString str;
bsalomon6ae83cf2014-12-17 14:38:49 -080047 str.printf("complexclip_%s%s%s",
robertphillips@google.com50a69a02012-07-12 13:48:46 +000048 fDoAAClip ? "aa" : "bw",
bsalomon6ae83cf2014-12-17 14:38:49 -080049 fDoSaveLayer ? "_layer" : "",
50 fInvertDraw ? "_invert" : "");
reed@google.coma8f60f22011-12-08 16:18:29 +000051 return str;
bsalomon@google.com807cec42011-03-10 19:20:15 +000052 }
53
Michael Ludwig9689e392020-05-05 10:56:42 -040054 SkISize onISize() override { return SkISize::Make(388, 780); }
bsalomon@google.com807cec42011-03-10 19:20:15 +000055
Mike Reedbc414ed2018-08-16 22:49:55 -040056 void onDraw(SkCanvas* canvas) override {
bsalomon@google.com807cec42011-03-10 19:20:15 +000057 SkPath path;
Mike Reedbc414ed2018-08-16 22:49:55 -040058 path.moveTo(0, 50)
59 .quadTo(0, 0, 50, 0)
60 .lineTo(175, 0)
61 .quadTo(200, 0, 200, 25)
62 .lineTo(200, 150)
63 .quadTo(200, 200, 150, 200)
64 .lineTo(0, 200)
65 .close()
66 .moveTo(50, 50)
67 .lineTo(150, 50)
68 .lineTo(150, 125)
69 .quadTo(150, 150, 125, 150)
70 .lineTo(50, 150)
71 .close();
bsalomon6ae83cf2014-12-17 14:38:49 -080072 if (fInvertDraw) {
Mike Reed7d34dc72019-11-26 12:17:17 -050073 path.setFillType(SkPathFillType::kInverseEvenOdd);
bsalomon6ae83cf2014-12-17 14:38:49 -080074 } else {
Mike Reed7d34dc72019-11-26 12:17:17 -050075 path.setFillType(SkPathFillType::kEvenOdd);
bsalomon6ae83cf2014-12-17 14:38:49 -080076 }
bsalomon@google.com807cec42011-03-10 19:20:15 +000077 SkPaint pathPaint;
78 pathPaint.setAntiAlias(true);
reed@google.coma8f60f22011-12-08 16:18:29 +000079 pathPaint.setColor(gPathColor);
bsalomon@google.com807cec42011-03-10 19:20:15 +000080
81 SkPath clipA;
Mike Reedbc414ed2018-08-16 22:49:55 -040082 clipA.addPoly({{10, 20}, {165, 22}, {70, 105}, {165, 177}, {-5, 180}}, false).close();
bsalomon@google.com807cec42011-03-10 19:20:15 +000083
84 SkPath clipB;
Mike Reedbc414ed2018-08-16 22:49:55 -040085 clipB.addPoly({{40, 10}, {190, 15}, {195, 190}, {40, 185}, {155, 100}}, false).close();
bsalomon@google.com807cec42011-03-10 19:20:15 +000086
Mike Kleinea3f0142019-03-20 11:12:10 -050087 SkFont font(ToolUtils::create_portable_typeface(), 20);
bsalomon@google.com807cec42011-03-10 19:20:15 +000088
mtkleindbfd7ab2016-09-01 11:24:54 -070089 constexpr struct {
Mike Reedc1f77742016-12-09 09:00:50 -050090 SkClipOp fOp;
reed73603f32016-09-20 08:42:38 -070091 const char* fName;
bsalomon@google.com807cec42011-03-10 19:20:15 +000092 } gOps[] = { //extra spaces in names for measureText
Mike Reedc1f77742016-12-09 09:00:50 -050093 {kIntersect_SkClipOp, "Isect "},
94 {kDifference_SkClipOp, "Diff " },
bsalomon@google.com807cec42011-03-10 19:20:15 +000095 };
96
Mike Reedbc414ed2018-08-16 22:49:55 -040097 canvas->translate(20, 20);
bsalomon@google.com807cec42011-03-10 19:20:15 +000098 canvas->scale(3 * SK_Scalar1 / 4, 3 * SK_Scalar1 / 4);
bsalomon@google.com807cec42011-03-10 19:20:15 +000099
robertphillips@google.com54bb7ab2012-07-13 14:55:25 +0000100 if (fDoSaveLayer) {
101 // We want the layer to appear symmetric relative to actual
102 // device boundaries so we need to "undo" the effect of the
103 // scale and translate
104 SkRect bounds = SkRect::MakeLTRB(
commit-bot@chromium.org4b413c82013-11-25 19:44:07 +0000105 4.0f/3.0f * -20,
106 4.0f/3.0f * -20,
107 4.0f/3.0f * (this->getISize().fWidth - 20),
108 4.0f/3.0f * (this->getISize().fHeight - 20));
robertphillips@google.com54bb7ab2012-07-13 14:55:25 +0000109
Mike Reedbc414ed2018-08-16 22:49:55 -0400110 bounds.inset(100, 100);
robertphillips@google.com50a69a02012-07-12 13:48:46 +0000111 SkPaint boundPaint;
112 boundPaint.setColor(SK_ColorRED);
113 boundPaint.setStyle(SkPaint::kStroke_Style);
114 canvas->drawRect(bounds, boundPaint);
Ben Wagner788a2dc2018-05-09 13:23:38 -0400115 canvas->clipRect(bounds);
halcanary96fcdcc2015-08-27 07:41:13 -0700116 canvas->saveLayer(&bounds, nullptr);
robertphillips@google.com50a69a02012-07-12 13:48:46 +0000117 }
118
reed@google.coma8f60f22011-12-08 16:18:29 +0000119 for (int invBits = 0; invBits < 4; ++invBits) {
120 canvas->save();
bsalomon@google.com807cec42011-03-10 19:20:15 +0000121 for (size_t op = 0; op < SK_ARRAY_COUNT(gOps); ++op) {
reed@google.coma8f60f22011-12-08 16:18:29 +0000122 this->drawHairlines(canvas, path, clipA, clipB);
123
124 bool doInvA = SkToBool(invBits & 1);
125 bool doInvB = SkToBool(invBits & 2);
bsalomon@google.com807cec42011-03-10 19:20:15 +0000126 canvas->save();
127 // set clip
Mike Reed7d34dc72019-11-26 12:17:17 -0500128 clipA.setFillType(doInvA ? SkPathFillType::kInverseEvenOdd :
129 SkPathFillType::kEvenOdd);
130 clipB.setFillType(doInvB ? SkPathFillType::kInverseEvenOdd :
131 SkPathFillType::kEvenOdd);
reed66998382016-09-21 11:15:07 -0700132 canvas->clipPath(clipA, fDoAAClip);
reed@google.coma8f60f22011-12-08 16:18:29 +0000133 canvas->clipPath(clipB, gOps[op].fOp, fDoAAClip);
bsalomon@google.com807cec42011-03-10 19:20:15 +0000134
bsalomon6ae83cf2014-12-17 14:38:49 -0800135 // In the inverse case we need to prevent the draw from covering the whole
136 // canvas.
137 if (fInvertDraw) {
138 SkRect rectClip = clipA.getBounds();
139 rectClip.join(path.getBounds());
140 rectClip.join(path.getBounds());
141 rectClip.outset(5, 5);
142 canvas->clipRect(rectClip);
143 }
144
bsalomon@google.com807cec42011-03-10 19:20:15 +0000145 // draw path clipped
146 canvas->drawPath(path, pathPaint);
147 canvas->restore();
148
bsalomon@google.com807cec42011-03-10 19:20:15 +0000149
Mike Reed2e6db182018-12-15 13:45:33 -0500150 SkPaint paint;
Mike Reedbc414ed2018-08-16 22:49:55 -0400151 SkScalar txtX = 45;
reed@google.coma8f60f22011-12-08 16:18:29 +0000152 paint.setColor(gClipAColor);
153 const char* aTxt = doInvA ? "InvA " : "A ";
Ben Wagner51e15a62019-05-07 15:38:46 -0400154 canvas->drawSimpleText(aTxt, strlen(aTxt), SkTextEncoding::kUTF8, txtX, 220, font, paint);
155 txtX += font.measureText(aTxt, strlen(aTxt), SkTextEncoding::kUTF8);
bsalomon@google.com807cec42011-03-10 19:20:15 +0000156 paint.setColor(SK_ColorBLACK);
Ben Wagner51e15a62019-05-07 15:38:46 -0400157 canvas->drawSimpleText(gOps[op].fName, strlen(gOps[op].fName), SkTextEncoding::kUTF8, txtX, 220,
Mike Reed2e6db182018-12-15 13:45:33 -0500158 font, paint);
Ben Wagner51e15a62019-05-07 15:38:46 -0400159 txtX += font.measureText(gOps[op].fName, strlen(gOps[op].fName), SkTextEncoding::kUTF8);
reed@google.coma8f60f22011-12-08 16:18:29 +0000160 paint.setColor(gClipBColor);
161 const char* bTxt = doInvB ? "InvB " : "B ";
Ben Wagner51e15a62019-05-07 15:38:46 -0400162 canvas->drawSimpleText(bTxt, strlen(bTxt), SkTextEncoding::kUTF8, txtX, 220, font, paint);
bsalomon@google.com807cec42011-03-10 19:20:15 +0000163
Mike Reedbc414ed2018-08-16 22:49:55 -0400164 canvas->translate(250,0);
bsalomon@google.com807cec42011-03-10 19:20:15 +0000165 }
reed@google.coma8f60f22011-12-08 16:18:29 +0000166 canvas->restore();
Mike Reedbc414ed2018-08-16 22:49:55 -0400167 canvas->translate(0, 250);
bsalomon@google.com807cec42011-03-10 19:20:15 +0000168 }
robertphillips@google.com50a69a02012-07-12 13:48:46 +0000169
robertphillips@google.com54bb7ab2012-07-13 14:55:25 +0000170 if (fDoSaveLayer) {
robertphillips@google.com50a69a02012-07-12 13:48:46 +0000171 canvas->restore();
172 }
bsalomon@google.com807cec42011-03-10 19:20:15 +0000173 }
174private:
reed@google.coma8f60f22011-12-08 16:18:29 +0000175 void drawHairlines(SkCanvas* canvas, const SkPath& path,
176 const SkPath& clipA, const SkPath& clipB) {
177 SkPaint paint;
178 paint.setAntiAlias(true);
179 paint.setStyle(SkPaint::kStroke_Style);
180 const SkAlpha fade = 0x33;
181
182 // draw path in hairline
183 paint.setColor(gPathColor); paint.setAlpha(fade);
184 canvas->drawPath(path, paint);
rmistry@google.comd6176b02012-08-23 18:14:13 +0000185
reed@google.coma8f60f22011-12-08 16:18:29 +0000186 // draw clips in hair line
187 paint.setColor(gClipAColor); paint.setAlpha(fade);
188 canvas->drawPath(clipA, paint);
189 paint.setColor(gClipBColor); paint.setAlpha(fade);
190 canvas->drawPath(clipB, paint);
191 }
192
bsalomon6ae83cf2014-12-17 14:38:49 -0800193 bool fDoAAClip;
194 bool fDoSaveLayer;
195 bool fInvertDraw;
196
bsalomon@google.com807cec42011-03-10 19:20:15 +0000197 typedef GM INHERITED;
198};
199
200//////////////////////////////////////////////////////////////////////////////
201
halcanary385fe4d2015-08-26 13:07:48 -0700202DEF_GM(return new ComplexClipGM(false, false, false);)
203DEF_GM(return new ComplexClipGM(false, false, true);)
204DEF_GM(return new ComplexClipGM(false, true, false);)
205DEF_GM(return new ComplexClipGM(false, true, true);)
206DEF_GM(return new ComplexClipGM(true, false, false);)
207DEF_GM(return new ComplexClipGM(true, false, true);)
208DEF_GM(return new ComplexClipGM(true, true, false);)
209DEF_GM(return new ComplexClipGM(true, true, true);)
bsalomon@google.com807cec42011-03-10 19:20:15 +0000210}
Mike Reed121c2af2020-03-10 14:02:56 -0400211
212DEF_SIMPLE_GM(clip_shader, canvas, 840, 650) {
213 auto img = GetResourceAsImage("images/yellow_rose.png");
214 auto sh = img->makeShader();
215
216 SkRect r = SkRect::MakeIWH(img->width(), img->height());
217 SkPaint p;
218
219 canvas->translate(10, 10);
220 canvas->drawImage(img, 0, 0, nullptr);
221
222 canvas->save();
223 canvas->translate(img->width() + 10, 0);
224 canvas->clipShader(sh, SkClipOp::kIntersect);
225 p.setColor(SK_ColorRED);
226 canvas->drawRect(r, p);
227 canvas->restore();
228
229 canvas->save();
230 canvas->translate(0, img->height() + 10);
231 canvas->clipShader(sh, SkClipOp::kDifference);
232 p.setColor(SK_ColorGREEN);
233 canvas->drawRect(r, p);
234 canvas->restore();
235
236 canvas->save();
237 canvas->translate(img->width() + 10, img->height() + 10);
238 canvas->clipShader(sh, SkClipOp::kIntersect);
239 canvas->save();
Mike Reed1f607332020-05-21 12:11:27 -0400240 SkMatrix lm = SkMatrix::Scale(1.0f/5, 1.0f/5);
Mike Reed121c2af2020-03-10 14:02:56 -0400241 canvas->clipShader(img->makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat, &lm));
242 canvas->drawImage(img, 0, 0, nullptr);
243
244 canvas->restore();
245 canvas->restore();
246}
Mike Reed84a9eb52020-03-12 13:55:44 -0400247
248DEF_SIMPLE_GM(clip_shader_layer, canvas, 430, 320) {
249 auto img = GetResourceAsImage("images/yellow_rose.png");
250 auto sh = img->makeShader();
251
252 SkRect r = SkRect::MakeIWH(img->width(), img->height());
253
254 canvas->translate(10, 10);
255 // now add the cool clip
256 canvas->clipRect(r);
257 canvas->clipShader(sh);
258 // now draw a layer with the same image, and watch it get restored w/ the clip
259 canvas->saveLayer(&r, nullptr);
260 canvas->drawColor(0xFFFF0000);
261 canvas->restore();
262}
Michael Ludwig49203842020-06-02 17:27:07 -0400263
264DEF_SIMPLE_GM(clip_shader_nested, canvas, 256, 256) {
265 float w = 64.f;
266 float h = 64.f;
267
268 const SkColor gradColors[] = {SK_ColorBLACK, SkColorSetARGB(128, 128, 128, 128)};
269 auto s = SkGradientShader::MakeRadial({0.5f * w, 0.5f * h}, 0.1f * w, gradColors, nullptr,
270 2, SkTileMode::kRepeat, 0, nullptr);
271
272 SkPaint p;
273
274 // A large black rect affected by two gradient clips
275 canvas->save();
276 canvas->clipShader(s);
277 canvas->scale(2.f, 2.f);
278 canvas->clipShader(s);
279 canvas->drawRect(SkRect::MakeWH(w, h), p);
280 canvas->restore();
281
282 canvas->translate(0.f, 2.f * h);
283
284 // A small red rect, with no clipping
285 canvas->save();
286 p.setColor(SK_ColorRED);
287 canvas->drawRect(SkRect::MakeWH(w, h), p);
288 canvas->restore();
289}
Michael Ludwig88b3b152020-06-03 10:22:49 -0400290
291namespace {
292
293// Where is canvas->concat(persp) called relative to the clipShader calls.
294enum ConcatPerspective {
295 kConcatBeforeClips,
296 kConcatAfterClips,
297 kConcatBetweenClips
298};
299// Order in which clipShader(image) and clipShader(gradient) are specified; only meaningful
300// when CanvasPerspective is kConcatBetweenClips.
301enum ClipOrder {
302 kClipImageFirst,
303 kClipGradientFirst,
304
305 kDoesntMatter = kClipImageFirst
306};
307// Which shaders have perspective applied as a local matrix.
308enum LocalMatrix {
309 kNoLocalMat,
310 kImageWithLocalMat,
311 kGradientWithLocalMat,
312 kBothWithLocalMat
313};
314struct Config {
315 ConcatPerspective fConcat;
316 ClipOrder fOrder;
317 LocalMatrix fLM;
318};
319
320static void draw_banner(SkCanvas* canvas, Config config) {
321 SkString banner;
322 banner.append("Persp: ");
323
324 if (config.fConcat == kConcatBeforeClips || config.fLM == kBothWithLocalMat) {
325 banner.append("Both Clips");
326 } else {
327 SkASSERT((config.fConcat == kConcatBetweenClips && config.fLM == kNoLocalMat) ||
328 (config.fConcat == kConcatAfterClips && (config.fLM == kImageWithLocalMat ||
329 config.fLM == kGradientWithLocalMat)));
330 if ((config.fConcat == kConcatBetweenClips && config.fOrder == kClipImageFirst) ||
331 config.fLM == kGradientWithLocalMat) {
332 banner.append("Gradient");
333 } else {
334 SkASSERT(config.fOrder == kClipGradientFirst || config.fLM == kImageWithLocalMat);
335 banner.append("Image");
336 }
337 }
338 if (config.fLM != kNoLocalMat) {
339 banner.append(" (w/ LM, should equal top row)");
340 }
341
342 static const SkFont kFont(ToolUtils::create_portable_typeface(), 12);
343 canvas->drawString(banner.c_str(), 20.f, -30.f, kFont, SkPaint());
344};
345
346} // anonymous
347
348DEF_SIMPLE_GM(clip_shader_persp, canvas, 1370, 1030) {
349 // Each draw has a clipShader(image-shader), a clipShader(gradient-shader), a concat(persp-mat),
350 // and each shader may or may not be wrapped with a perspective local matrix.
351
352 // Pairs of configs that should match in appearance where first config doesn't use a local
353 // matrix (top row of GM) and the second does (bottom row of GM).
354 Config matches[][2] = {
355 // Everything has perspective
356 {{kConcatBeforeClips, kDoesntMatter, kNoLocalMat},
357 {kConcatAfterClips, kDoesntMatter, kBothWithLocalMat}},
358 // Image shader has perspective
359 {{kConcatBetweenClips, kClipGradientFirst, kNoLocalMat},
360 {kConcatAfterClips, kDoesntMatter, kImageWithLocalMat}},
361 // Gradient shader has perspective
362 {{kConcatBetweenClips, kClipImageFirst, kNoLocalMat},
363 {kConcatAfterClips, kDoesntMatter, kGradientWithLocalMat}}
364 };
365
366 // The image that is drawn
367 auto img = GetResourceAsImage("images/yellow_rose.png");
368 // Scale factor always applied to the image shader so that it tiles
369 SkMatrix scale = SkMatrix::Scale(1.f / 4.f, 1.f / 4.f);
370 // The perspective matrix applied wherever needed
371 SkPoint src[4];
372 SkRect::Make(img->dimensions()).toQuad(src);
373 SkPoint dst[4] = {{0, 80.f},
374 {img->width() + 28.f, -100.f},
375 {img->width() - 28.f, img->height() + 100.f},
376 {0.f, img->height() - 80.f}};
377 SkMatrix persp;
378 SkAssertResult(persp.setPolyToPoly(src, dst, 4));
379
380 SkMatrix perspScale = SkMatrix::Concat(persp, scale);
381
382 auto drawConfig = [&](Config config) {
383 canvas->save();
384
385 draw_banner(canvas, config);
386
387 // Make clipShaders (possibly with local matrices)
388 bool gradLM = config.fLM == kGradientWithLocalMat || config.fLM == kBothWithLocalMat;
389 const SkColor gradColors[] = {SK_ColorBLACK, SkColorSetARGB(128, 128, 128, 128)};
390 auto gradShader = SkGradientShader::MakeRadial({0.5f * img->width(), 0.5f * img->height()},
391 0.1f * img->width(), gradColors, nullptr, 2,
392 SkTileMode::kRepeat, 0,
393 gradLM ? &persp : nullptr);
394 bool imageLM = config.fLM == kImageWithLocalMat || config.fLM == kBothWithLocalMat;
395 auto imgShader = img->makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat,
396 imageLM ? perspScale : scale);
397
398 // Perspective before any clipShader
399 if (config.fConcat == kConcatBeforeClips) {
400 canvas->concat(persp);
401 }
402
403 // First clipshader
404 canvas->clipShader(config.fOrder == kClipImageFirst ? imgShader : gradShader);
405
406 // Perspective between clipShader
407 if (config.fConcat == kConcatBetweenClips) {
408 canvas->concat(persp);
409 }
410
411 // Second clipShader
412 canvas->clipShader(config.fOrder == kClipImageFirst ? gradShader : imgShader);
413
414 // Perspective after clipShader
415 if (config.fConcat == kConcatAfterClips) {
416 canvas->concat(persp);
417 }
418
419 // Actual draw and clip boundary are the same for all configs
420 canvas->clipRect(SkRect::MakeIWH(img->width(), img->height()));
421 canvas->clear(SK_ColorBLACK);
422 canvas->drawImage(img, 0, 0);
423
424 canvas->restore();
425 };
426
427 SkIRect grid = persp.mapRect(SkRect::Make(img->dimensions())).roundOut();
428 grid.fLeft -= 20; // manual adjust to look nicer
429
430 canvas->translate(10.f, 10.f);
431
432 for (size_t i = 0; i < SK_ARRAY_COUNT(matches); ++i) {
433 canvas->save();
434 canvas->translate(-grid.fLeft, -grid.fTop);
435 drawConfig(matches[i][0]);
436 canvas->translate(0.f, grid.height());
437 drawConfig(matches[i][1]);
438 canvas->restore();
439
440 canvas->translate(grid.width(), 0.f);
441 }
442}