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