1describe('Canvas Behavior', () => {
2    let container;
3
4    beforeEach(async () => {
5        await LoadCanvasKit;
6        container = document.createElement('div');
7        container.innerHTML = `
8            <canvas width=600 height=600 id=test></canvas>
9            <canvas width=600 height=600 id=report></canvas>`;
10        document.body.appendChild(container);
11    });
12
13    afterEach(() => {
14        document.body.removeChild(container);
15    });
16
17    gm('canvas_api_example', (canvas) => {
18        const paint = new CanvasKit.Paint();
19        paint.setStrokeWidth(2.0);
20        paint.setAntiAlias(true);
21        paint.setColor(CanvasKit.Color(0, 0, 0, 1.0));
22        paint.setStyle(CanvasKit.PaintStyle.Stroke);
23
24        canvas.drawLine(3, 10, 30, 15, paint);
25        const rrect = CanvasKit.RRectXY([5, 35, 45, 80], 15, 10);
26        canvas.drawRRect(rrect, paint);
27
28        canvas.drawOval(CanvasKit.LTRBRect(5, 35, 45, 80), paint);
29
30        canvas.drawArc(CanvasKit.LTRBRect(55, 35, 95, 80), 15, 270, true, paint);
31
32        const font = new CanvasKit.Font(null, 20);
33        canvas.drawText('this is ascii text', 5, 100, paint, font);
34
35        const blob = CanvasKit.TextBlob.MakeFromText('Unicode chars � é É ص', font);
36        canvas.drawTextBlob(blob, 5, 130, paint);
37
38        font.delete();
39        blob.delete();
40        paint.delete();
41        // See canvas2d for more API tests
42    });
43
44    gm('effect_and_text_example', (canvas) => {
45        const path = starPath(CanvasKit);
46        const paint = new CanvasKit.Paint();
47
48        const textPaint = new CanvasKit.Paint();
49        textPaint.setColor(CanvasKit.Color(40, 0, 0, 1.0));
50        textPaint.setAntiAlias(true);
51
52        const textFont = new CanvasKit.Font(null, 30);
53
54        const dpe = CanvasKit.PathEffect.MakeDash([15, 5, 5, 10], 1);
55
56        paint.setPathEffect(dpe);
57        paint.setStyle(CanvasKit.PaintStyle.Stroke);
58        paint.setStrokeWidth(5.0);
59        paint.setAntiAlias(true);
60        paint.setColor(CanvasKit.Color(66, 129, 164, 1.0));
61
62        canvas.clear(CanvasKit.Color(255, 255, 255, 1.0));
63
64        canvas.drawPath(path, paint);
65        canvas.drawText('This is text', 10, 280, textPaint, textFont);
66
67        dpe.delete();
68        path.delete();
69        paint.delete();
70        textFont.delete();
71        textPaint.delete();
72    });
73
74    gm('patheffects_canvas', (canvas) => {
75        canvas.clear(CanvasKit.WHITE);
76        const path = starPath(CanvasKit, 100, 100, 100);
77        const paint = new CanvasKit.Paint();
78
79        const cornerEffect = CanvasKit.PathEffect.MakeCorner(10);
80        const discreteEffect = CanvasKit.PathEffect.MakeDiscrete(5, 10, 0);
81
82        paint.setPathEffect(cornerEffect);
83        paint.setStyle(CanvasKit.PaintStyle.Stroke);
84        paint.setStrokeWidth(5.0);
85        paint.setAntiAlias(true);
86        paint.setColor(CanvasKit.Color(66, 129, 164, 1.0));
87        canvas.drawPath(path, paint);
88
89        canvas.translate(200, 0);
90
91        paint.setPathEffect(discreteEffect);
92        canvas.drawPath(path, paint);
93
94        cornerEffect.delete();
95        path.delete();
96        paint.delete();
97    });
98
99    it('returns the depth of the save state stack', () => {
100        const canvas = new CanvasKit.Canvas();
101        expect(canvas.getSaveCount()).toEqual(1);
102        canvas.save();
103        canvas.save();
104        canvas.restore();
105        canvas.save();
106        canvas.save();
107        expect(canvas.getSaveCount()).toEqual(4);
108        // does nothing, by the SkCanvas API
109        canvas.restoreToCount(500);
110        expect(canvas.getSaveCount()).toEqual(4);
111        canvas.restore();
112        expect(canvas.getSaveCount()).toEqual(3);
113        canvas.save();
114        canvas.restoreToCount(2);
115        expect(canvas.getSaveCount()).toEqual(2);
116    });
117
118    gm('circle_canvas', (canvas) => {
119        const path = starPath(CanvasKit);
120
121        const paint = new CanvasKit.Paint();
122
123        paint.setStyle(CanvasKit.PaintStyle.Stroke);
124        paint.setStrokeWidth(5.0);
125        paint.setAntiAlias(true);
126        paint.setColor(CanvasKit.CYAN);
127
128        canvas.clear(CanvasKit.WHITE);
129
130        canvas.drawCircle(30, 50, 15, paint);
131
132        paint.setStyle(CanvasKit.PaintStyle.Fill);
133        paint.setColor(CanvasKit.RED);
134        canvas.drawCircle(130, 80, 60, paint);
135        canvas.drawCircle(20, 150, 60, paint);
136
137        path.delete();
138        paint.delete();
139    });
140
141    gm('rrect_canvas', (canvas) => {
142        const path = starPath(CanvasKit);
143
144        const paint = new CanvasKit.Paint();
145
146        paint.setStyle(CanvasKit.PaintStyle.Stroke);
147        paint.setStrokeWidth(3.0);
148        paint.setAntiAlias(true);
149        paint.setColor(CanvasKit.BLACK);
150
151        canvas.clear(CanvasKit.WHITE);
152
153        canvas.drawRRect(CanvasKit.RRectXY(
154            CanvasKit.LTRBRect(10, 10, 50, 50), 5, 10), paint);
155
156        canvas.drawRRect(CanvasKit.RRectXY(
157            CanvasKit.LTRBRect(60, 10, 110, 50), 10, 5), paint);
158
159        canvas.drawRRect(CanvasKit.RRectXY(
160            CanvasKit.LTRBRect(10, 60, 210, 260), 0, 30), paint);
161
162        canvas.drawRRect(CanvasKit.RRectXY(
163            CanvasKit.LTRBRect(50, 90, 160, 210), 30, 30), paint);
164
165        path.delete();
166        paint.delete();
167    });
168
169    gm('rrect_8corners_canvas', (canvas) => {
170        const path = starPath(CanvasKit);
171
172        const paint = new CanvasKit.Paint();
173
174        paint.setStyle(CanvasKit.PaintStyle.Stroke);
175        paint.setStrokeWidth(3.0);
176        paint.setAntiAlias(true);
177        paint.setColor(CanvasKit.BLACK);
178
179        canvas.clear(CanvasKit.WHITE);
180
181        canvas.drawRRect([10, 10, 210, 210,
182          // top left corner, going clockwise
183          10, 30,
184          30, 10,
185          50, 75,
186          120, 120,
187        ], paint);
188
189        path.delete();
190        paint.delete();
191    });
192
193    // As above, except with the array passed in via malloc'd memory.
194    gm('rrect_8corners_malloc_canvas', (canvas) => {
195        const path = starPath(CanvasKit);
196
197        const paint = new CanvasKit.Paint();
198
199        paint.setStyle(CanvasKit.PaintStyle.Stroke);
200        paint.setStrokeWidth(3.0);
201        paint.setAntiAlias(true);
202        paint.setColor(CanvasKit.BLACK);
203
204        canvas.clear(CanvasKit.WHITE);
205
206        const rrect = CanvasKit.Malloc(Float32Array, 12);
207        rrect.toTypedArray().set([10, 10, 210, 210,
208          // top left corner, going clockwise
209          10, 30,
210          30, 10,
211          50, 75,
212          120, 120,
213        ]);
214
215        canvas.drawRRect(rrect, paint);
216
217        CanvasKit.Free(rrect);
218        path.delete();
219        paint.delete();
220    });
221
222    gm('drawDRRect_canvas', (canvas) => {
223        const path = starPath(CanvasKit);
224
225        const paint = new CanvasKit.Paint();
226
227        paint.setStyle(CanvasKit.PaintStyle.Fill);
228        paint.setStrokeWidth(3.0);
229        paint.setAntiAlias(true);
230        paint.setColor(CanvasKit.BLACK);
231
232        canvas.clear(CanvasKit.WHITE);
233
234        const outer = CanvasKit.RRectXY(CanvasKit.LTRBRect(10, 60, 210, 260), 10, 5);
235        const inner = CanvasKit.RRectXY(CanvasKit.LTRBRect(50, 90, 160, 210), 30, 30);
236
237        canvas.drawDRRect(outer, inner, paint);
238
239        path.delete();
240        paint.delete();
241    });
242
243    gm('colorfilters_canvas', (canvas) => {
244        const paint = new CanvasKit.Paint();
245
246        const blue = CanvasKit.ColorFilter.MakeBlend(
247            CanvasKit.BLUE, CanvasKit.BlendMode.SrcIn);
248        const red =  CanvasKit.ColorFilter.MakeBlend(
249            CanvasKit.Color(255, 0, 0, 0.8), CanvasKit.BlendMode.SrcOver);
250        const lerp = CanvasKit.ColorFilter.MakeLerp(0.6, red, blue);
251
252        paint.setStyle(CanvasKit.PaintStyle.Fill);
253        paint.setAntiAlias(true);
254
255        canvas.clear(CanvasKit.Color(230, 230, 230));
256
257        paint.setColorFilter(blue)
258        canvas.drawRect(CanvasKit.LTRBRect(10, 10, 60, 60), paint);
259        paint.setColorFilter(lerp)
260        canvas.drawRect(CanvasKit.LTRBRect(50, 10, 100, 60), paint);
261        paint.setColorFilter(red)
262        canvas.drawRect4f(90, 10, 140, 60, paint);
263
264        const r = CanvasKit.ColorMatrix.rotated(0, .707, -.707);
265        const b = CanvasKit.ColorMatrix.rotated(2, .5, .866);
266        const s = CanvasKit.ColorMatrix.scaled(0.9, 1.5, 0.8, 0.8);
267        let cm = CanvasKit.ColorMatrix.concat(r, s);
268        cm = CanvasKit.ColorMatrix.concat(cm, b);
269        CanvasKit.ColorMatrix.postTranslate(cm, 20, 0, -10, 0);
270
271        const mat = CanvasKit.ColorFilter.MakeMatrix(cm);
272        const final = CanvasKit.ColorFilter.MakeCompose(mat, lerp);
273
274        paint.setColorFilter(final)
275        canvas.drawRect(CanvasKit.LTRBRect(10, 70, 140, 120), paint);
276
277        paint.delete();
278        blue.delete();
279        red.delete();
280        lerp.delete();
281        final.delete();
282    });
283
284    gm('blendmodes_canvas', (canvas) => {
285        canvas.clear(CanvasKit.WHITE);
286
287        const blendModeNames = Object.keys(CanvasKit.BlendMode).filter((key) => key !== 'values');
288
289        const PASTEL_MUSTARD_YELLOW = CanvasKit.Color(248, 213, 85, 1.0);
290        const PASTEL_SKY_BLUE = CanvasKit.Color(74, 174, 245, 1.0);
291
292        const shapePaint = new CanvasKit.Paint();
293        shapePaint.setColor(PASTEL_MUSTARD_YELLOW);
294        shapePaint.setAntiAlias(true);
295
296        const textPaint = new CanvasKit.Paint();
297        textPaint.setAntiAlias(true);
298
299        const textFont = new CanvasKit.Font(null, 10);
300
301        let x = 10;
302        let y = 20;
303        for (const blendModeName of blendModeNames) {
304            // Draw a checkerboard for each blend mode.
305            // Each checkerboard is labelled with a blendmode's name.
306            canvas.drawText(blendModeName, x, y - 5, textPaint, textFont);
307            drawCheckerboard(canvas, x, y, x + 80, y + 80);
308
309            // A blue square is drawn on to each checkerboard with yellow circle.
310            // In each checkerboard the blue square is drawn using a different blendmode.
311            const blendMode = CanvasKit.BlendMode[blendModeName];
312            canvas.drawOval(CanvasKit.LTRBRect(x + 5, y + 5, x + 55, y + 55), shapePaint);
313            drawRectangle(x + 30, y + 30, x + 70, y + 70, PASTEL_SKY_BLUE, blendMode);
314
315            x += 90;
316            if (x > 500) {
317                x = 10;
318                y += 110;
319            }
320        }
321
322        function drawCheckerboard(canvas, x1, y1, x2, y2) {
323            const CHECKERBOARD_SQUARE_SIZE = 5;
324            const GREY = CanvasKit.Color(220, 220, 220, 0.5);
325            // Draw black border and white background for checkerboard
326            drawRectangle(x1-1, y1-1, x2+1, y2+1, CanvasKit.BLACK);
327            drawRectangle(x1, y1, x2, y2, CanvasKit.WHITE);
328
329            // Draw checkerboard squares
330            const numberOfColumns = (x2 - x1) / CHECKERBOARD_SQUARE_SIZE;
331            const numberOfRows = (y2 - y1) / CHECKERBOARD_SQUARE_SIZE
332
333            for (let row = 0; row < numberOfRows; row++) {
334                for (let column = 0; column < numberOfColumns; column++) {
335                    const rowIsEven = row % 2 === 0;
336                    const columnIsEven = column % 2 === 0;
337
338                    if ((rowIsEven && !columnIsEven) || (!rowIsEven && columnIsEven)) {
339                        drawRectangle(
340                            x1 + CHECKERBOARD_SQUARE_SIZE * row,
341                            y1 + CHECKERBOARD_SQUARE_SIZE * column,
342                            Math.min(x1 + CHECKERBOARD_SQUARE_SIZE * row + CHECKERBOARD_SQUARE_SIZE, x2),
343                            Math.min(y1 + CHECKERBOARD_SQUARE_SIZE * column + CHECKERBOARD_SQUARE_SIZE, y2),
344                            GREY
345                        );
346                    }
347                }
348            }
349        }
350
351        function drawRectangle(x1, y1, x2, y2, color, blendMode=CanvasKit.BlendMode.srcOver) {
352            canvas.save();
353            canvas.clipRect(CanvasKit.LTRBRect(x1, y1, x2, y2), CanvasKit.ClipOp.Intersect, true);
354            canvas.drawColor(color, blendMode);
355            canvas.restore();
356        }
357    });
358
359    gm('colorfilters_malloc_canvas', (canvas) => {
360        const paint = new CanvasKit.Paint();
361
362        const src = [
363             0.8,   0.45,      2,   0,  20,
364            0.53, -0.918,  0.566,   0,   0,
365            0.53, -0.918, -0.566,   0, -10,
366               0,      0,      0, 0.8,   0,
367        ]
368        const colorObj = new CanvasKit.Malloc(Float32Array, 20);
369        const cm = colorObj.toTypedArray();
370        for (i in src) {
371            cm[i] = src[i];
372        }
373        // MakeMatrix will free the malloc'd array when it is done with it.
374        const final = CanvasKit.ColorFilter.MakeMatrix(cm);
375
376        paint.setColorFilter(final)
377        canvas.drawRect(CanvasKit.LTRBRect(10, 70, 140, 120), paint);
378
379        CanvasKit.Free(colorObj);
380        paint.delete();
381        final.delete();
382    });
383
384    gm('clips_canvas', (canvas) => {
385        const path = starPath(CanvasKit);
386        const paint = new CanvasKit.Paint();
387        paint.setColor(CanvasKit.BLUE);
388        const rrect = CanvasKit.RRectXY(CanvasKit.LTRBRect(300, 300, 500, 500), 40, 40);
389
390        canvas.save();
391        // draw magenta around the outside edge of an rrect.
392        canvas.clipRRect(rrect, CanvasKit.ClipOp.Difference, true);
393        canvas.drawColorComponents(250/255, 30/255, 240/255, 0.9, CanvasKit.BlendMode.SrcOver);
394        canvas.restore();
395
396        // draw grey inside of a star pattern, then the blue star on top
397        canvas.clipPath(path, CanvasKit.ClipOp.Intersect, false);
398        canvas.drawColorInt(CanvasKit.ColorAsInt(200, 200, 200, 255), CanvasKit.BlendMode.SrcOver);
399        canvas.drawPath(path, paint);
400
401        path.delete();
402        paint.delete();
403    });
404
405    // inspired by https://fiddle.skia.org/c/feb2a08bb09ede5309678d6a0ab3f981
406    gm('savelayer_rect_paint_canvas', (canvas) => {
407        canvas.clear(CanvasKit.WHITE);
408        const redPaint = new CanvasKit.Paint();
409        redPaint.setColor(CanvasKit.RED);
410        const solidBluePaint = new CanvasKit.Paint();
411        solidBluePaint.setColor(CanvasKit.BLUE);
412
413        const thirtyBluePaint = new CanvasKit.Paint();
414        thirtyBluePaint.setColor(CanvasKit.BLUE);
415        thirtyBluePaint.setAlphaf(0.3);
416
417        const alpha = new CanvasKit.Paint();
418        alpha.setAlphaf(0.3);
419
420        // Draw 4 solid red rectangles on the 0th layer.
421        canvas.drawRect(CanvasKit.LTRBRect(10, 10, 60, 60), redPaint);
422        canvas.drawRect(CanvasKit.LTRBRect(150, 10, 200, 60), redPaint);
423        canvas.drawRect(CanvasKit.LTRBRect(10, 70, 60, 120), redPaint);
424        canvas.drawRect(CanvasKit.LTRBRect(150, 70, 200, 120), redPaint);
425
426        // Draw 2 blue rectangles that overlap. One is solid, the other
427        // is 30% transparent. We should see purple from the right one,
428        // the left one overlaps the red because it is opaque.
429        canvas.drawRect(CanvasKit.LTRBRect(30, 10, 80, 60), solidBluePaint);
430        canvas.drawRect(CanvasKit.LTRBRect(170, 10, 220, 60), thirtyBluePaint);
431
432        // Save a new layer. When the 1st layer gets merged onto the
433        // 0th layer (i.e. when restore() is called), it will use the provided
434        // paint to do so. The provided paint is set to have 30% opacity, but
435        // it could also have things set like blend modes or image filters.
436        // The rectangle is just a hint, so I've set it to be the area that
437        // we actually draw in before restore is called. It could also be omitted,
438        // see the test below.
439        canvas.saveLayer(alpha, CanvasKit.LTRBRect(10, 10, 220, 180));
440
441        // Draw the same blue overlapping rectangles as before. Notice in the
442        // final output, we have two different shades of purple instead of the
443        // solid blue overwriting the red. This proves the opacity was applied.
444        canvas.drawRect(CanvasKit.LTRBRect(30, 70, 80, 120), solidBluePaint);
445        canvas.drawRect(CanvasKit.LTRBRect(170, 70, 220, 120), thirtyBluePaint);
446
447        // We draw two more sets of overlapping red and blue rectangles. Notice
448        // the solid blue overwrites the red. This proves that the opacity from
449        // the alpha paint isn't available when the drawing happens - it only
450        // matters when restore() is called.
451        canvas.drawRect(CanvasKit.LTRBRect(10, 130, 60, 180), redPaint);
452        canvas.drawRect(CanvasKit.LTRBRect(30, 130, 80, 180), solidBluePaint);
453
454        canvas.drawRect(CanvasKit.LTRBRect(150, 130, 200, 180), redPaint);
455        canvas.drawRect(CanvasKit.LTRBRect(170, 130, 220, 180), thirtyBluePaint);
456
457        canvas.restore();
458
459        redPaint.delete();
460        solidBluePaint.delete();
461        thirtyBluePaint.delete();
462        alpha.delete();
463    });
464
465    // identical to the test above, except the save layer only has the paint, not
466    // the rectangle.
467    gm('savelayer_paint_canvas', (canvas) => {
468        canvas.clear(CanvasKit.WHITE);
469        const redPaint = new CanvasKit.Paint();
470        redPaint.setColor(CanvasKit.RED);
471        const solidBluePaint = new CanvasKit.Paint();
472        solidBluePaint.setColor(CanvasKit.BLUE);
473
474        const thirtyBluePaint = new CanvasKit.Paint();
475        thirtyBluePaint.setColor(CanvasKit.BLUE);
476        thirtyBluePaint.setAlphaf(0.3);
477
478        const alpha = new CanvasKit.Paint();
479        alpha.setAlphaf(0.3);
480
481        // Draw 4 solid red rectangles on the 0th layer.
482        canvas.drawRect(CanvasKit.LTRBRect(10, 10, 60, 60), redPaint);
483        canvas.drawRect(CanvasKit.LTRBRect(150, 10, 200, 60), redPaint);
484        canvas.drawRect(CanvasKit.LTRBRect(10, 70, 60, 120), redPaint);
485        canvas.drawRect(CanvasKit.LTRBRect(150, 70, 200, 120), redPaint);
486
487        // Draw 2 blue rectangles that overlap. One is solid, the other
488        // is 30% transparent. We should see purple from the right one,
489        // the left one overlaps the red because it is opaque.
490        canvas.drawRect(CanvasKit.LTRBRect(30, 10, 80, 60), solidBluePaint);
491        canvas.drawRect(CanvasKit.LTRBRect(170, 10, 220, 60), thirtyBluePaint);
492
493        // Save a new layer. When the 1st layer gets merged onto the
494        // 0th layer (i.e. when restore() is called), it will use the provided
495        // paint to do so. The provided paint is set to have 30% opacity, but
496        // it could also have things set like blend modes or image filters.
497        canvas.saveLayerPaint(alpha);
498
499        // Draw the same blue overlapping rectangles as before. Notice in the
500        // final output, we have two different shades of purple instead of the
501        // solid blue overwriting the red. This proves the opacity was applied.
502        canvas.drawRect(CanvasKit.LTRBRect(30, 70, 80, 120), solidBluePaint);
503        canvas.drawRect(CanvasKit.LTRBRect(170, 70, 220, 120), thirtyBluePaint);
504
505        // We draw two more sets of overlapping red and blue rectangles. Notice
506        // the solid blue overwrites the red. This proves that the opacity from
507        // the alpha paint isn't available when the drawing happens - it only
508        // matters when restore() is called.
509        canvas.drawRect(CanvasKit.LTRBRect(10, 130, 60, 180), redPaint);
510        canvas.drawRect(CanvasKit.LTRBRect(30, 130, 80, 180), solidBluePaint);
511
512        canvas.drawRect(CanvasKit.LTRBRect(150, 130, 200, 180), redPaint);
513        canvas.drawRect(CanvasKit.LTRBRect(170, 130, 220, 180), thirtyBluePaint);
514
515        canvas.restore();
516
517        redPaint.delete();
518        solidBluePaint.delete();
519        thirtyBluePaint.delete();
520        alpha.delete();
521    });
522
523    gm('savelayerrec_canvas', (canvas) => {
524        // Note: fiddle.skia.org quietly draws a white background before doing
525        // other things, which is noticed in cases like this where we use saveLayer
526        // with the rec struct.
527        canvas.clear(CanvasKit.WHITE);
528        canvas.scale(8, 8);
529        const redPaint = new CanvasKit.Paint();
530        redPaint.setColor(CanvasKit.RED);
531        redPaint.setAntiAlias(true);
532        canvas.drawCircle(21, 21, 8, redPaint);
533
534        const bluePaint = new CanvasKit.Paint();
535        bluePaint.setColor(CanvasKit.BLUE);
536        canvas.drawCircle(31, 21, 8, bluePaint);
537
538        const blurIF = CanvasKit.ImageFilter.MakeBlur(8, 0.2, CanvasKit.TileMode.Decal, null);
539
540        const count = canvas.saveLayer(null, null, blurIF, 0);
541        expect(count).toEqual(1);
542        canvas.scale(1/4, 1/4);
543        canvas.drawCircle(125, 85, 8, redPaint);
544        canvas.restore();
545
546        blurIF.delete();
547        redPaint.delete();
548        bluePaint.delete();
549    });
550
551    gm('drawpoints_canvas', (canvas) => {
552        canvas.clear(CanvasKit.WHITE);
553        const paint = new CanvasKit.Paint();
554        paint.setAntiAlias(true);
555        paint.setStyle(CanvasKit.PaintStyle.Stroke);
556        paint.setStrokeWidth(10);
557        paint.setColor(CanvasKit.Color(153, 204, 162, 0.82));
558
559        const points = [32, 16, 48, 48, 16, 32];
560
561        const caps = [CanvasKit.StrokeCap.Round, CanvasKit.StrokeCap.Square,
562                      CanvasKit.StrokeCap.Butt];
563        const joins = [CanvasKit.StrokeJoin.Round, CanvasKit.StrokeJoin.Miter,
564                       CanvasKit.StrokeJoin.Bevel];
565        const modes = [CanvasKit.PointMode.Points, CanvasKit.PointMode.Lines,
566                       CanvasKit.PointMode.Polygon];
567
568        for (let i = 0; i < caps.length; i++) {
569            paint.setStrokeCap(caps[i]);
570            paint.setStrokeJoin(joins[i]);
571
572            for (const m of modes) {
573                canvas.drawPoints(m, points, paint);
574                canvas.translate(64, 0);
575            }
576            // Try with the malloc approach. Note that the drawPoints
577            // will free the pointer when done.
578            const mPointsObj = CanvasKit.Malloc(Float32Array, 3*2);
579            const mPoints = mPointsObj.toTypedArray();
580            mPoints.set([32, 16, 48, 48, 16, 32]);
581
582            // The obj from Malloc can be passed in instead of the typed array.
583            canvas.drawPoints(CanvasKit.PointMode.Polygon, mPointsObj, paint);
584            canvas.translate(-192, 64);
585            CanvasKit.Free(mPointsObj);
586        }
587
588        paint.delete();
589    });
590
591    gm('drawPoints in different modes', (canvas) => {
592        canvas.clear(CanvasKit.WHITE);
593        // From https://bugs.chromium.org/p/skia/issues/detail?id=11012
594        const boxPaint = new CanvasKit.Paint();
595        boxPaint.setStyle(CanvasKit.PaintStyle.Stroke);
596        boxPaint.setStrokeWidth(1);
597
598        const paint = new CanvasKit.Paint();
599        paint.setStyle(CanvasKit.PaintStyle.Stroke);
600        paint.setStrokeWidth(5);
601        paint.setStrokeCap(CanvasKit.StrokeCap.Round);
602        paint.setColorInt(0xFF0000FF); // Blue
603        paint.setAntiAlias(true);
604
605        const points = Float32Array.of(40, 40, 80, 40, 120, 80, 160, 80);
606
607        canvas.drawRect(CanvasKit.LTRBRect(35, 35, 165, 85), boxPaint);
608        canvas.drawPoints(CanvasKit.PointMode.Points, points, paint);
609
610        canvas.translate(0, 50);
611        canvas.drawRect(CanvasKit.LTRBRect(35, 35, 165, 85), boxPaint);
612        canvas.drawPoints(CanvasKit.PointMode.Lines, points, paint);
613
614        canvas.translate(0, 50);
615        canvas.drawRect(CanvasKit.LTRBRect(35, 35, 165, 85), boxPaint);
616        canvas.drawPoints(CanvasKit.PointMode.Polygon, points, paint);
617
618        // The control version using drawPath
619        canvas.translate(0, 50);
620        canvas.drawRect(CanvasKit.LTRBRect(35, 35, 165, 85), boxPaint);
621        const path = new CanvasKit.Path();
622        path.moveTo(40, 40);
623        path.lineTo(80, 40);
624        path.lineTo(120, 80);
625        path.lineTo(160, 80);
626        paint.setColorInt(0xFFFF0000); // RED
627        canvas.drawPath(path, paint);
628
629        paint.delete();
630        path.delete();
631        boxPaint.delete();
632    });
633
634    gm('drawImageNine_canvas', (canvas, fetchedByteBuffers) => {
635        const img = CanvasKit.MakeImageFromEncoded(fetchedByteBuffers[0]);
636        expect(img).toBeTruthy();
637
638        canvas.clear(CanvasKit.WHITE);
639        const paint = new CanvasKit.Paint();
640
641        canvas.drawImageNine(img, CanvasKit.LTRBiRect(40, 40, 400, 300),
642            CanvasKit.LTRBRect(5, 5, 300, 650), CanvasKit.FilterMode.Nearest, paint);
643        paint.delete();
644        img.delete();
645    }, '/assets/mandrill_512.png');
646
647        // This should be a nice, clear image.
648    gm('makeImageShaderCubic_canvas', (canvas, fetchedByteBuffers) => {
649        const img = CanvasKit.MakeImageFromEncoded(fetchedByteBuffers[0]);
650        expect(img).toBeTruthy();
651
652        canvas.clear(CanvasKit.WHITE);
653        const paint = new CanvasKit.Paint();
654        const shader = img.makeShaderCubic(CanvasKit.TileMode.Decal, CanvasKit.TileMode.Clamp,
655                                           1/3 /*B*/, 1/3 /*C*/,
656                                           CanvasKit.Matrix.rotated(0.1));
657        paint.setShader(shader);
658
659        canvas.drawPaint(paint);
660        paint.delete();
661        shader.delete();
662        img.delete();
663    }, '/assets/mandrill_512.png');
664
665    // This will look more blocky than the version above.
666    gm('makeImageShaderOptions_canvas', (canvas, fetchedByteBuffers) => {
667        const img = CanvasKit.MakeImageFromEncoded(fetchedByteBuffers[0]);
668        expect(img).toBeTruthy();
669        const imgWithMipMap = img.makeCopyWithDefaultMipmaps();
670
671        canvas.clear(CanvasKit.WHITE);
672        const paint = new CanvasKit.Paint();
673        const shader = imgWithMipMap.makeShaderOptions(CanvasKit.TileMode.Decal,
674                                                       CanvasKit.TileMode.Clamp,
675                                                       CanvasKit.FilterMode.Nearest,
676                                                       CanvasKit.MipmapMode.Linear,
677                                                       CanvasKit.Matrix.rotated(0.1));
678        paint.setShader(shader);
679
680        canvas.drawPaint(paint);
681        paint.delete();
682        shader.delete();
683        img.delete();
684        imgWithMipMap.delete();
685    }, '/assets/mandrill_512.png');
686
687    gm('drawvertices_canvas', (canvas) => {
688        const paint = new CanvasKit.Paint();
689        paint.setAntiAlias(true);
690
691        const points = [0, 0,  250, 0,  100, 100,  0, 250];
692        // 2d float color array
693        const colors = [CanvasKit.RED, CanvasKit.BLUE,
694                        CanvasKit.YELLOW, CanvasKit.CYAN];
695        const vertices = CanvasKit.MakeVertices(CanvasKit.VertexMode.TriangleFan,
696            points, null /*textureCoordinates*/, colors, false /*isVolatile*/);
697
698        const bounds = vertices.bounds();
699        expect(bounds).toEqual(CanvasKit.LTRBRect(0, 0, 250, 250));
700
701        canvas.drawVertices(vertices, CanvasKit.BlendMode.Src, paint);
702        vertices.delete();
703        paint.delete();
704    });
705
706    gm('drawvertices_canvas_flat_floats', (canvas) => {
707        const paint = new CanvasKit.Paint();
708        paint.setAntiAlias(true);
709
710        const points = [0, 0,  250, 0,  100, 100,  0, 250];
711        // 1d float color array
712        const colors = Float32Array.of(...CanvasKit.RED, ...CanvasKit.BLUE,
713                                       ...CanvasKit.YELLOW, ...CanvasKit.CYAN);
714        const vertices = CanvasKit.MakeVertices(CanvasKit.VertexMode.TriangleFan,
715            points, null /*textureCoordinates*/, colors, false /*isVolatile*/);
716
717        const bounds = vertices.bounds();
718        expect(bounds).toEqual(CanvasKit.LTRBRect(0, 0, 250, 250));
719
720        canvas.drawVertices(vertices, CanvasKit.BlendMode.Src, paint);
721        vertices.delete();
722        paint.delete();
723    });
724
725    gm('drawvertices_texture_canvas', (canvas, fetchedByteBuffers) => {
726        const img = CanvasKit.MakeImageFromEncoded(fetchedByteBuffers[0]);
727
728        const paint = new CanvasKit.Paint();
729        paint.setAntiAlias(true);
730
731        const points = [
732             70, 170,   40, 90,  130, 150,  100, 50,
733            225, 150,  225, 60,  310, 180,  330, 100,
734        ];
735        const textureCoordinates = [
736              0, 240,    0, 0,   80, 240,   80, 0,
737            160, 240,  160, 0,  240, 240,  240, 0,
738        ];
739        const vertices = CanvasKit.MakeVertices(CanvasKit.VertexMode.TrianglesStrip,
740            points, textureCoordinates, null /* colors */, false /*isVolatile*/);
741
742        const shader = img.makeShaderCubic(CanvasKit.TileMode.Repeat, CanvasKit.TileMode.Mirror,
743            1/3 /*B*/, 1/3 /*C*/,);
744        paint.setShader(shader);
745        canvas.drawVertices(vertices, CanvasKit.BlendMode.Src, paint);
746
747        shader.delete();
748        vertices.delete();
749        paint.delete();
750        img.delete();
751    }, '/assets/brickwork-texture.jpg');
752
753    it('can change the 3x3 matrix on the canvas and read it back', () => {
754        const canvas = new CanvasKit.Canvas();
755
756        let matr = canvas.getTotalMatrix();
757        expect(matr).toEqual(CanvasKit.Matrix.identity());
758
759        // This fills the internal _scratch4x4MatrixPtr with garbage (aka sentinel) values to
760        // make sure the 3x3 matrix properly sets these to 0 when it uses the same buffer.
761        canvas.save();
762        const garbageMatrix = new Float32Array(16);
763        garbageMatrix.fill(-3);
764        canvas.concat(garbageMatrix);
765        canvas.restore();
766
767        canvas.concat(CanvasKit.Matrix.rotated(Math.PI/4));
768        const d = new DOMMatrix().translate(20, 10);
769        canvas.concat(d);
770
771        matr = canvas.getTotalMatrix();
772        const expected = CanvasKit.Matrix.multiply(
773            CanvasKit.Matrix.rotated(Math.PI/4),
774            CanvasKit.Matrix.translated(20, 10)
775        );
776        expect3x3MatricesToMatch(expected, matr);
777
778        // The 3x3 should be expanded into a 4x4, with 0s in the 3rd row and column.
779        matr = canvas.getLocalToDevice();
780        expect4x4MatricesToMatch([
781            0.707106, -0.707106, 0,  7.071067,
782            0.707106,  0.707106, 0, 21.213203,
783            0       ,  0       , 0,  0       ,
784            0       ,  0       , 0,  1       ], matr);
785    });
786
787    it('can accept a 3x2 matrix', () => {
788        const canvas = new CanvasKit.Canvas();
789
790        let matr = canvas.getTotalMatrix();
791        expect(matr).toEqual(CanvasKit.Matrix.identity());
792
793        // This fills the internal _scratch4x4MatrixPtr with garbage (aka sentinel) values to
794        // make sure the 3x2 matrix properly sets these to 0 when it uses the same buffer.
795        canvas.save();
796        const garbageMatrix = new Float32Array(16);
797        garbageMatrix.fill(-3);
798        canvas.concat(garbageMatrix);
799        canvas.restore();
800
801        canvas.concat([1.4, -0.2, 12,
802                       0.2,  1.4, 24]);
803
804        matr = canvas.getTotalMatrix();
805        const expected = [1.4, -0.2, 12,
806                          0.2,  1.4, 24,
807                            0,    0,  1];
808        expect3x3MatricesToMatch(expected, matr);
809
810        // The 3x2 should be expanded into a 4x4, with 0s in the 3rd row and column
811        // and the perspective filled in.
812        matr = canvas.getLocalToDevice();
813        expect4x4MatricesToMatch([
814            1.4, -0.2, 0, 12,
815            0.2,  1.4, 0, 24,
816            0  ,  0  , 0,  0,
817            0  ,  0  , 0,  1], matr);
818    });
819
820    it('can mark a CTM and retrieve it', () => {
821        const canvas = new CanvasKit.Canvas();
822
823        canvas.concat(CanvasKit.M44.rotated([0, 1, 0], Math.PI/4));
824        canvas.concat(CanvasKit.M44.rotated([1, 0, 1], Math.PI/8));
825        canvas.markCTM('krispykreme');
826
827        const expected = CanvasKit.M44.multiply(
828          CanvasKit.M44.rotated([0, 1, 0], Math.PI/4),
829          CanvasKit.M44.rotated([1, 0, 1], Math.PI/8),
830        );
831
832        expect4x4MatricesToMatch(expected, canvas.findMarkedCTM('krispykreme'));
833    });
834
835    it('returns null for an invalid CTM marker', () => {
836        const canvas = new CanvasKit.Canvas();
837        expect(canvas.findMarkedCTM('dunkindonuts')).toBeNull();
838    });
839
840    it('can change the 4x4 matrix on the canvas and read it back', () => {
841        const canvas = new CanvasKit.Canvas();
842
843        let matr = canvas.getLocalToDevice();
844        expect(matr).toEqual(CanvasKit.M44.identity());
845
846        canvas.concat(CanvasKit.M44.rotated([0, 1, 0], Math.PI/4));
847        canvas.concat(CanvasKit.M44.rotated([1, 0, 1], Math.PI/8));
848
849        const expected = CanvasKit.M44.multiply(
850          CanvasKit.M44.rotated([0, 1, 0], Math.PI/4),
851          CanvasKit.M44.rotated([1, 0, 1], Math.PI/8),
852        );
853
854        expect4x4MatricesToMatch(expected, canvas.getLocalToDevice());
855        // TODO(kjlubick) add test for DOMMatrix
856        // TODO(nifong) add more involved test for camera-related math.
857    });
858
859    gm('concat_with4x4_canvas', (canvas) => {
860        const path = starPath(CanvasKit, CANVAS_WIDTH/2, CANVAS_HEIGHT/2);
861        const paint = new CanvasKit.Paint();
862        paint.setAntiAlias(true);
863        canvas.clear(CanvasKit.WHITE);
864
865        // Rotate it a bit on all 3 major axis, centered on the screen.
866        // To play with rotations, see https://jsfiddle.skia.org/canvaskit/0525300405796aa87c3b84cc0d5748516fca0045d7d6d9c7840710ab771edcd4
867        const turn = CanvasKit.M44.multiply(
868          CanvasKit.M44.translated([CANVAS_WIDTH/2, 0, 0]),
869          CanvasKit.M44.rotated([1, 0, 0], Math.PI/3),
870          CanvasKit.M44.rotated([0, 1, 0], Math.PI/4),
871          CanvasKit.M44.rotated([0, 0, 1], Math.PI/16),
872          CanvasKit.M44.translated([-CANVAS_WIDTH/2, 0, 0]),
873        );
874        canvas.concat(turn);
875
876        // Draw some stripes to help the eye detect the turn
877        const stripeWidth = 10;
878        paint.setColor(CanvasKit.BLACK);
879        for (let i = 0; i < CANVAS_WIDTH; i += 2*stripeWidth) {
880            canvas.drawRect(CanvasKit.LTRBRect(i, 0, i + stripeWidth, CANVAS_HEIGHT), paint);
881        }
882
883        paint.setColor(CanvasKit.YELLOW);
884        canvas.drawPath(path, paint);
885        paint.delete();
886        path.delete();
887    });
888
889    gm('particles_canvas', (canvas) => {
890        const curveParticles = {
891            'MaxCount': 1000,
892            'Drawable': {
893               'Type': 'SkCircleDrawable',
894               'Radius': 2
895            },
896            'Code': [
897               `void effectSpawn(inout Effect effect) {
898                  effect.rate = 200;
899                  effect.color = float4(1, 0, 0, 1);
900                }
901                void spawn(inout Particle p) {
902                  p.lifetime = 3 + rand(p.seed);
903                  p.vel.y = -50;
904                }
905
906                void update(inout Particle p) {
907                  float w = mix(15, 3, p.age);
908                  p.pos.x = sin(radians(p.age * 320)) * mix(25, 10, p.age) + mix(-w, w, rand(p.seed));
909                  if (rand(p.seed) < 0.5) { p.pos.x = -p.pos.x; }
910
911                  p.color.g = (mix(75, 220, p.age) + mix(-30, 30, rand(p.seed))) / 255;
912                }`
913            ],
914            'Bindings': []
915        };
916
917        const particles = CanvasKit.MakeParticles(JSON.stringify(curveParticles));
918        particles.start(0, true);
919        particles.setPosition([0, 0]);
920
921        const paint = new CanvasKit.Paint();
922        paint.setAntiAlias(true);
923        paint.setColor(CanvasKit.WHITE);
924        const font = new CanvasKit.Font(null, 12);
925
926        canvas.clear(CanvasKit.BLACK);
927
928        // Draw a 5x5 set of different times in the particle system
929        // like a filmstrip of motion of particles.
930        const LEFT_MARGIN = 90;
931        const TOP_MARGIN = 100;
932        for (let row = 0; row < 5; row++) {
933            for (let column = 0; column < 5; column++) {
934                canvas.save();
935                canvas.translate(LEFT_MARGIN + column*100, TOP_MARGIN + row*100);
936
937                // Time moves in row-major order in increments of 0.02.
938                const particleTime = row/10 + column/50;
939
940                canvas.drawText('time ' + particleTime.toFixed(2), -30, 20, paint, font);
941                particles.update(particleTime);
942
943                particles.draw(canvas);
944                canvas.restore();
945            }
946        }
947    });
948});
949
950const expect3x3MatricesToMatch = (expected, actual) => {
951    expect(expected.length).toEqual(9);
952    expect(actual.length).toEqual(9);
953    for (let i = 0; i < expected.length; i++) {
954        expect(expected[i]).toBeCloseTo(actual[i], 5);
955    }
956};
957
958const expect4x4MatricesToMatch = (expected, actual) => {
959    expect(expected.length).toEqual(16);
960    expect(actual.length).toEqual(16);
961    for (let i = 0; i < expected.length; i++) {
962        expect(expected[i]).toBeCloseTo(actual[i], 5);
963    }
964};
965