• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5part of engine;
6
7/// A raw HTML canvas that is directly written to.
8class BitmapCanvas extends EngineCanvas with SaveStackTracking {
9  /// The rectangle positioned relative to the parent layer's coordinate
10  /// system's origin, within which this canvas paints.
11  ///
12  /// Painting outside these bounds will result in cropping.
13  ui.Rect get bounds => _bounds;
14  set bounds(ui.Rect newValue) {
15    assert(newValue != null);
16    _bounds = newValue;
17  }
18
19  ui.Rect _bounds;
20
21  /// The amount of padding to add around the edges of this canvas to
22  /// ensure that anti-aliased arcs are not clipped.
23  static const int paddingPixels = 1;
24
25  @override
26  final html.Element rootElement = html.Element.tag('flt-canvas');
27
28  html.CanvasElement _canvas;
29  html.CanvasRenderingContext2D _ctx;
30
31  /// The size of the paint [bounds].
32  ui.Size get size => _bounds.size;
33
34  /// The last paragraph style is cached to optimize the case where the style
35  /// hasn't changed.
36  ParagraphGeometricStyle _cachedLastStyle;
37
38  /// List of extra sibling elements created for paragraphs and clipping.
39  final List<html.Element> _children = <html.Element>[];
40
41  /// The number of pixels along the width of the bitmap that the canvas element
42  /// renders into.
43  ///
44  /// These pixels are different from the logical CSS pixels. Here a pixel
45  /// literally means 1 point with a RGBA color.
46  int get widthInBitmapPixels => _widthInBitmapPixels;
47  int _widthInBitmapPixels;
48
49  /// The number of pixels along the width of the bitmap that the canvas element
50  /// renders into.
51  ///
52  /// These pixels are different from the logical CSS pixels. Here a pixel
53  /// literally means 1 point with a RGBA color.
54  int get heightInBitmapPixels => _heightInBitmapPixels;
55  int _heightInBitmapPixels;
56
57  /// The number of pixels in the bitmap that the canvas element renders into.
58  ///
59  /// These pixels are different from the logical CSS pixels. Here a pixel
60  /// literally means 1 point with a RGBA color.
61  int get bitmapPixelCount => widthInBitmapPixels * heightInBitmapPixels;
62
63  int _saveCount = 0;
64
65  /// Keeps track of what device pixel ratio was used when this [BitmapCanvas]
66  /// was created.
67  final double _devicePixelRatio = html.window.devicePixelRatio;
68
69  // Cached current filter, fill and stroke style to reduce updates to
70  // CanvasRenderingContext2D that are slow even when resetting to null.
71  String _prevFilter = 'none';
72  Object _prevFillStyle;
73  Object _prevStrokeStyle;
74
75  /// Allocates a canvas with enough memory to paint a picture within the given
76  /// [bounds].
77  ///
78  /// This canvas can be reused by pictures with different paint bounds as long
79  /// as the [Rect.size] of the bounds fully fit within the size used to
80  /// initialize this canvas.
81  BitmapCanvas(this._bounds) : assert(_bounds != null) {
82    rootElement.style.position = 'absolute';
83
84    // Adds one extra pixel to the requested size. This is to compensate for
85    // _initializeViewport() snapping canvas position to 1 pixel, causing
86    // painting to overflow by at most 1 pixel.
87    final double boundsWidth = size.width + 1 + 2 * paddingPixels;
88    final double boundsHeight = size.height + 1 + 2 * paddingPixels;
89    _widthInBitmapPixels = (boundsWidth * html.window.devicePixelRatio).ceil();
90    _heightInBitmapPixels =
91        (boundsHeight * html.window.devicePixelRatio).ceil();
92
93    // Compute the final CSS canvas size given the actual pixel count we
94    // allocated. This is done for the following reasons:
95    //
96    // * To satisfy the invariant: pixel size = css size * device pixel ratio.
97    // * To make sure that when we scale the canvas by devicePixelRatio (see
98    //   _initializeViewport below) the pixels line up.
99    final double cssWidth = _widthInBitmapPixels / html.window.devicePixelRatio;
100    final double cssHeight =
101        _heightInBitmapPixels / html.window.devicePixelRatio;
102
103    _canvas = html.CanvasElement(
104      width: _widthInBitmapPixels,
105      height: _heightInBitmapPixels,
106    );
107    _canvas.style
108      ..position = 'absolute'
109      ..width = '${cssWidth}px'
110      ..height = '${cssHeight}px';
111    _ctx = _canvas.context2D;
112    rootElement.append(_canvas);
113    _initializeViewport();
114  }
115
116  @override
117  void dispose() {
118    super.dispose();
119    // Webkit has a threshold for the amount of canvas pixels an app can
120    // allocate. Even though our canvases are being garbage-collected as
121    // expected when we don't need them, Webkit keeps track of their sizes
122    // towards the threshold. Setting width and height to zero tricks Webkit
123    // into thinking that this canvas has a zero size so it doesn't count it
124    // towards the threshold.
125    if (browserEngine == BrowserEngine.webkit) {
126      _canvas.width = _canvas.height = 0;
127    }
128  }
129
130  /// Prepare to reuse this canvas by clearing it's current contents.
131  @override
132  void clear() {
133    super.clear();
134    final int len = _children.length;
135    for (int i = 0; i < len; i++) {
136      _children[i].remove();
137    }
138    _children.clear();
139    _cachedLastStyle = null;
140    // Restore to the state where we have only applied the scaling.
141    if (_ctx != null) {
142      _ctx.restore();
143      _ctx.clearRect(0, 0, _widthInBitmapPixels, _heightInBitmapPixels);
144      _ctx.font = '';
145      _initializeViewport();
146    }
147    if (_canvas != null) {
148      _canvas.style.transformOrigin = '';
149      _canvas.style.transform = '';
150    }
151  }
152
153  /// Checks whether this [BitmapCanvas] can still be recycled and reused.
154  ///
155  /// See also:
156  ///
157  /// * [PersistedStandardPicture._applyBitmapPaint] which uses this method to
158  ///   decide whether to reuse this canvas or not.
159  /// * [PersistedStandardPicture._recycleCanvas] which also uses this method
160  ///   for the same reason.
161  bool isReusable() {
162    return _devicePixelRatio == html.window.devicePixelRatio;
163  }
164
165  /// Configures the canvas such that its coordinate system follows the scene's
166  /// coordinate system, and the pixel ratio is applied such that CSS pixels are
167  /// translated to bitmap pixels.
168  void _initializeViewport() {
169    // Save the canvas state with top-level transforms so we can undo
170    // any clips later when we reuse the canvas.
171    _ctx.save();
172
173    // We always start with identity transform because the surrounding transform
174    // is applied on the DOM elements.
175    _ctx.setTransform(1, 0, 0, 1, 0, 0);
176
177    // This scale makes sure that 1 CSS pixel is translated to the correct
178    // number of bitmap pixels.
179    _ctx.scale(html.window.devicePixelRatio, html.window.devicePixelRatio);
180
181    // Flutter emits paint operations positioned relative to the parent layer's
182    // coordinate system. However, canvas' coordinate system's origin is always
183    // in the top-left corner of the canvas. We therefore need to inject an
184    // initial translation so the paint operations are positioned as expected.
185
186    // The flooring of the value is to ensure that canvas' top-left corner
187    // lands on the physical pixel.
188    final int canvasPositionX = _bounds.left.floor() - paddingPixels;
189    final int canvasPositionY = _bounds.top.floor() - paddingPixels;
190    final double canvasPositionCorrectionX =
191        _bounds.left - paddingPixels - canvasPositionX.toDouble();
192    final double canvasPositionCorrectionY =
193        _bounds.top - paddingPixels - canvasPositionY.toDouble();
194
195    rootElement.style.transform =
196        'translate(${canvasPositionX}px, ${canvasPositionY}px)';
197
198    // This compensates for the translate on the `rootElement`.
199    translate(
200      -_bounds.left + canvasPositionCorrectionX + paddingPixels,
201      -_bounds.top + canvasPositionCorrectionY + paddingPixels,
202    );
203  }
204
205  /// The `<canvas>` element used by this bitmap canvas.
206  html.CanvasElement get canvas => _canvas;
207
208  /// The 2D context of the `<canvas>` element used by this bitmap canvas.
209  html.CanvasRenderingContext2D get ctx => _ctx;
210
211  /// Sets the global paint styles to correspond to [paint].
212  void _applyPaint(ui.PaintData paint) {
213    ctx.globalCompositeOperation =
214        _stringForBlendMode(paint.blendMode) ?? 'source-over';
215    ctx.lineWidth = paint.strokeWidth ?? 1.0;
216    final ui.StrokeCap cap = paint.strokeCap;
217    if (cap != null) {
218      ctx.lineCap = _stringForStrokeCap(cap);
219    } else {
220      ctx.lineCap = 'butt';
221    }
222    final ui.StrokeJoin join = paint.strokeJoin;
223    if (join != null) {
224      ctx.lineJoin = _stringForStrokeJoin(join);
225    } else {
226      ctx.lineJoin = 'miter';
227    }
228    if (paint.shader != null) {
229      final Object paintStyle = paint.shader.createPaintStyle(ctx);
230      _setFillAndStrokeStyle(paintStyle, paintStyle);
231    } else if (paint.color != null) {
232      final String colorString = paint.color.toCssString();
233      _setFillAndStrokeStyle(colorString, colorString);
234    }
235    if (paint.maskFilter != null) {
236      _setFilter('blur(${paint.maskFilter.webOnlySigma}px)');
237    }
238  }
239
240  void _strokeOrFill(ui.PaintData paint, {bool resetPaint = true}) {
241    switch (paint.style) {
242      case ui.PaintingStyle.stroke:
243        ctx.stroke();
244        break;
245      case ui.PaintingStyle.fill:
246      default:
247        ctx.fill();
248        break;
249    }
250    if (resetPaint) {
251      _resetPaint();
252    }
253  }
254
255  /// Resets the paint styles that were set due to a previous paint command.
256  ///
257  /// For example, if a previous paint commands has a blur filter, we need to
258  /// undo that filter here.
259  ///
260  /// This needs to be called after [_applyPaint].
261  void _resetPaint() {
262    _setFilter('none');
263    _setFillAndStrokeStyle(null, null);
264  }
265
266  void _setFilter(String value) {
267    if (_prevFilter != value) {
268      _prevFilter = ctx.filter = value;
269    }
270  }
271
272  void _setFillAndStrokeStyle(Object fillStyle, Object strokeStyle) {
273    final html.CanvasRenderingContext2D _ctx = ctx;
274    if (!identical(_prevFillStyle, fillStyle)) {
275      _prevFillStyle = _ctx.fillStyle = fillStyle;
276    }
277    if (!identical(_prevStrokeStyle, strokeStyle)) {
278      _prevStrokeStyle = _ctx.strokeStyle = strokeStyle;
279    }
280  }
281
282  @override
283  int save() {
284    super.save();
285    ctx.save();
286    return _saveCount++;
287  }
288
289  void saveLayer(ui.Rect bounds, ui.Paint paint) {
290    save();
291  }
292
293  @override
294  void restore() {
295    super.restore();
296    ctx.restore();
297    _saveCount--;
298    _cachedLastStyle = null;
299  }
300
301  // TODO(yjbanov): not sure what this is attempting to do, but it is probably
302  //                wrong because some clips and transforms are expressed using
303  //                HTML DOM elements.
304  void restoreToCount(int count) {
305    assert(_saveCount >= count);
306    final int restores = _saveCount - count;
307    for (int i = 0; i < restores; i++) {
308      ctx.restore();
309    }
310    _saveCount = count;
311  }
312
313  @override
314  void translate(double dx, double dy) {
315    super.translate(dx, dy);
316    ctx.translate(dx, dy);
317  }
318
319  @override
320  void scale(double sx, double sy) {
321    super.scale(sx, sy);
322    ctx.scale(sx, sy);
323  }
324
325  @override
326  void rotate(double radians) {
327    super.rotate(radians);
328    ctx.rotate(radians);
329  }
330
331  @override
332  void skew(double sx, double sy) {
333    super.skew(sx, sy);
334    ctx.transform(1, sy, sx, 1, 0, 0);
335    //            |  |   |   |  |  |
336    //            |  |   |   |  |  f - vertical translation
337    //            |  |   |   |  e - horizontal translation
338    //            |  |   |   d - vertical scaling
339    //            |  |   c - horizontal skewing
340    //            |  b - vertical skewing
341    //            a - horizontal scaling
342    //
343    // Source: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/transform
344  }
345
346  @override
347  void transform(Float64List matrix4) {
348    super.transform(matrix4);
349
350    // Canvas2D transform API:
351    //
352    // ctx.transform(a, b, c, d, e, f);
353    //
354    // In 3x3 matrix form assuming vector representation of (x, y, 1):
355    //
356    // a c e
357    // b d f
358    // 0 0 1
359    //
360    // This translates to 4x4 matrix with vector representation of (x, y, z, 1)
361    // as:
362    //
363    // a c 0 e
364    // b d 0 f
365    // 0 0 1 0
366    // 0 0 0 1
367    //
368    // This matrix is sufficient to represent 2D rotates, translates, scales,
369    // and skews.
370    assert(() {
371      if (matrix4[2] != 0.0 ||
372          matrix4[3] != 0.0 ||
373          matrix4[7] != 0.0 ||
374          matrix4[8] != 0.0 ||
375          matrix4[9] != 0.0 ||
376          matrix4[10] != 1.0 ||
377          matrix4[11] != 0.0 ||
378          matrix4[14] != 0.0 ||
379          matrix4[15] != 1.0) {
380        print('WARNING: 3D transformation matrix was passed to BitmapCanvas.');
381      }
382      return true;
383    }());
384    _ctx.transform(
385      matrix4[0],
386      matrix4[1],
387      matrix4[4],
388      matrix4[5],
389      matrix4[12],
390      matrix4[13],
391    );
392  }
393
394  @override
395  void clipRect(ui.Rect rect) {
396    super.clipRect(rect);
397    ctx.beginPath();
398    ctx.rect(rect.left, rect.top, rect.width, rect.height);
399    ctx.clip();
400  }
401
402  @override
403  void clipRRect(ui.RRect rrect) {
404    super.clipRRect(rrect);
405    final ui.Path path = ui.Path()..addRRect(rrect);
406    _runPath(path);
407    ctx.clip();
408  }
409
410  @override
411  void clipPath(ui.Path path) {
412    super.clipPath(path);
413    _runPath(path);
414    ctx.clip();
415  }
416
417  @override
418  void drawColor(ui.Color color, ui.BlendMode blendMode) {
419    ctx.globalCompositeOperation = _stringForBlendMode(blendMode);
420
421    // Fill a virtually infinite rect with the color.
422    //
423    // We can't use (0, 0, width, height) because the current transform can
424    // cause it to not fill the entire clip.
425    ctx.fillRect(-10000, -10000, 20000, 20000);
426  }
427
428  @override
429  void drawLine(ui.Offset p1, ui.Offset p2, ui.PaintData paint) {
430    _applyPaint(paint);
431    ctx.beginPath();
432    ctx.moveTo(p1.dx, p1.dy);
433    ctx.lineTo(p2.dx, p2.dy);
434    ctx.stroke();
435    _resetPaint();
436  }
437
438  @override
439  void drawPaint(ui.PaintData paint) {
440    _applyPaint(paint);
441    ctx.beginPath();
442
443    // Fill a virtually infinite rect with the color.
444    //
445    // We can't use (0, 0, width, height) because the current transform can
446    // cause it to not fill the entire clip.
447    ctx.fillRect(-10000, -10000, 20000, 20000);
448    _resetPaint();
449  }
450
451  @override
452  void drawRect(ui.Rect rect, ui.PaintData paint) {
453    _applyPaint(paint);
454    ctx.beginPath();
455    ctx.rect(rect.left, rect.top, rect.width, rect.height);
456    _strokeOrFill(paint);
457  }
458
459  @override
460  void drawRRect(ui.RRect rrect, ui.PaintData paint) {
461    _applyPaint(paint);
462    _drawRRectPath(rrect);
463    _strokeOrFill(paint);
464  }
465
466  void _drawRRectPath(ui.RRect rrect, {bool startNewPath = true}) {
467    // TODO(mdebbar): there's a bug in this code, it doesn't correctly handle
468    //                the case when the radius is greater than the width of the
469    //                rect. When we fix that in houdini_painter.js, we need to
470    //                fix it here too.
471    // To draw the rounded rectangle, perform the following 8 steps:
472    //   1. Flip left,right top,bottom since web doesn't support flipped
473    //      coordinates with negative radii.
474    //   2. draw the line for the top
475    //   3. draw the arc for the top-right corner
476    //   4. draw the line for the right side
477    //   5. draw the arc for the bottom-right corner
478    //   6. draw the line for the bottom of the rectangle
479    //   7. draw the arc for the bottom-left corner
480    //   8. draw the line for the left side
481    //   9. draw the arc for the top-left corner
482    //
483    // After drawing, the current point will be the left side of the top of the
484    // rounded rectangle (after the corner).
485    // TODO(het): Confirm that this is the end point in Flutter for RRect
486
487    double left = rrect.left;
488    double right = rrect.right;
489    double top = rrect.top;
490    double bottom = rrect.bottom;
491    if (left > right) {
492      left = right;
493      right = rrect.left;
494    }
495    if (top > bottom) {
496      top = bottom;
497      bottom = rrect.top;
498    }
499    final double trRadiusX = rrect.trRadiusX.abs();
500    final double tlRadiusX = rrect.tlRadiusX.abs();
501    final double trRadiusY = rrect.trRadiusY.abs();
502    final double tlRadiusY = rrect.tlRadiusY.abs();
503    final double blRadiusX = rrect.blRadiusX.abs();
504    final double brRadiusX = rrect.brRadiusX.abs();
505    final double blRadiusY = rrect.blRadiusY.abs();
506    final double brRadiusY = rrect.brRadiusY.abs();
507
508    ctx.moveTo(left + trRadiusX, top);
509
510    if (startNewPath) {
511      ctx.beginPath();
512    }
513
514    // Top side and top-right corner
515    ctx.lineTo(right - trRadiusX, top);
516    ctx.ellipse(
517      right - trRadiusX,
518      top + trRadiusY,
519      trRadiusX,
520      trRadiusY,
521      0,
522      1.5 * math.pi,
523      2.0 * math.pi,
524      false,
525    );
526
527    // Right side and bottom-right corner
528    ctx.lineTo(right, bottom - brRadiusY);
529    ctx.ellipse(
530      right - brRadiusX,
531      bottom - brRadiusY,
532      brRadiusX,
533      brRadiusY,
534      0,
535      0,
536      0.5 * math.pi,
537      false,
538    );
539
540    // Bottom side and bottom-left corner
541    ctx.lineTo(left + blRadiusX, bottom);
542    ctx.ellipse(
543      left + blRadiusX,
544      bottom - blRadiusY,
545      blRadiusX,
546      blRadiusY,
547      0,
548      0.5 * math.pi,
549      math.pi,
550      false,
551    );
552
553    // Left side and top-left corner
554    ctx.lineTo(left, top + tlRadiusY);
555    ctx.ellipse(
556      left + tlRadiusX,
557      top + tlRadiusY,
558      tlRadiusX,
559      tlRadiusY,
560      0,
561      math.pi,
562      1.5 * math.pi,
563      false,
564    );
565  }
566
567  void _drawRRectPathReverse(ui.RRect rrect, {bool startNewPath = true}) {
568    double left = rrect.left;
569    double right = rrect.right;
570    double top = rrect.top;
571    double bottom = rrect.bottom;
572    final double trRadiusX = rrect.trRadiusX.abs();
573    final double tlRadiusX = rrect.tlRadiusX.abs();
574    final double trRadiusY = rrect.trRadiusY.abs();
575    final double tlRadiusY = rrect.tlRadiusY.abs();
576    final double blRadiusX = rrect.blRadiusX.abs();
577    final double brRadiusX = rrect.brRadiusX.abs();
578    final double blRadiusY = rrect.blRadiusY.abs();
579    final double brRadiusY = rrect.brRadiusY.abs();
580
581    if (left > right) {
582      left = right;
583      right = rrect.left;
584    }
585    if (top > bottom) {
586      top = bottom;
587      bottom = rrect.top;
588    }
589    // Draw the rounded rectangle, counterclockwise.
590    ctx.moveTo(right - trRadiusX, top);
591
592    if (startNewPath) {
593      ctx.beginPath();
594    }
595
596    // Top side and top-left corner
597    ctx.lineTo(left + tlRadiusX, top);
598    ctx.ellipse(
599      left + tlRadiusX,
600      top + tlRadiusY,
601      tlRadiusX,
602      tlRadiusY,
603      0,
604      1.5 * math.pi,
605      1 * math.pi,
606      true,
607    );
608
609    // Left side and bottom-left corner
610    ctx.lineTo(left, bottom - blRadiusY);
611    ctx.ellipse(
612      left + blRadiusX,
613      bottom - blRadiusY,
614      blRadiusX,
615      blRadiusY,
616      0,
617      1 * math.pi,
618      0.5 * math.pi,
619      true,
620    );
621
622    // Bottom side and bottom-right corner
623    ctx.lineTo(right - brRadiusX, bottom);
624    ctx.ellipse(
625      right - brRadiusX,
626      bottom - brRadiusY,
627      brRadiusX,
628      brRadiusY,
629      0,
630      0.5 * math.pi,
631      0 * math.pi,
632      true,
633    );
634
635    // Right side and top-right corner
636    ctx.lineTo(right, top + trRadiusY);
637    ctx.ellipse(
638      right - trRadiusX,
639      top + trRadiusY,
640      trRadiusX,
641      trRadiusY,
642      0,
643      0 * math.pi,
644      1.5 * math.pi,
645      true,
646    );
647  }
648
649  @override
650  void drawDRRect(ui.RRect outer, ui.RRect inner, ui.PaintData paint) {
651    _applyPaint(paint);
652    _drawRRectPath(outer);
653    _drawRRectPathReverse(inner, startNewPath: false);
654    _strokeOrFill(paint);
655  }
656
657  @override
658  void drawOval(ui.Rect rect, ui.PaintData paint) {
659    _applyPaint(paint);
660    ctx.beginPath();
661    ctx.ellipse(rect.center.dx, rect.center.dy, rect.width / 2, rect.height / 2,
662        0, 0, 2.0 * math.pi, false);
663    _strokeOrFill(paint);
664  }
665
666  @override
667  void drawCircle(ui.Offset c, double radius, ui.PaintData paint) {
668    _applyPaint(paint);
669    ctx.beginPath();
670    ctx.ellipse(c.dx, c.dy, radius, radius, 0, 0, 2.0 * math.pi, false);
671    _strokeOrFill(paint);
672  }
673
674  @override
675  void drawPath(ui.Path path, ui.PaintData paint) {
676    _applyPaint(paint);
677    _runPath(path);
678    _strokeOrFill(paint);
679  }
680
681  @override
682  void drawShadow(ui.Path path, ui.Color color, double elevation,
683      bool transparentOccluder) {
684    final List<CanvasShadow> shadows =
685        ElevationShadow.computeCanvasShadows(elevation, color);
686    if (shadows.isNotEmpty) {
687      for (final CanvasShadow shadow in shadows) {
688        // TODO(het): Shadows with transparent occluders are not supported
689        // on webkit since filter is unsupported.
690        if (transparentOccluder && browserEngine != BrowserEngine.webkit) {
691          // We paint shadows using a path and a mask filter instead of the
692          // built-in shadow* properties. This is because the color alpha of the
693          // paint is added to the shadow. The effect we're looking for is to just
694          // paint the shadow without the path itself, but if we use a non-zero
695          // alpha for the paint the path is painted in addition to the shadow,
696          // which is undesirable.
697          final ui.Paint paint = ui.Paint()
698            ..color = shadow.color
699            ..style = ui.PaintingStyle.fill
700            ..strokeWidth = 0.0
701            ..maskFilter = ui.MaskFilter.blur(ui.BlurStyle.normal, shadow.blur);
702          _ctx.save();
703          _ctx.translate(shadow.offsetX, shadow.offsetY);
704          final ui.PaintData paintData = paint.webOnlyPaintData;
705          _applyPaint(paintData);
706          _runPath(path);
707          _strokeOrFill(paintData, resetPaint: false);
708          _ctx.restore();
709        } else {
710          // TODO(het): We fill the path with this paint, then later we clip
711          // by the same path and fill it with a fully opaque color (we know
712          // the color is fully opaque because `transparentOccluder` is false.
713          // However, due to anti-aliasing of the clip, a few pixels of the
714          // path we are about to paint may still be visible after we fill with
715          // the opaque occluder. For that reason, we fill with the shadow color,
716          // and set the shadow color to fully opaque. This way, the visible
717          // pixels are less opaque and less noticeable.
718          final ui.Paint paint = ui.Paint()
719            ..color = shadow.color
720            ..style = ui.PaintingStyle.fill
721            ..strokeWidth = 0.0;
722          _ctx.save();
723          final ui.PaintData paintData = paint.webOnlyPaintData;
724          _applyPaint(paintData);
725          _ctx.shadowBlur = shadow.blur;
726          _ctx.shadowColor = shadow.color.withAlpha(0xff).toCssString();
727          _ctx.shadowOffsetX = shadow.offsetX;
728          _ctx.shadowOffsetY = shadow.offsetY;
729          _runPath(path);
730          _strokeOrFill(paintData, resetPaint: false);
731          _ctx.restore();
732        }
733      }
734      _resetPaint();
735    }
736  }
737
738  @override
739  void drawImage(ui.Image image, ui.Offset p, ui.PaintData paint) {
740    _applyPaint(paint);
741    final HtmlImage htmlImage = image;
742    final html.Element imgElement = htmlImage.imgElement.clone(true);
743    imgElement.style
744      ..position = 'absolute'
745      ..transform = 'translate(${p.dx}px, ${p.dy}px)';
746    rootElement.append(imgElement);
747  }
748
749  @override
750  void drawImageRect(
751      ui.Image image, ui.Rect src, ui.Rect dst, ui.PaintData paint) {
752    // TODO(het): Check if the src rect is the entire image, and if so just
753    // append the imgElement and set it's height and width.
754    final HtmlImage htmlImage = image;
755    ctx.drawImageScaledFromSource(
756      htmlImage.imgElement,
757      src.left,
758      src.top,
759      src.width,
760      src.height,
761      dst.left,
762      dst.top,
763      dst.width,
764      dst.height,
765    );
766  }
767
768  void _drawTextLine(
769      ParagraphGeometricStyle style, String line, double x, double y) {
770    final double letterSpacing = style.letterSpacing;
771    if (letterSpacing == null || letterSpacing == 0.0) {
772      ctx.fillText(line, x, y);
773    } else {
774      // When letter-spacing is set, we go through a more expensive code path
775      // that renders each character separately with the correct spacing
776      // between them.
777      //
778      // We are drawing letter spacing like the web does it, by adding the
779      // spacing after each letter. This is different from Flutter which puts
780      // the spacing around each letter i.e. for a 10px letter spacing, Flutter
781      // would put 5px before each letter and 5px after it, but on the web, we
782      // put no spacing before the letter and 10px after it. This is how the DOM
783      // does it.
784      final int len = line.length;
785      for (int i = 0; i < len; i++) {
786        final String char = line[i];
787        ctx.fillText(char, x, y);
788        x += letterSpacing + ctx.measureText(char).width;
789      }
790    }
791  }
792
793  @override
794  void drawParagraph(EngineParagraph paragraph, ui.Offset offset) {
795    assert(paragraph._isLaidOut);
796
797    final ParagraphGeometricStyle style = paragraph._geometricStyle;
798
799    if (paragraph._drawOnCanvas) {
800      final List<String> lines =
801          paragraph._lines ?? <String>[paragraph._plainText];
802
803      final ui.PaintData backgroundPaint =
804          paragraph._background?.webOnlyPaintData;
805      if (backgroundPaint != null) {
806        final ui.Rect rect = ui.Rect.fromLTWH(
807            offset.dx, offset.dy, paragraph.width, paragraph.height);
808        drawRect(rect, backgroundPaint);
809      }
810
811      if (style != _cachedLastStyle) {
812        ctx.font = style.cssFontString;
813        _cachedLastStyle = style;
814      }
815      _applyPaint(paragraph._paint.webOnlyPaintData);
816
817      final double x = offset.dx + paragraph._alignOffset;
818      double y = offset.dy + paragraph.alphabeticBaseline;
819      final int len = lines.length;
820      for (int i = 0; i < len; i++) {
821        _drawTextLine(style, lines[i], x, y);
822        y += paragraph._lineHeight;
823      }
824      _resetPaint();
825      return;
826    }
827
828    final html.Element paragraphElement =
829        _drawParagraphElement(paragraph, offset);
830
831    if (isClipped) {
832      final List<html.Element> clipElements =
833          _clipContent(_clipStack, paragraphElement, offset, currentTransform);
834      for (html.Element clipElement in clipElements) {
835        rootElement.append(clipElement);
836        _children.add(clipElement);
837      }
838    } else {
839      final String cssTransform =
840          matrix4ToCssTransform(transformWithOffset(currentTransform, offset));
841      paragraphElement.style.transform = cssTransform;
842      rootElement.append(paragraphElement);
843    }
844    _children.add(paragraphElement);
845  }
846
847  /// Paints the [picture] into this canvas.
848  void drawPicture(ui.Picture picture) {
849    picture.recordingCanvas.apply(this);
850  }
851
852  /// 'Runs' the given [path] by applying all of its commands to the canvas.
853  void _runPath(ui.Path path) {
854    ctx.beginPath();
855    for (Subpath subpath in path.subpaths) {
856      for (PathCommand command in subpath.commands) {
857        switch (command.type) {
858          case PathCommandTypes.bezierCurveTo:
859            final BezierCurveTo curve = command;
860            ctx.bezierCurveTo(
861                curve.x1, curve.y1, curve.x2, curve.y2, curve.x3, curve.y3);
862            break;
863          case PathCommandTypes.close:
864            ctx.closePath();
865            break;
866          case PathCommandTypes.ellipse:
867            final Ellipse ellipse = command;
868            ctx.ellipse(
869                ellipse.x,
870                ellipse.y,
871                ellipse.radiusX,
872                ellipse.radiusY,
873                ellipse.rotation,
874                ellipse.startAngle,
875                ellipse.endAngle,
876                ellipse.anticlockwise);
877            break;
878          case PathCommandTypes.lineTo:
879            final LineTo lineTo = command;
880            ctx.lineTo(lineTo.x, lineTo.y);
881            break;
882          case PathCommandTypes.moveTo:
883            final MoveTo moveTo = command;
884            ctx.moveTo(moveTo.x, moveTo.y);
885            break;
886          case PathCommandTypes.rRect:
887            final RRectCommand rrectCommand = command;
888            _drawRRectPath(rrectCommand.rrect, startNewPath: false);
889            break;
890          case PathCommandTypes.rect:
891            final RectCommand rectCommand = command;
892            ctx.rect(rectCommand.x, rectCommand.y, rectCommand.width,
893                rectCommand.height);
894            break;
895          case PathCommandTypes.quadraticCurveTo:
896            final QuadraticCurveTo quadraticCurveTo = command;
897            ctx.quadraticCurveTo(quadraticCurveTo.x1, quadraticCurveTo.y1,
898                quadraticCurveTo.x2, quadraticCurveTo.y2);
899            break;
900          default:
901            throw UnimplementedError('Unknown path command $command');
902        }
903      }
904    }
905  }
906}
907
908String _stringForBlendMode(ui.BlendMode blendMode) {
909  if (blendMode == null) {
910    return null;
911  }
912  switch (blendMode) {
913    case ui.BlendMode.srcOver:
914      return 'source-over';
915    case ui.BlendMode.srcIn:
916      return 'source-in';
917    case ui.BlendMode.srcOut:
918      return 'source-out';
919    case ui.BlendMode.srcATop:
920      return 'source-atop';
921    case ui.BlendMode.dstOver:
922      return 'destination-over';
923    case ui.BlendMode.dstIn:
924      return 'destination-in';
925    case ui.BlendMode.dstOut:
926      return 'destination-out';
927    case ui.BlendMode.dstATop:
928      return 'destination-atop';
929    case ui.BlendMode.plus:
930      return 'lighten';
931    case ui.BlendMode.src:
932      return 'copy';
933    case ui.BlendMode.xor:
934      return 'xor';
935    case ui.BlendMode.multiply:
936    // Falling back to multiply, ignoring alpha channel.
937    // TODO(flutter_web): only used for debug, find better fallback for web.
938    case ui.BlendMode.modulate:
939      return 'multiply';
940    case ui.BlendMode.screen:
941      return 'screen';
942    case ui.BlendMode.overlay:
943      return 'overlay';
944    case ui.BlendMode.darken:
945      return 'darken';
946    case ui.BlendMode.lighten:
947      return 'lighten';
948    case ui.BlendMode.colorDodge:
949      return 'color-dodge';
950    case ui.BlendMode.colorBurn:
951      return 'color-burn';
952    case ui.BlendMode.hardLight:
953      return 'hard-light';
954    case ui.BlendMode.softLight:
955      return 'soft-light';
956    case ui.BlendMode.difference:
957      return 'difference';
958    case ui.BlendMode.exclusion:
959      return 'exclusion';
960    case ui.BlendMode.hue:
961      return 'hue';
962    case ui.BlendMode.saturation:
963      return 'saturation';
964    case ui.BlendMode.color:
965      return 'color';
966    case ui.BlendMode.luminosity:
967      return 'luminosity';
968    default:
969      throw UnimplementedError(
970          'Flutter Web does not support the blend mode: $blendMode');
971  }
972}
973
974String _stringForStrokeCap(ui.StrokeCap strokeCap) {
975  if (strokeCap == null) {
976    return null;
977  }
978  switch (strokeCap) {
979    case ui.StrokeCap.butt:
980      return 'butt';
981    case ui.StrokeCap.round:
982      return 'round';
983    case ui.StrokeCap.square:
984    default:
985      return 'square';
986  }
987}
988
989String _stringForStrokeJoin(ui.StrokeJoin strokeJoin) {
990  assert(strokeJoin != null);
991  switch (strokeJoin) {
992    case ui.StrokeJoin.round:
993      return 'round';
994    case ui.StrokeJoin.bevel:
995      return 'bevel';
996    case ui.StrokeJoin.miter:
997    default:
998      return 'miter';
999  }
1000}
1001
1002/// Clips the content element against a stack of clip operations and returns
1003/// root of a tree that contains content node.
1004///
1005/// The stack of clipping rectangles generate an element that either uses
1006/// overflow:hidden with bounds to clip child or sets a clip-path to clip
1007/// it's contents. The clipping rectangles are nested and returned together
1008/// with a list of svg elements that provide clip-paths.
1009List<html.Element> _clipContent(List<_SaveClipEntry> clipStack,
1010    html.HtmlElement content, ui.Offset offset, Matrix4 currentTransform) {
1011  html.Element root, curElement;
1012  final List<html.Element> clipDefs = <html.Element>[];
1013  final int len = clipStack.length;
1014  for (int clipIndex = 0; clipIndex < len; clipIndex++) {
1015    final _SaveClipEntry entry = clipStack[clipIndex];
1016    final html.HtmlElement newElement = html.DivElement();
1017    if (root == null) {
1018      root = newElement;
1019    } else {
1020      domRenderer.append(curElement, newElement);
1021    }
1022    curElement = newElement;
1023    final ui.Rect rect = entry.rect;
1024    Matrix4 newClipTransform = entry.currentTransform;
1025    if (rect != null) {
1026      final double clipOffsetX = rect.left;
1027      final double clipOffsetY = rect.top;
1028      newClipTransform = newClipTransform.clone()
1029        ..translate(clipOffsetX, clipOffsetY);
1030      curElement.style
1031        ..overflow = 'hidden'
1032        ..transform = matrix4ToCssTransform(newClipTransform)
1033        ..transformOrigin = '0 0 0'
1034        ..width = '${rect.right - clipOffsetX}px'
1035        ..height = '${rect.bottom - clipOffsetY}px';
1036    } else if (entry.rrect != null) {
1037      final ui.RRect roundRect = entry.rrect;
1038      final String borderRadius =
1039          '${roundRect.tlRadiusX}px ${roundRect.trRadiusX}px '
1040          '${roundRect.brRadiusX}px ${roundRect.blRadiusX}px';
1041      final double clipOffsetX = roundRect.left;
1042      final double clipOffsetY = roundRect.top;
1043      newClipTransform = newClipTransform.clone()
1044        ..translate(clipOffsetX, clipOffsetY);
1045      curElement.style
1046        ..borderRadius = borderRadius
1047        ..overflow = 'hidden'
1048        ..transform = matrix4ToCssTransform(newClipTransform)
1049        ..transformOrigin = '0 0 0'
1050        ..width = '${roundRect.right - clipOffsetX}px'
1051        ..height = '${roundRect.bottom - clipOffsetY}px';
1052    } else if (entry.path != null) {
1053      curElement.style.transform = matrix4ToCssTransform(newClipTransform);
1054      final String svgClipPath = _pathToSvgClipPath(entry.path);
1055      final html.Element clipElement =
1056          html.Element.html(svgClipPath, treeSanitizer: _NullTreeSanitizer());
1057      domRenderer.setElementStyle(
1058          curElement, 'clip-path', 'url(#svgClip$_clipIdCounter)');
1059      domRenderer.setElementStyle(
1060          curElement, '-webkit-clip-path', 'url(#svgClip$_clipIdCounter)');
1061      clipDefs.add(clipElement);
1062    }
1063    // Reverse the transform of the clipping element so children can use
1064    // effective transform to render.
1065    // TODO(flutter_web): When we have more than a single clip element,
1066    // reduce number of div nodes by merging (multiplying transforms).
1067    final html.Element reverseTransformDiv = html.DivElement();
1068    reverseTransformDiv.style
1069      ..transform =
1070          _cssTransformAtOffset(newClipTransform.clone()..invert(), 0, 0)
1071      ..transformOrigin = '0 0 0';
1072    curElement.append(reverseTransformDiv);
1073    curElement = reverseTransformDiv;
1074  }
1075
1076  root.style.position = 'absolute';
1077  domRenderer.append(curElement, content);
1078  content.style.transform =
1079      _cssTransformAtOffset(currentTransform, offset.dx, offset.dy);
1080  return <html.Element>[root]..addAll(clipDefs);
1081}
1082
1083String _cssTransformAtOffset(
1084    Matrix4 transform, double offsetX, double offsetY) {
1085  return matrix4ToCssTransform(
1086      transformWithOffset(transform, ui.Offset(offsetX, offsetY)));
1087}
1088