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 ui; 6 7/// Defines how a list of points is interpreted when drawing a set of points. 8/// 9/// Used by [Canvas.drawPoints]. 10enum PointMode { 11 /// Draw each point separately. 12 /// 13 /// If the [Paint.strokeCap] is [StrokeCap.round], then each point is drawn 14 /// as a circle with the diameter of the [Paint.strokeWidth], filled as 15 /// described by the [Paint] (ignoring [Paint.style]). 16 /// 17 /// Otherwise, each point is drawn as an axis-aligned square with sides of 18 /// length [Paint.strokeWidth], filled as described by the [Paint] (ignoring 19 /// [Paint.style]). 20 points, 21 22 /// Draw each sequence of two points as a line segment. 23 /// 24 /// If the number of points is odd, then the last point is ignored. 25 /// 26 /// The lines are stroked as described by the [Paint] (ignoring 27 /// [Paint.style]). 28 lines, 29 30 /// Draw the entire sequence of point as one line. 31 /// 32 /// The lines are stroked as described by the [Paint] (ignoring 33 /// [Paint.style]). 34 polygon, 35} 36 37/// Defines how a new clip region should be merged with the existing clip 38/// region. 39/// 40/// Used by [Canvas.clipRect]. 41enum ClipOp { 42 /// Subtract the new region from the existing region. 43 difference, 44 45 /// Intersect the new region from the existing region. 46 intersect, 47} 48 49enum VertexMode { 50 /// Draw each sequence of three points as the vertices of a triangle. 51 triangles, 52 53 /// Draw each sliding window of three points as the vertices of a triangle. 54 triangleStrip, 55 56 /// Draw the first point and each sliding window of two points as the vertices of a triangle. 57 triangleFan, 58} 59 60/// A set of vertex data used by [Canvas.drawVertices]. 61class Vertices { 62 Vertices( 63 VertexMode mode, 64 List<Offset> positions, { 65 List<Offset> textureCoordinates, 66 List<Color> colors, 67 List<int> indices, 68 }) : assert(mode != null), 69 assert(positions != null); 70 71 Vertices.raw( 72 VertexMode mode, 73 Float32List positions, { 74 Float32List textureCoordinates, 75 Int32List colors, 76 Uint16List indices, 77 }) : assert(mode != null), 78 assert(positions != null); 79} 80 81/// Records a [Picture] containing a sequence of graphical operations. 82/// 83/// To begin recording, construct a [Canvas] to record the commands. 84/// To end recording, use the [PictureRecorder.endRecording] method. 85class PictureRecorder { 86 /// Creates a new idle PictureRecorder. To associate it with a 87 /// [Canvas] and begin recording, pass this [PictureRecorder] to the 88 /// [Canvas] constructor. 89 factory PictureRecorder() { 90 if (engine.experimentalUseSkia) { 91 return engine.SkPictureRecorder(); 92 } else { 93 return PictureRecorder._(); 94 } 95 } 96 97 PictureRecorder._(); 98 99 engine.RecordingCanvas _canvas; 100 Rect cullRect; 101 bool _isRecording = false; 102 103 engine.RecordingCanvas beginRecording(Rect bounds) { 104 assert(!_isRecording); 105 cullRect = bounds; 106 _isRecording = true; 107 _canvas = engine.RecordingCanvas(cullRect); 108 return _canvas; 109 } 110 111 /// Whether this object is currently recording commands. 112 /// 113 /// Specifically, this returns true if a [Canvas] object has been 114 /// created to record commands and recording has not yet ended via a 115 /// call to [endRecording], and false if either this 116 /// [PictureRecorder] has not yet been associated with a [Canvas], 117 /// or the [endRecording] method has already been called. 118 bool get isRecording => _isRecording; 119 120 /// Finishes recording graphical operations. 121 /// 122 /// Returns a picture containing the graphical operations that have been 123 /// recorded thus far. After calling this function, both the picture recorder 124 /// and the canvas objects are invalid and cannot be used further. 125 /// 126 /// Returns null if the PictureRecorder is not associated with a canvas. 127 Picture endRecording() { 128 // Returning null is what the flutter engine does: 129 // lib/ui/painting/picture_recorder.cc 130 if (!_isRecording) { 131 return null; 132 } 133 _isRecording = false; 134 return Picture._(_canvas, cullRect); 135 } 136} 137 138/// An interface for recording graphical operations. 139/// 140/// [Canvas] objects are used in creating [Picture] objects, which can 141/// themselves be used with a [SceneBuilder] to build a [Scene]. In 142/// normal usage, however, this is all handled by the framework. 143/// 144/// A canvas has a current transformation matrix which is applied to all 145/// operations. Initially, the transformation matrix is the identity transform. 146/// It can be modified using the [translate], [scale], [rotate], [skew], 147/// and [transform] methods. 148/// 149/// A canvas also has a current clip region which is applied to all operations. 150/// Initially, the clip region is infinite. It can be modified using the 151/// [clipRect], [clipRRect], and [clipPath] methods. 152/// 153/// The current transform and clip can be saved and restored using the stack 154/// managed by the [save], [saveLayer], and [restore] methods. 155class Canvas { 156 engine.RecordingCanvas _canvas; 157 158 /// Creates a canvas for recording graphical operations into the 159 /// given picture recorder. 160 /// 161 /// Graphical operations that affect pixels entirely outside the given 162 /// `cullRect` might be discarded by the implementation. However, the 163 /// implementation might draw outside these bounds if, for example, a command 164 /// draws partially inside and outside the `cullRect`. To ensure that pixels 165 /// outside a given region are discarded, consider using a [clipRect]. The 166 /// `cullRect` is optional; by default, all operations are kept. 167 /// 168 /// To end the recording, call [PictureRecorder.endRecording] on the 169 /// given recorder. 170 Canvas(PictureRecorder recorder, [Rect cullRect]) : assert(recorder != null) { 171 if (recorder.isRecording) { 172 throw ArgumentError( 173 '"recorder" must not already be associated with another Canvas.'); 174 } 175 cullRect ??= Rect.largest; 176 _canvas = recorder.beginRecording(cullRect); 177 } 178 179 /// Saves a copy of the current transform and clip on the save stack. 180 /// 181 /// Call [restore] to pop the save stack. 182 /// 183 /// See also: 184 /// 185 /// * [saveLayer], which does the same thing but additionally also groups the 186 /// commands done until the matching [restore]. 187 void save() { 188 _canvas.save(); 189 } 190 191 /// Saves a copy of the current transform and clip on the save stack, and then 192 /// creates a new group which subsequent calls will become a part of. When the 193 /// save stack is later popped, the group will be flattened into a layer and 194 /// have the given `paint`'s [Paint.colorFilter] and [Paint.blendMode] 195 /// applied. 196 /// 197 /// This lets you create composite effects, for example making a group of 198 /// drawing commands semi-transparent. Without using [saveLayer], each part of 199 /// the group would be painted individually, so where they overlap would be 200 /// darker than where they do not. By using [saveLayer] to group them 201 /// together, they can be drawn with an opaque color at first, and then the 202 /// entire group can be made transparent using the [saveLayer]'s paint. 203 /// 204 /// Call [restore] to pop the save stack and apply the paint to the group. 205 /// 206 /// ## Using saveLayer with clips 207 /// 208 /// When a rectangular clip operation (from [clipRect]) is not axis-aligned 209 /// with the raster buffer, or when the clip operation is not rectalinear (e.g. 210 /// because it is a rounded rectangle clip created by [clipRRect] or an 211 /// arbitrarily complicated path clip created by [clipPath]), the edge of the 212 /// clip needs to be anti-aliased. 213 /// 214 /// If two draw calls overlap at the edge of such a clipped region, without 215 /// using [saveLayer], the first drawing will be anti-aliased with the 216 /// background first, and then the second will be anti-aliased with the result 217 /// of blending the first drawing and the background. On the other hand, if 218 /// [saveLayer] is used immediately after establishing the clip, the second 219 /// drawing will cover the first in the layer, and thus the second alone will 220 /// be anti-aliased with the background when the layer is clipped and 221 /// composited (when [restore] is called). 222 /// 223 /// For example, this [CustomPainter.paint] method paints a clean white 224 /// rounded rectangle: 225 /// 226 /// ```dart 227 /// void paint(Canvas canvas, Size size) { 228 /// Rect rect = Offset.zero & size; 229 /// canvas.save(); 230 /// canvas.clipRRect(new RRect.fromRectXY(rect, 100.0, 100.0)); 231 /// canvas.saveLayer(rect, new Paint()); 232 /// canvas.drawPaint(new Paint()..color = Colors.red); 233 /// canvas.drawPaint(new Paint()..color = Colors.white); 234 /// canvas.restore(); 235 /// canvas.restore(); 236 /// } 237 /// ``` 238 /// 239 /// On the other hand, this one renders a red outline, the result of the red 240 /// paint being anti-aliased with the background at the clip edge, then the 241 /// white paint being similarly anti-aliased with the background _including 242 /// the clipped red paint_: 243 /// 244 /// ```dart 245 /// void paint(Canvas canvas, Size size) { 246 /// // (this example renders poorly, prefer the example above) 247 /// Rect rect = Offset.zero & size; 248 /// canvas.save(); 249 /// canvas.clipRRect(new RRect.fromRectXY(rect, 100.0, 100.0)); 250 /// canvas.drawPaint(new Paint()..color = Colors.red); 251 /// canvas.drawPaint(new Paint()..color = Colors.white); 252 /// canvas.restore(); 253 /// } 254 /// ``` 255 /// 256 /// This point is moot if the clip only clips one draw operation. For example, 257 /// the following paint method paints a pair of clean white rounded 258 /// rectangles, even though the clips are not done on a separate layer: 259 /// 260 /// ```dart 261 /// void paint(Canvas canvas, Size size) { 262 /// canvas.save(); 263 /// canvas.clipRRect(new RRect.fromRectXY(Offset.zero & (size / 2.0), 50.0, 50.0)); 264 /// canvas.drawPaint(new Paint()..color = Colors.white); 265 /// canvas.restore(); 266 /// canvas.save(); 267 /// canvas.clipRRect(new RRect.fromRectXY(size.center(Offset.zero) & (size / 2.0), 50.0, 50.0)); 268 /// canvas.drawPaint(new Paint()..color = Colors.white); 269 /// canvas.restore(); 270 /// } 271 /// ``` 272 /// 273 /// (Incidentally, rather than using [clipRRect] and [drawPaint] to draw 274 /// rounded rectangles like this, prefer the [drawRRect] method. These 275 /// examples are using [drawPaint] as a proxy for "complicated draw operations 276 /// that will get clipped", to illustrate the point.) 277 /// 278 /// ## Performance considerations 279 /// 280 /// Generally speaking, [saveLayer] is relatively expensive. 281 /// 282 /// There are a several different hardware architectures for GPUs (graphics 283 /// processing units, the hardware that handles graphics), but most of them 284 /// involve batching commands and reordering them for performance. When layers 285 /// are used, they cause the rendering pipeline to have to switch render 286 /// target (from one layer to another). Render target switches can flush the 287 /// GPU's command buffer, which typically means that optimizations that one 288 /// could get with larger batching are lost. Render target switches also 289 /// generate a lot of memory churn because the GPU needs to copy out the 290 /// current frame buffer contents from the part of memory that's optimized for 291 /// writing, and then needs to copy it back in once the previous render target 292 /// (layer) is restored. 293 /// 294 /// See also: 295 /// 296 /// * [save], which saves the current state, but does not create a new layer 297 /// for subsequent commands. 298 /// * [BlendMode], which discusses the use of [Paint.blendMode] with 299 /// [saveLayer]. 300 void saveLayer(Rect bounds, Paint paint) { 301 assert(paint != null); 302 if (bounds == null) { 303 _saveLayerWithoutBounds(paint); 304 } else { 305 assert(engine.rectIsValid(bounds)); 306 _saveLayer(bounds, paint); 307 } 308 } 309 310 void _saveLayerWithoutBounds(Paint paint) { 311 _canvas.saveLayerWithoutBounds(paint); 312 } 313 314 void _saveLayer(Rect bounds, Paint paint) { 315 _canvas.saveLayer(bounds, paint); 316 } 317 318 /// Pops the current save stack, if there is anything to pop. 319 /// Otherwise, does nothing. 320 /// 321 /// Use [save] and [saveLayer] to push state onto the stack. 322 /// 323 /// If the state was pushed with with [saveLayer], then this call will also 324 /// cause the new layer to be composited into the previous layer. 325 void restore() { 326 _canvas.restore(); 327 } 328 329 /// Returns the number of items on the save stack, including the 330 /// initial state. This means it returns 1 for a clean canvas, and 331 /// that each call to [save] and [saveLayer] increments it, and that 332 /// each matching call to [restore] decrements it. 333 /// 334 /// This number cannot go below 1. 335 int getSaveCount() => _canvas.saveCount; 336 337 /// Add a translation to the current transform, shifting the coordinate space 338 /// horizontally by the first argument and vertically by the second argument. 339 void translate(double dx, double dy) { 340 _canvas.translate(dx, dy); 341 } 342 343 /// Add an axis-aligned scale to the current transform, scaling by the first 344 /// argument in the horizontal direction and the second in the vertical 345 /// direction. 346 /// 347 /// If [sy] is unspecified, [sx] will be used for the scale in both 348 /// directions. 349 void scale(double sx, [double sy]) => _scale(sx, sy ?? sx); 350 351 void _scale(double sx, double sy) { 352 _canvas.scale(sx, sy); 353 } 354 355 /// Add a rotation to the current transform. The argument is in radians clockwise. 356 void rotate(double radians) { 357 _canvas.rotate(radians); 358 } 359 360 /// Add an axis-aligned skew to the current transform, with the first argument 361 /// being the horizontal skew in radians clockwise around the origin, and the 362 /// second argument being the vertical skew in radians clockwise around the 363 /// origin. 364 void skew(double sx, double sy) { 365 _canvas.skew(sx, sy); 366 } 367 368 /// Multiply the current transform by the specified 4⨉4 transformation matrix 369 /// specified as a list of values in column-major order. 370 void transform(Float64List matrix4) { 371 assert(matrix4 != null); 372 if (matrix4.length != 16) { 373 throw ArgumentError('"matrix4" must have 16 entries.'); 374 } 375 _transform(matrix4); 376 } 377 378 void _transform(Float64List matrix4) { 379 _canvas.transform(matrix4); 380 } 381 382 /// Reduces the clip region to the intersection of the current clip and the 383 /// given rectangle. 384 /// 385 /// If [doAntiAlias] is true, then the clip will be anti-aliased. 386 /// 387 /// If multiple draw commands intersect with the clip boundary, this can result 388 /// in incorrect blending at the clip boundary. See [saveLayer] for a 389 /// discussion of how to address that. 390 /// 391 /// Use [ClipOp.difference] to subtract the provided rectangle from the 392 /// current clip. 393 void clipRect(Rect rect, 394 {ClipOp clipOp = ClipOp.intersect, bool doAntiAlias = true}) { 395 assert(engine.rectIsValid(rect)); 396 assert(clipOp != null); 397 assert(doAntiAlias != null); 398 _clipRect(rect, clipOp, doAntiAlias); 399 } 400 401 void _clipRect(Rect rect, ClipOp clipOp, bool doAntiAlias) { 402 _canvas.clipRect(rect); 403 } 404 405 /// Reduces the clip region to the intersection of the current clip and the 406 /// given rounded rectangle. 407 /// 408 /// If [doAntiAlias] is true, then the clip will be anti-aliased. 409 /// 410 /// If multiple draw commands intersect with the clip boundary, this can result 411 /// in incorrect blending at the clip boundary. See [saveLayer] for a 412 /// discussion of how to address that and some examples of using [clipRRect]. 413 void clipRRect(RRect rrect, {bool doAntiAlias = true}) { 414 assert(engine.rrectIsValid(rrect)); 415 assert(doAntiAlias != null); 416 _clipRRect(rrect, doAntiAlias); 417 } 418 419 void _clipRRect(RRect rrect, bool doAntiAlias) { 420 _canvas.clipRRect(rrect); 421 } 422 423 /// Reduces the clip region to the intersection of the current clip and the 424 /// given [Path]. 425 /// 426 /// If [doAntiAlias] is true, then the clip will be anti-aliased. 427 /// 428 /// If multiple draw commands intersect with the clip boundary, this can result 429 /// multiple draw commands intersect with the clip boundary, this can result 430 /// in incorrect blending at the clip boundary. See [saveLayer] for a 431 /// discussion of how to address that. 432 void clipPath(Path path, {bool doAntiAlias = true}) { 433 assert(path != null); // path is checked on the engine side 434 assert(doAntiAlias != null); 435 _clipPath(path, doAntiAlias); 436 } 437 438 void _clipPath(Path path, bool doAntiAlias) { 439 _canvas.clipPath(path); 440 } 441 442 /// Paints the given [Color] onto the canvas, applying the given 443 /// [BlendMode], with the given color being the source and the background 444 /// being the destination. 445 void drawColor(Color color, BlendMode blendMode) { 446 assert(color != null); 447 assert(blendMode != null); 448 _drawColor(color, blendMode); 449 } 450 451 void _drawColor(Color color, BlendMode blendMode) { 452 _canvas.drawColor(color, blendMode); 453 } 454 455 /// Draws a line between the given points using the given paint. The line is 456 /// stroked, the value of the [Paint.style] is ignored for this call. 457 /// 458 /// The `p1` and `p2` arguments are interpreted as offsets from the origin. 459 void drawLine(Offset p1, Offset p2, Paint paint) { 460 assert(engine.offsetIsValid(p1)); 461 assert(engine.offsetIsValid(p2)); 462 assert(paint != null); 463 _drawLine(p1, p2, paint); 464 } 465 466 void _drawLine(Offset p1, Offset p2, Paint paint) { 467 _canvas.drawLine(p1, p2, paint); 468 } 469 470 /// Fills the canvas with the given [Paint]. 471 /// 472 /// To fill the canvas with a solid color and blend mode, consider 473 /// [drawColor] instead. 474 void drawPaint(Paint paint) { 475 assert(paint != null); 476 _drawPaint(paint); 477 } 478 479 void _drawPaint(Paint paint) { 480 _canvas.drawPaint(paint); 481 } 482 483 /// Draws a rectangle with the given [Paint]. Whether the rectangle is filled 484 /// or stroked (or both) is controlled by [Paint.style]. 485 void drawRect(Rect rect, Paint paint) { 486 assert(engine.rectIsValid(rect)); 487 assert(paint != null); 488 _drawRect(rect, paint); 489 } 490 491 void _drawRect(Rect rect, Paint paint) { 492 _canvas.drawRect(rect, paint); 493 } 494 495 /// Draws a rounded rectangle with the given [Paint]. Whether the rectangle is 496 /// filled or stroked (or both) is controlled by [Paint.style]. 497 void drawRRect(RRect rrect, Paint paint) { 498 assert(engine.rrectIsValid(rrect)); 499 assert(paint != null); 500 _drawRRect(rrect, paint); 501 } 502 503 void _drawRRect(RRect rrect, Paint paint) { 504 _canvas.drawRRect(rrect, paint); 505 } 506 507 /// Draws a shape consisting of the difference between two rounded rectangles 508 /// with the given [Paint]. Whether this shape is filled or stroked (or both) 509 /// is controlled by [Paint.style]. 510 /// 511 /// This shape is almost but not quite entirely unlike an annulus. 512 void drawDRRect(RRect outer, RRect inner, Paint paint) { 513 assert(engine.rrectIsValid(outer)); 514 assert(engine.rrectIsValid(inner)); 515 assert(paint != null); 516 _drawDRRect(outer, inner, paint); 517 } 518 519 void _drawDRRect(RRect outer, RRect inner, Paint paint) { 520 _canvas.drawDRRect(outer, inner, paint); 521 } 522 523 /// Draws an axis-aligned oval that fills the given axis-aligned rectangle 524 /// with the given [Paint]. Whether the oval is filled or stroked (or both) is 525 /// controlled by [Paint.style]. 526 void drawOval(Rect rect, Paint paint) { 527 assert(engine.rectIsValid(rect)); 528 assert(paint != null); 529 _drawOval(rect, paint); 530 } 531 532 void _drawOval(Rect rect, Paint paint) { 533 _canvas.drawOval(rect, paint); 534 } 535 536 /// Draws a circle centered at the point given by the first argument and 537 /// that has the radius given by the second argument, with the [Paint] given in 538 /// the third argument. Whether the circle is filled or stroked (or both) is 539 /// controlled by [Paint.style]. 540 void drawCircle(Offset c, double radius, Paint paint) { 541 assert(engine.offsetIsValid(c)); 542 assert(paint != null); 543 _drawCircle(c, radius, paint); 544 } 545 546 void _drawCircle(Offset c, double radius, Paint paint) { 547 _canvas.drawCircle(c, radius, paint); 548 } 549 550 /// Draw an arc scaled to fit inside the given rectangle. It starts from 551 /// startAngle radians around the oval up to startAngle + sweepAngle 552 /// radians around the oval, with zero radians being the point on 553 /// the right hand side of the oval that crosses the horizontal line 554 /// that intersects the center of the rectangle and with positive 555 /// angles going clockwise around the oval. If useCenter is true, the arc is 556 /// closed back to the center, forming a circle sector. Otherwise, the arc is 557 /// not closed, forming a circle segment. 558 /// 559 /// This method is optimized for drawing arcs and should be faster than [Path.arcTo]. 560 void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, 561 Paint paint) { 562 assert(engine.rectIsValid(rect)); 563 assert(paint != null); 564 const double pi = math.pi; 565 const double pi2 = 2.0 * pi; 566 567 final Path path = Path(); 568 if (useCenter) { 569 path.moveTo( 570 (rect.left + rect.right) / 2.0, (rect.top + rect.bottom) / 2.0); 571 } 572 bool forceMoveTo = !useCenter; 573 if (sweepAngle <= -pi2) { 574 path.arcTo(rect, startAngle, -pi, forceMoveTo); 575 startAngle -= pi; 576 path.arcTo(rect, startAngle, -pi, false); 577 startAngle -= pi; 578 forceMoveTo = false; 579 sweepAngle += pi2; 580 } 581 while (sweepAngle >= pi2) { 582 path.arcTo(rect, startAngle, pi, forceMoveTo); 583 startAngle += pi; 584 path.arcTo(rect, startAngle, pi, false); 585 startAngle += pi; 586 forceMoveTo = false; 587 sweepAngle -= pi2; 588 } 589 path.arcTo(rect, startAngle, sweepAngle, forceMoveTo); 590 if (useCenter) { 591 path.close(); 592 } 593 _canvas.drawPath(path, paint); 594 } 595 596 /// Draws the given [Path] with the given [Paint]. Whether this shape is 597 /// filled or stroked (or both) is controlled by [Paint.style]. If the path is 598 /// filled, then subpaths within it are implicitly closed (see [Path.close]). 599 void drawPath(Path path, Paint paint) { 600 assert(path != null); // path is checked on the engine side 601 assert(paint != null); 602 _drawPath(path, paint); 603 } 604 605 void _drawPath(Path path, Paint paint) { 606 _canvas.drawPath(path, paint); 607 } 608 609 /// Draws the given [Image] into the canvas with its top-left corner at the 610 /// given [Offset]. The image is composited into the canvas using the given [Paint]. 611 void drawImage(Image image, Offset p, Paint paint) { 612 assert(image != null); // image is checked on the engine side 613 assert(engine.offsetIsValid(p)); 614 assert(paint != null); 615 _drawImage(image, p, paint); 616 } 617 618 void _drawImage(Image image, Offset p, Paint paint) { 619 _canvas.drawImage(image, p, paint); 620 } 621 622 /// Draws the subset of the given image described by the `src` argument into 623 /// the canvas in the axis-aligned rectangle given by the `dst` argument. 624 /// 625 /// This might sample from outside the `src` rect by up to half the width of 626 /// an applied filter. 627 /// 628 /// Multiple calls to this method with different arguments (from the same 629 /// image) can be batched into a single call to [drawAtlas] to improve 630 /// performance. 631 void drawImageRect(Image image, Rect src, Rect dst, Paint paint) { 632 assert(image != null); // image is checked on the engine side 633 assert(engine.rectIsValid(src)); 634 assert(engine.rectIsValid(dst)); 635 assert(paint != null); 636 _drawImageRect(image, src, dst, paint); 637 } 638 639 void _drawImageRect(Image image, Rect src, Rect dst, Paint paint) { 640 _canvas.drawImageRect(image, src, dst, paint); 641 } 642 643 /// Draws the given [Image] into the canvas using the given [Paint]. 644 /// 645 /// The image is drawn in nine portions described by splitting the image by 646 /// drawing two horizontal lines and two vertical lines, where the `center` 647 /// argument describes the rectangle formed by the four points where these 648 /// four lines intersect each other. (This forms a 3-by-3 grid of regions, 649 /// the center region being described by the `center` argument.) 650 /// 651 /// The four regions in the corners are drawn, without scaling, in the four 652 /// corners of the destination rectangle described by `dst`. The remaining 653 /// five regions are drawn by stretching them to fit such that they exactly 654 /// cover the destination rectangle while maintaining their relative 655 /// positions. 656 void drawImageNine(Image image, Rect center, Rect dst, Paint paint) { 657 assert(image != null); // image is checked on the engine side 658 assert(engine.rectIsValid(center)); 659 assert(engine.rectIsValid(dst)); 660 assert(paint != null); 661 662 // Assert you can fit the scaled image into dst. 663 assert(image.width - center.width >= dst.width); 664 assert(image.height - center.height >= dst.height); 665 666 // The four unscaled corner rectangles in the from the src. 667 final Rect srcTopLeft = Rect.fromLTWH( 668 0, 669 0, 670 center.left, 671 center.top, 672 ); 673 final Rect srcTopRight = Rect.fromLTWH( 674 center.right, 675 0, 676 image.width - center.right, 677 center.top, 678 ); 679 final Rect srcBottomLeft = Rect.fromLTWH( 680 0, 681 center.bottom, 682 center.left, 683 image.height - center.bottom, 684 ); 685 final Rect srcBottomRight = Rect.fromLTWH( 686 center.right, 687 center.bottom, 688 image.width - center.right, 689 image.height - center.bottom, 690 ); 691 692 final Rect dstTopLeft = srcTopLeft.shift(dst.topLeft); 693 694 // The center rectangle in the dst region 695 final Rect dstCenter = Rect.fromLTWH( 696 dstTopLeft.right, 697 dstTopLeft.bottom, 698 dst.width - (srcTopLeft.width + srcTopRight.width), 699 dst.height - (srcTopLeft.height + srcBottomLeft.height), 700 ); 701 702 drawImageRect(image, srcTopLeft, dstTopLeft, paint); 703 704 final Rect dstTopRight = Rect.fromLTWH( 705 dstCenter.right, 706 dst.top, 707 srcTopRight.width, 708 srcTopRight.height, 709 ); 710 drawImageRect(image, srcTopRight, dstTopRight, paint); 711 712 final Rect dstBottomLeft = Rect.fromLTWH( 713 dst.left, 714 dstCenter.bottom, 715 srcBottomLeft.width, 716 srcBottomLeft.height, 717 ); 718 drawImageRect(image, srcBottomLeft, dstBottomLeft, paint); 719 720 final Rect dstBottomRight = Rect.fromLTWH( 721 dstCenter.right, 722 dstCenter.bottom, 723 srcBottomRight.width, 724 srcBottomRight.height, 725 ); 726 drawImageRect(image, srcBottomRight, dstBottomRight, paint); 727 728 // Draw the top center rectangle. 729 drawImageRect( 730 image, 731 Rect.fromLTRB( 732 srcTopLeft.right, 733 srcTopLeft.top, 734 srcTopRight.left, 735 srcTopRight.bottom, 736 ), 737 Rect.fromLTRB( 738 dstTopLeft.right, 739 dstTopLeft.top, 740 dstTopRight.left, 741 dstTopRight.bottom, 742 ), 743 paint, 744 ); 745 746 // Draw the middle left rectangle. 747 drawImageRect( 748 image, 749 Rect.fromLTRB( 750 srcTopLeft.left, 751 srcTopLeft.bottom, 752 srcBottomLeft.right, 753 srcBottomLeft.top, 754 ), 755 Rect.fromLTRB( 756 dstTopLeft.left, 757 dstTopLeft.bottom, 758 dstBottomLeft.right, 759 dstBottomLeft.top, 760 ), 761 paint, 762 ); 763 764 // Draw the center rectangle. 765 drawImageRect(image, center, dstCenter, paint); 766 767 // Draw the middle right rectangle. 768 drawImageRect( 769 image, 770 Rect.fromLTRB( 771 srcTopRight.left, 772 srcTopRight.bottom, 773 srcBottomRight.right, 774 srcBottomRight.top, 775 ), 776 Rect.fromLTRB( 777 dstTopRight.left, 778 dstTopRight.bottom, 779 dstBottomRight.right, 780 dstBottomRight.top, 781 ), 782 paint, 783 ); 784 785 // Draw the bottom center rectangle. 786 drawImageRect( 787 image, 788 Rect.fromLTRB( 789 srcBottomLeft.right, 790 srcBottomLeft.top, 791 srcBottomRight.left, 792 srcBottomRight.bottom, 793 ), 794 Rect.fromLTRB( 795 dstBottomLeft.right, 796 dstBottomLeft.top, 797 dstBottomRight.left, 798 dstBottomRight.bottom, 799 ), 800 paint, 801 ); 802 } 803 804 /// Draw the given picture onto the canvas. To create a picture, see 805 /// [PictureRecorder]. 806 void drawPicture(Picture picture) { 807 assert(picture != null); // picture is checked on the engine side 808 // TODO(het): Support this 809 throw UnimplementedError(); 810 } 811 812 /// Draws the text in the given [Paragraph] into this canvas at the given 813 /// [Offset]. 814 /// 815 /// The [Paragraph] object must have had [Paragraph.layout] called on it 816 /// first. 817 /// 818 /// To align the text, set the `textAlign` on the [ParagraphStyle] object 819 /// passed to the [new ParagraphBuilder] constructor. For more details see 820 /// [TextAlign] and the discussion at [new ParagraphStyle]. 821 /// 822 /// If the text is left aligned or justified, the left margin will be at the 823 /// position specified by the `offset` argument's [Offset.dx] coordinate. 824 /// 825 /// If the text is right aligned or justified, the right margin will be at the 826 /// position described by adding the [ParagraphConstraints.width] given to 827 /// [Paragraph.layout], to the `offset` argument's [Offset.dx] coordinate. 828 /// 829 /// If the text is centered, the centering axis will be at the position 830 /// described by adding half of the [ParagraphConstraints.width] given to 831 /// [Paragraph.layout], to the `offset` argument's [Offset.dx] coordinate. 832 void drawParagraph(Paragraph paragraph, Offset offset) { 833 assert(paragraph != null); 834 assert(engine.offsetIsValid(offset)); 835 _drawParagraph(paragraph, offset); 836 } 837 838 void _drawParagraph(Paragraph paragraph, Offset offset) { 839 _canvas.drawParagraph(paragraph, offset); 840 } 841 842 /// Draws a sequence of points according to the given [PointMode]. 843 /// 844 /// The `points` argument is interpreted as offsets from the origin. 845 /// 846 /// See also: 847 /// 848 /// * [drawRawPoints], which takes `points` as a [Float32List] rather than a 849 /// [List<Offset>]. 850 void drawPoints(PointMode pointMode, List<Offset> points, Paint paint) { 851 assert(pointMode != null); 852 assert(points != null); 853 assert(paint != null); 854 throw UnimplementedError(); 855 } 856 857 /// Draws a sequence of points according to the given [PointMode]. 858 /// 859 /// The `points` argument is interpreted as a list of pairs of floating point 860 /// numbers, where each pair represents an x and y offset from the origin. 861 /// 862 /// See also: 863 /// 864 /// * [drawPoints], which takes `points` as a [List<Offset>] rather than a 865 /// [List<Float32List>]. 866 void drawRawPoints(PointMode pointMode, Float32List points, Paint paint) { 867 assert(pointMode != null); 868 assert(points != null); 869 assert(paint != null); 870 if (points.length % 2 != 0) { 871 throw ArgumentError('"points" must have an even number of values.'); 872 } 873 throw UnimplementedError(); 874 } 875 876 void drawVertices(Vertices vertices, BlendMode blendMode, Paint paint) { 877 assert(vertices != null); // vertices is checked on the engine side 878 assert(paint != null); 879 assert(blendMode != null); 880 throw UnimplementedError(); 881 } 882 883 // 884 // See also: 885 // 886 // * [drawRawAtlas], which takes its arguments as typed data lists rather 887 // than objects. 888 void drawAtlas(Image atlas, List<RSTransform> transforms, List<Rect> rects, 889 List<Color> colors, BlendMode blendMode, Rect cullRect, Paint paint) { 890 assert(atlas != null); // atlas is checked on the engine side 891 assert(transforms != null); 892 assert(rects != null); 893 assert(colors != null); 894 assert(blendMode != null); 895 assert(paint != null); 896 897 final int rectCount = rects.length; 898 if (transforms.length != rectCount) { 899 throw ArgumentError('"transforms" and "rects" lengths must match.'); 900 } 901 if (colors.isNotEmpty && colors.length != rectCount) { 902 throw ArgumentError( 903 'If non-null, "colors" length must match that of "transforms" and "rects".'); 904 } 905 906 // TODO(het): Do we need to support this? 907 throw UnimplementedError(); 908 } 909 910 // 911 // The `rstTransforms` argument is interpreted as a list of four-tuples, with 912 // each tuple being ([RSTransform.scos], [RSTransform.ssin], 913 // [RSTransform.tx], [RSTransform.ty]). 914 // 915 // The `rects` argument is interpreted as a list of four-tuples, with each 916 // tuple being ([Rect.left], [Rect.top], [Rect.right], [Rect.bottom]). 917 // 918 // The `colors` argument, which can be null, is interpreted as a list of 919 // 32-bit colors, with the same packing as [Color.value]. 920 // 921 // See also: 922 // 923 // * [drawAtlas], which takes its arguments as objects rather than typed 924 // data lists. 925 void drawRawAtlas(Image atlas, Float32List rstTransforms, Float32List rects, 926 Int32List colors, BlendMode blendMode, Rect cullRect, Paint paint) { 927 assert(atlas != null); // atlas is checked on the engine side 928 assert(rstTransforms != null); 929 assert(rects != null); 930 assert(colors != null); 931 assert(blendMode != null); 932 assert(paint != null); 933 934 final int rectCount = rects.length; 935 if (rstTransforms.length != rectCount) { 936 throw ArgumentError('"rstTransforms" and "rects" lengths must match.'); 937 } 938 if (rectCount % 4 != 0) { 939 throw ArgumentError( 940 '"rstTransforms" and "rects" lengths must be a multiple of four.'); 941 } 942 if (colors != null && colors.length * 4 != rectCount) { 943 throw ArgumentError( 944 'If non-null, "colors" length must be one fourth the length of "rstTransforms" and "rects".'); 945 } 946 947 // TODO(het): Do we need to support this? 948 throw UnimplementedError(); 949 } 950 951 /// Draws a shadow for a [Path] representing the given material elevation. 952 /// 953 /// The `transparentOccluder` argument should be true if the occluding object 954 /// is not opaque. 955 /// 956 /// The arguments must not be null. 957 void drawShadow( 958 Path path, Color color, double elevation, bool transparentOccluder) { 959 assert(path != null); // path is checked on the engine side 960 assert(color != null); 961 assert(transparentOccluder != null); 962 _canvas.drawShadow(path, color, elevation, transparentOccluder); 963 } 964} 965 966/// An object representing a sequence of recorded graphical operations. 967/// 968/// To create a [Picture], use a [PictureRecorder]. 969/// 970/// A [Picture] can be placed in a [Scene] using a [SceneBuilder], via 971/// the [SceneBuilder.addPicture] method. A [Picture] can also be 972/// drawn into a [Canvas], using the [Canvas.drawPicture] method. 973class Picture { 974 /// This class is created by the engine, and should not be instantiated 975 /// or extended directly. 976 /// 977 /// To create a [Picture], use a [PictureRecorder]. 978 Picture._(this.recordingCanvas, this.cullRect); 979 980 /// Creates an image from this picture. 981 /// 982 /// The picture is rasterized using the number of pixels specified by the 983 /// given width and height. 984 /// 985 /// Although the image is returned synchronously, the picture is actually 986 /// rasterized the first time the image is drawn and then cached. 987 Future<Image> toImage(int width, int height) => null; 988 989 /// Release the resources used by this object. The object is no longer usable 990 /// after this method is called. 991 void dispose() {} 992 993 /// Returns the approximate number of bytes allocated for this object. 994 /// 995 /// The actual size of this picture may be larger, particularly if it contains 996 /// references to image or other large objects. 997 int get approximateBytesUsed => 0; 998 999 final engine.RecordingCanvas recordingCanvas; 1000 final Rect cullRect; 1001} 1002 1003/// Determines the winding rule that decides how the interior of a [Path] is 1004/// calculated. 1005/// 1006/// This enum is used by the [Path.fillType] property. 1007enum PathFillType { 1008 /// The interior is defined by a non-zero sum of signed edge crossings. 1009 /// 1010 /// For a given point, the point is considered to be on the inside of the path 1011 /// if a line drawn from the point to infinity crosses lines going clockwise 1012 /// around the point a different number of times than it crosses lines going 1013 /// counter-clockwise around that point. 1014 /// 1015 /// See: <https://en.wikipedia.org/wiki/Nonzero-rule> 1016 nonZero, 1017 1018 /// The interior is defined by an odd number of edge crossings. 1019 /// 1020 /// For a given point, the point is considered to be on the inside of the path 1021 /// if a line drawn from the point to infinity crosses an odd number of lines. 1022 /// 1023 /// See: <https://en.wikipedia.org/wiki/Even-odd_rule> 1024 evenOdd, 1025} 1026 1027/// Strategies for combining paths. 1028/// 1029/// See also: 1030/// 1031/// * [Path.combine], which uses this enum to decide how to combine two paths. 1032// Must be kept in sync with SkPathOp 1033enum PathOperation { 1034 /// Subtract the second path from the first path. 1035 /// 1036 /// For example, if the two paths are overlapping circles of equal diameter 1037 /// but differing centers, the result would be a crescent portion of the 1038 /// first circle that was not overlapped by the second circle. 1039 /// 1040 /// See also: 1041 /// 1042 /// * [reverseDifference], which is the same but subtracting the first path 1043 /// from the second. 1044 difference, 1045 1046 /// Create a new path that is the intersection of the two paths, leaving the 1047 /// overlapping pieces of the path. 1048 /// 1049 /// For example, if the two paths are overlapping circles of equal diameter 1050 /// but differing centers, the result would be only the overlapping portion 1051 /// of the two circles. 1052 /// 1053 /// See also: 1054 /// * [xor], which is the inverse of this operation 1055 intersect, 1056 1057 /// Create a new path that is the union (inclusive-or) of the two paths. 1058 /// 1059 /// For example, if the two paths are overlapping circles of equal diameter 1060 /// but differing centers, the result would be a figure-eight like shape 1061 /// matching the outer boundaries of both circles. 1062 union, 1063 1064 /// Create a new path that is the exclusive-or of the two paths, leaving 1065 /// everything but the overlapping pieces of the path. 1066 /// 1067 /// For example, if the two paths are overlapping circles of equal diameter 1068 /// but differing centers, the figure-eight like shape less the overlapping 1069 /// parts 1070 /// 1071 /// See also: 1072 /// * [intersect], which is the inverse of this operation 1073 xor, 1074 1075 /// Subtract the first path from the second path. 1076 /// 1077 /// For example, if the two paths are overlapping circles of equal diameter 1078 /// but differing centers, the result would be a crescent portion of the 1079 /// second circle that was not overlapped by the first circle. 1080 /// 1081 /// See also: 1082 /// 1083 /// * [difference], which is the same but subtracting the second path 1084 /// from the first. 1085 reverseDifference, 1086} 1087 1088/// A complex, one-dimensional subset of a plane. 1089/// 1090/// A path consists of a number of subpaths, and a _current point_. 1091/// 1092/// Subpaths consist of segments of various types, such as lines, 1093/// arcs, or beziers. Subpaths can be open or closed, and can 1094/// self-intersect. 1095/// 1096/// Closed subpaths enclose a (possibly discontiguous) region of the 1097/// plane based on the current [fillType]. 1098/// 1099/// The _current point_ is initially at the origin. After each 1100/// operation adding a segment to a subpath, the current point is 1101/// updated to the end of that segment. 1102/// 1103/// Paths can be drawn on canvases using [Canvas.drawPath], and can 1104/// used to create clip regions using [Canvas.clipPath]. 1105class Path { 1106 final List<engine.Subpath> subpaths; 1107 PathFillType _fillType = PathFillType.nonZero; 1108 1109 engine.Subpath get _currentSubpath => subpaths.isEmpty ? null : subpaths.last; 1110 1111 List<engine.PathCommand> get _commands => _currentSubpath?.commands; 1112 1113 /// The current x-coordinate for this path. 1114 double get _currentX => _currentSubpath?.currentX ?? 0.0; 1115 1116 /// The current y-coordinate for this path. 1117 double get _currentY => _currentSubpath?.currentY ?? 0.0; 1118 1119 /// Recorder used for hit testing paths. 1120 static RawRecordingCanvas _rawRecorder; 1121 1122 /// Create a new empty [Path] object. 1123 factory Path() { 1124 if (engine.experimentalUseSkia) { 1125 return engine.SkPath(); 1126 } else { 1127 return Path._(); 1128 } 1129 } 1130 1131 Path._() : subpaths = <engine.Subpath>[]; 1132 1133 /// Creates a copy of another [Path]. 1134 /// 1135 /// This copy is fast and does not require additional memory unless either 1136 /// the `source` path or the path returned by this constructor are modified. 1137 Path.from(Path source) 1138 : subpaths = List<engine.Subpath>.from(source.subpaths); 1139 1140 Path._clone(this.subpaths, this._fillType); 1141 1142 /// Determines how the interior of this path is calculated. 1143 /// 1144 /// Defaults to the non-zero winding rule, [PathFillType.nonZero]. 1145 PathFillType get fillType => _fillType; 1146 set fillType(PathFillType value) { 1147 _fillType = value; 1148 } 1149 1150 /// Opens a new subpath with starting point (x, y). 1151 void _openNewSubpath(double x, double y) { 1152 subpaths.add(engine.Subpath(x, y)); 1153 _setCurrentPoint(x, y); 1154 } 1155 1156 /// Sets the current point to (x, y). 1157 void _setCurrentPoint(double x, double y) { 1158 _currentSubpath.currentX = x; 1159 _currentSubpath.currentY = y; 1160 } 1161 1162 /// Starts a new subpath at the given coordinate. 1163 void moveTo(double x, double y) { 1164 _openNewSubpath(x, y); 1165 _commands.add(engine.MoveTo(x, y)); 1166 } 1167 1168 /// Starts a new subpath at the given offset from the current point. 1169 void relativeMoveTo(double dx, double dy) { 1170 final double newX = _currentX + dx; 1171 final double newY = _currentY + dy; 1172 _openNewSubpath(newX, newY); 1173 _commands.add(engine.MoveTo(newX, newY)); 1174 } 1175 1176 /// Adds a straight line segment from the current point to the given 1177 /// point. 1178 void lineTo(double x, double y) { 1179 if (subpaths.isEmpty) { 1180 moveTo(0.0, 0.0); 1181 } 1182 _commands.add(engine.LineTo(x, y)); 1183 _setCurrentPoint(x, y); 1184 } 1185 1186 /// Adds a straight line segment from the current point to the point 1187 /// at the given offset from the current point. 1188 void relativeLineTo(double dx, double dy) { 1189 final double newX = _currentX + dx; 1190 final double newY = _currentY + dy; 1191 if (subpaths.isEmpty) { 1192 moveTo(0.0, 0.0); 1193 } 1194 _commands.add(engine.LineTo(newX, newY)); 1195 _setCurrentPoint(newX, newY); 1196 } 1197 1198 void _ensurePathStarted() { 1199 if (subpaths.isEmpty) { 1200 subpaths.add(engine.Subpath(0.0, 0.0)); 1201 } 1202 } 1203 1204 /// Adds a quadratic bezier segment that curves from the current 1205 /// point to the given point (x2,y2), using the control point 1206 /// (x1,y1). 1207 void quadraticBezierTo(double x1, double y1, double x2, double y2) { 1208 _ensurePathStarted(); 1209 _commands.add(engine.QuadraticCurveTo(x1, y1, x2, y2)); 1210 _setCurrentPoint(x2, y2); 1211 } 1212 1213 /// Adds a quadratic bezier segment that curves from the current 1214 /// point to the point at the offset (x2,y2) from the current point, 1215 /// using the control point at the offset (x1,y1) from the current 1216 /// point. 1217 void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) { 1218 _ensurePathStarted(); 1219 _commands.add(engine.QuadraticCurveTo( 1220 x1 + _currentX, y1 + _currentY, x2 + _currentX, y2 + _currentY)); 1221 _setCurrentPoint(x2 + _currentX, y2 + _currentY); 1222 } 1223 1224 /// Adds a cubic bezier segment that curves from the current point 1225 /// to the given point (x3,y3), using the control points (x1,y1) and 1226 /// (x2,y2). 1227 void cubicTo( 1228 double x1, double y1, double x2, double y2, double x3, double y3) { 1229 _ensurePathStarted(); 1230 _commands.add(engine.BezierCurveTo(x1, y1, x2, y2, x3, y3)); 1231 _setCurrentPoint(x3, y3); 1232 } 1233 1234 /// Adds a cubic bezier segment that curves from the current point 1235 /// to the point at the offset (x3,y3) from the current point, using 1236 /// the control points at the offsets (x1,y1) and (x2,y2) from the 1237 /// current point. 1238 void relativeCubicTo( 1239 double x1, double y1, double x2, double y2, double x3, double y3) { 1240 _ensurePathStarted(); 1241 _commands.add(engine.BezierCurveTo(x1 + _currentX, y1 + _currentY, 1242 x2 + _currentX, y2 + _currentY, x3 + _currentX, y3 + _currentY)); 1243 _setCurrentPoint(x3 + _currentX, y3 + _currentY); 1244 } 1245 1246 /// Adds a bezier segment that curves from the current point to the 1247 /// given point (x2,y2), using the control points (x1,y1) and the 1248 /// weight w. If the weight is greater than 1, then the curve is a 1249 /// hyperbola; if the weight equals 1, it's a parabola; and if it is 1250 /// less than 1, it is an ellipse. 1251 void conicTo(double x1, double y1, double x2, double y2, double w) { 1252 final List<Offset> quads = 1253 engine.Conic(_currentX, _currentY, x1, y1, x2, y2, w).toQuads(); 1254 final int len = quads.length; 1255 for (int i = 1; i < len; i += 2) { 1256 quadraticBezierTo( 1257 quads[i].dx, quads[i].dy, quads[i + 1].dx, quads[i + 1].dy); 1258 } 1259 } 1260 1261 /// Adds a bezier segment that curves from the current point to the 1262 /// point at the offset (x2,y2) from the current point, using the 1263 /// control point at the offset (x1,y1) from the current point and 1264 /// the weight w. If the weight is greater than 1, then the curve is 1265 /// a hyperbola; if the weight equals 1, it's a parabola; and if it 1266 /// is less than 1, it is an ellipse. 1267 void relativeConicTo(double x1, double y1, double x2, double y2, double w) { 1268 conicTo(_currentX + x1, _currentY + y1, _currentX + x2, _currentY + y2, w); 1269 } 1270 1271 /// If the `forceMoveTo` argument is false, adds a straight line 1272 /// segment and an arc segment. 1273 /// 1274 /// If the `forceMoveTo` argument is true, starts a new subpath 1275 /// consisting of an arc segment. 1276 /// 1277 /// In either case, the arc segment consists of the arc that follows 1278 /// the edge of the oval bounded by the given rectangle, from 1279 /// startAngle radians around the oval up to startAngle + sweepAngle 1280 /// radians around the oval, with zero radians being the point on 1281 /// the right hand side of the oval that crosses the horizontal line 1282 /// that intersects the center of the rectangle and with positive 1283 /// angles going clockwise around the oval. 1284 /// 1285 /// The line segment added if `forceMoveTo` is false starts at the 1286 /// current point and ends at the start of the arc. 1287 void arcTo( 1288 Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) { 1289 assert(engine.rectIsValid(rect)); 1290 final Offset center = rect.center; 1291 final double radiusX = rect.width / 2; 1292 final double radiusY = rect.height / 2; 1293 final double startX = radiusX * math.cos(startAngle) + center.dx; 1294 final double startY = radiusY * math.sin(startAngle) + center.dy; 1295 if (forceMoveTo) { 1296 _openNewSubpath(startX, startY); 1297 } else { 1298 lineTo(startX, startY); 1299 } 1300 _commands.add(engine.Ellipse(center.dx, center.dy, radiusX, radiusY, 0.0, 1301 startAngle, startAngle + sweepAngle, sweepAngle.isNegative)); 1302 1303 _setCurrentPoint(radiusX * math.cos(startAngle + sweepAngle) + center.dx, 1304 radiusY * math.sin(startAngle + sweepAngle) + center.dy); 1305 } 1306 1307 /// Appends up to four conic curves weighted to describe an oval of `radius` 1308 /// and rotated by `rotation`. 1309 /// 1310 /// The first curve begins from the last point in the path and the last ends 1311 /// at `arcEnd`. The curves follow a path in a direction determined by 1312 /// `clockwise` and `largeArc` in such a way that the sweep angle 1313 /// is always less than 360 degrees. 1314 /// 1315 /// A simple line is appended if either either radii are zero or the last 1316 /// point in the path is `arcEnd`. The radii are scaled to fit the last path 1317 /// point if both are greater than zero but too small to describe an arc. 1318 /// 1319 /// See Conversion from endpoint to center parametrization described in 1320 /// https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter 1321 /// as reference for implementation. 1322 void arcToPoint( 1323 Offset arcEnd, { 1324 Radius radius = Radius.zero, 1325 double rotation = 0.0, 1326 bool largeArc = false, 1327 bool clockwise = true, 1328 }) { 1329 assert(engine.offsetIsValid(arcEnd)); 1330 assert(engine.radiusIsValid(radius)); 1331 // _currentX, _currentY are the coordinates of start point on path, 1332 // arcEnd is final point of arc. 1333 // rx,ry are the radii of the eclipse (semi-major/semi-minor axis) 1334 // largeArc is false if arc is spanning less than or equal to 180 degrees. 1335 // clockwise is false if arc sweeps through decreasing angles or true 1336 // if sweeping through increasing angles. 1337 // rotation is the angle from the x-axis of the current coordinate 1338 // system to the x-axis of the eclipse. 1339 1340 double rx = radius.x.abs(); 1341 double ry = radius.y.abs(); 1342 1343 // If the current point and target point for the arc are identical, it 1344 // should be treated as a zero length path. This ensures continuity in 1345 // animations. 1346 final bool isSamePoint = _currentX == arcEnd.dx && _currentY == arcEnd.dy; 1347 1348 // If rx = 0 or ry = 0 then this arc is treated as a straight line segment 1349 // (a "lineto") joining the endpoints. 1350 // http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters 1351 if (isSamePoint || rx.toInt() == 0 || ry.toInt() == 0) { 1352 _commands.add(engine.LineTo(arcEnd.dx, arcEnd.dy)); 1353 _setCurrentPoint(arcEnd.dx, arcEnd.dy); 1354 return; 1355 } 1356 1357 // As an intermediate point to finding center parametrization, place the 1358 // origin on the midpoint between start/end points and rotate to align 1359 // coordinate axis with axes of the ellipse. 1360 final double midPointX = (_currentX - arcEnd.dx) / 2.0; 1361 final double midPointY = (_currentY - arcEnd.dy) / 2.0; 1362 1363 // Convert rotation or radians. 1364 final double xAxisRotation = math.pi * rotation / 180.0; 1365 1366 // Cache cos/sin value. 1367 final double cosXAxisRotation = math.cos(xAxisRotation); 1368 final double sinXAxisRotation = math.sin(xAxisRotation); 1369 1370 // Calculate rotate midpoint as x/yPrime. 1371 final double xPrime = 1372 (cosXAxisRotation * midPointX) + (sinXAxisRotation * midPointY); 1373 final double yPrime = 1374 (-sinXAxisRotation * midPointX) + (cosXAxisRotation * midPointY); 1375 1376 // Check if the radii are big enough to draw the arc, scale radii if not. 1377 // http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii 1378 double rxSquare = rx * rx; 1379 double rySquare = ry * ry; 1380 final double xPrimeSquare = xPrime * xPrime; 1381 final double yPrimeSquare = yPrime * yPrime; 1382 1383 double radiiScale = (xPrimeSquare / rxSquare) + (yPrimeSquare / rySquare); 1384 if (radiiScale > 1) { 1385 radiiScale = math.sqrt(radiiScale); 1386 rx *= radiiScale; 1387 ry *= radiiScale; 1388 rxSquare = rx * rx; 1389 rySquare = ry * ry; 1390 } 1391 1392 // Compute transformed center. eq. 5.2 1393 final double distanceSquare = 1394 (rxSquare * yPrimeSquare) + rySquare * xPrimeSquare; 1395 final double cNumerator = (rxSquare * rySquare) - distanceSquare; 1396 double scaleFactor = math.sqrt(math.max(cNumerator / distanceSquare, 0.0)); 1397 if (largeArc == clockwise) { 1398 scaleFactor = -scaleFactor; 1399 } 1400 // Ready to compute transformed center. 1401 final double cxPrime = scaleFactor * ((rx * yPrime) / ry); 1402 final double cyPrime = scaleFactor * (-(ry * xPrime) / rx); 1403 1404 // Rotate to find actual center. 1405 final double cx = cosXAxisRotation * cxPrime - 1406 sinXAxisRotation * cyPrime + 1407 ((_currentX + arcEnd.dx) / 2.0); 1408 final double cy = sinXAxisRotation * cxPrime + 1409 cosXAxisRotation * cyPrime + 1410 ((_currentY + arcEnd.dy) / 2.0); 1411 1412 // Calculate start angle and sweep. 1413 // Start vector is from midpoint of start/end points to transformed center. 1414 final double startVectorX = (xPrime - cxPrime) / rx; 1415 final double startVectorY = (yPrime - cyPrime) / ry; 1416 1417 final double startAngle = math.atan2(startVectorY, startVectorX); 1418 final double endVectorX = (-xPrime - cxPrime) / rx; 1419 final double endVectorY = (-yPrime - cyPrime) / ry; 1420 double sweepAngle = math.atan2(endVectorY, endVectorX) - startAngle; 1421 1422 if (clockwise && sweepAngle < 0) { 1423 sweepAngle += math.pi * 2.0; 1424 } else if (!clockwise && sweepAngle > 0) { 1425 sweepAngle -= math.pi * 2.0; 1426 } 1427 1428 _commands.add(engine.Ellipse(cx, cy, rx, ry, xAxisRotation, startAngle, 1429 startAngle + sweepAngle, sweepAngle.isNegative)); 1430 1431 _setCurrentPoint(arcEnd.dx, arcEnd.dy); 1432 } 1433 1434 /// Appends up to four conic curves weighted to describe an oval of `radius` 1435 /// and rotated by `rotation`. 1436 /// 1437 /// The last path point is described by (px, py). 1438 /// 1439 /// The first curve begins from the last point in the path and the last ends 1440 /// at `arcEndDelta.dx + px` and `arcEndDelta.dy + py`. The curves follow a 1441 /// path in a direction determined by `clockwise` and `largeArc` 1442 /// in such a way that the sweep angle is always less than 360 degrees. 1443 /// 1444 /// A simple line is appended if either either radii are zero, or, both 1445 /// `arcEndDelta.dx` and `arcEndDelta.dy` are zero. The radii are scaled to 1446 /// fit the last path point if both are greater than zero but too small to 1447 /// describe an arc. 1448 void relativeArcToPoint( 1449 Offset arcEndDelta, { 1450 Radius radius = Radius.zero, 1451 double rotation = 0.0, 1452 bool largeArc = false, 1453 bool clockwise = true, 1454 }) { 1455 assert(engine.offsetIsValid(arcEndDelta)); 1456 assert(engine.radiusIsValid(radius)); 1457 arcToPoint(Offset(_currentX + arcEndDelta.dx, _currentY + arcEndDelta.dy), 1458 radius: radius, 1459 rotation: rotation, 1460 largeArc: largeArc, 1461 clockwise: clockwise); 1462 } 1463 1464 /// Adds a new subpath that consists of four lines that outline the 1465 /// given rectangle. 1466 void addRect(Rect rect) { 1467 assert(engine.rectIsValid(rect)); 1468 _openNewSubpath(rect.left, rect.top); 1469 _commands 1470 .add(engine.RectCommand(rect.left, rect.top, rect.width, rect.height)); 1471 } 1472 1473 /// Adds a new subpath that consists of a curve that forms the 1474 /// ellipse that fills the given rectangle. 1475 /// 1476 /// To add a circle, pass an appropriate rectangle as `oval`. 1477 /// [Rect.fromCircle] can be used to easily describe the circle's center 1478 /// [Offset] and radius. 1479 void addOval(Rect oval) { 1480 assert(engine.rectIsValid(oval)); 1481 final Offset center = oval.center; 1482 final double radiusX = oval.width / 2; 1483 final double radiusY = oval.height / 2; 1484 1485 /// At startAngle = 0, the path will begin at center + cos(0) * radius. 1486 _openNewSubpath(center.dx + radiusX, center.dy); 1487 _commands.add(engine.Ellipse( 1488 center.dx, center.dy, radiusX, radiusY, 0.0, 0.0, 2 * math.pi, false)); 1489 } 1490 1491 /// Adds a new subpath with one arc segment that consists of the arc 1492 /// that follows the edge of the oval bounded by the given 1493 /// rectangle, from startAngle radians around the oval up to 1494 /// startAngle + sweepAngle radians around the oval, with zero 1495 /// radians being the point on the right hand side of the oval that 1496 /// crosses the horizontal line that intersects the center of the 1497 /// rectangle and with positive angles going clockwise around the 1498 /// oval. 1499 void addArc(Rect oval, double startAngle, double sweepAngle) { 1500 assert(engine.rectIsValid(oval)); 1501 final Offset center = oval.center; 1502 final double radiusX = oval.width / 2; 1503 final double radiusY = oval.height / 2; 1504 _openNewSubpath(radiusX * math.cos(startAngle) + center.dx, 1505 radiusY * math.sin(startAngle) + center.dy); 1506 _commands.add(engine.Ellipse(center.dx, center.dy, radiusX, radiusY, 0.0, 1507 startAngle, startAngle + sweepAngle, sweepAngle.isNegative)); 1508 1509 _setCurrentPoint(radiusX * math.cos(startAngle + sweepAngle) + center.dx, 1510 radiusY * math.sin(startAngle + sweepAngle) + center.dy); 1511 } 1512 1513 /// Adds a new subpath with a sequence of line segments that connect the given 1514 /// points. 1515 /// 1516 /// If `close` is true, a final line segment will be added that connects the 1517 /// last point to the first point. 1518 /// 1519 /// The `points` argument is interpreted as offsets from the origin. 1520 void addPolygon(List<Offset> points, bool close) { 1521 assert(points != null); 1522 if (points.isEmpty) { 1523 return; 1524 } 1525 1526 moveTo(points.first.dx, points.first.dy); 1527 for (int i = 1; i < points.length; i++) { 1528 final Offset point = points[i]; 1529 lineTo(point.dx, point.dy); 1530 } 1531 if (close) { 1532 this.close(); 1533 } else { 1534 _setCurrentPoint(points.last.dx, points.last.dy); 1535 } 1536 } 1537 1538 /// Adds a new subpath that consists of the straight lines and 1539 /// curves needed to form the rounded rectangle described by the 1540 /// argument. 1541 void addRRect(RRect rrect) { 1542 assert(engine.rrectIsValid(rrect)); 1543 1544 // Set the current point to the top left corner of the rectangle (the 1545 // point on the top of the rectangle farthest to the left that isn't in 1546 // the rounded corner). 1547 // TODO(het): Is this the current point in Flutter? 1548 _openNewSubpath(rrect.tallMiddleRect.left, rrect.top); 1549 _commands.add(engine.RRectCommand(rrect)); 1550 } 1551 1552 /// Adds a new subpath that consists of the given `path` offset by the given 1553 /// `offset`. 1554 /// 1555 /// If `matrix4` is specified, the path will be transformed by this matrix 1556 /// after the matrix is translated by the given offset. The matrix is a 4x4 1557 /// matrix stored in column major order. 1558 void addPath(Path path, Offset offset, {Float64List matrix4}) { 1559 assert(path != null); // path is checked on the engine side 1560 assert(engine.offsetIsValid(offset)); 1561 if (matrix4 != null) { 1562 assert(engine.matrix4IsValid(matrix4)); 1563 _addPathWithMatrix(path, offset.dx, offset.dy, matrix4); 1564 } else { 1565 _addPath(path, offset.dx, offset.dy); 1566 } 1567 } 1568 1569 void _addPath(Path path, double dx, double dy) { 1570 if (dx == 0.0 && dy == 0.0) { 1571 subpaths.addAll(path.subpaths); 1572 } else { 1573 throw UnimplementedError('Cannot add path with non-zero offset'); 1574 } 1575 } 1576 1577 void _addPathWithMatrix(Path path, double dx, double dy, Float64List matrix) { 1578 throw UnimplementedError('Cannot add path with transform matrix'); 1579 } 1580 1581 /// Adds the given path to this path by extending the current segment of this 1582 /// path with the the first segment of the given path. 1583 /// 1584 /// If `matrix4` is specified, the path will be transformed by this matrix 1585 /// after the matrix is translated by the given `offset`. The matrix is a 4x4 1586 /// matrix stored in column major order. 1587 void extendWithPath(Path path, Offset offset, {Float64List matrix4}) { 1588 assert(path != null); // path is checked on the engine side 1589 assert(engine.offsetIsValid(offset)); 1590 if (matrix4 != null) { 1591 assert(engine.matrix4IsValid(matrix4)); 1592 _extendWithPathAndMatrix(path, offset.dx, offset.dy, matrix4); 1593 } else { 1594 _extendWithPath(path, offset.dx, offset.dy); 1595 } 1596 } 1597 1598 void _extendWithPath(Path path, double dx, double dy) { 1599 if (dx == 0.0 && dy == 0.0) { 1600 assert(path.subpaths.length == 1); 1601 _ensurePathStarted(); 1602 _commands.addAll(path.subpaths.single.commands); 1603 _setCurrentPoint( 1604 path.subpaths.single.currentX, path.subpaths.single.currentY); 1605 } else { 1606 throw UnimplementedError('Cannot extend path with non-zero offset'); 1607 } 1608 } 1609 1610 void _extendWithPathAndMatrix( 1611 Path path, double dx, double dy, Float64List matrix) { 1612 throw UnimplementedError('Cannot extend path with transform matrix'); 1613 } 1614 1615 /// Closes the last subpath, as if a straight line had been drawn 1616 /// from the current point to the first point of the subpath. 1617 void close() { 1618 _ensurePathStarted(); 1619 _commands.add(const engine.CloseCommand()); 1620 _setCurrentPoint(_currentSubpath.startX, _currentSubpath.startY); 1621 } 1622 1623 /// Clears the [Path] object of all subpaths, returning it to the 1624 /// same state it had when it was created. The _current point_ is 1625 /// reset to the origin. 1626 void reset() { 1627 subpaths.clear(); 1628 } 1629 1630 /// Tests to see if the given point is within the path. (That is, whether the 1631 /// point would be in the visible portion of the path if the path was used 1632 /// with [Canvas.clipPath].) 1633 /// 1634 /// The `point` argument is interpreted as an offset from the origin. 1635 /// 1636 /// Returns true if the point is in the path, and false otherwise. 1637 /// 1638 /// Note: Not very efficient, it creates a canvas, plays path and calls 1639 /// Context2D isPointInPath. If performance becomes issue, retaining 1640 /// RawRecordingCanvas can remove create/remove rootElement cost. 1641 bool contains(Offset point) { 1642 assert(engine.offsetIsValid(point)); 1643 final int subPathCount = subpaths.length; 1644 if (subPathCount == 0) { 1645 return false; 1646 } 1647 final double pointX = point.dx; 1648 final double pointY = point.dy; 1649 if (subPathCount == 1) { 1650 // Optimize for rect/roundrect checks. 1651 final engine.Subpath subPath = subpaths[0]; 1652 if (subPath.commands.length == 1) { 1653 final engine.PathCommand cmd = subPath.commands[0]; 1654 if (cmd is engine.RectCommand) { 1655 if (pointY < cmd.y || pointY > (cmd.y + cmd.height)) { 1656 return false; 1657 } 1658 if (pointX < cmd.x || pointX > (cmd.x + cmd.width)) { 1659 return false; 1660 } 1661 return true; 1662 } else if (cmd is engine.RRectCommand) { 1663 final RRect rRect = cmd.rrect; 1664 if (pointY < rRect.top || pointY > rRect.bottom) { 1665 return false; 1666 } 1667 if (pointX < rRect.left || pointX > rRect.right) { 1668 return false; 1669 } 1670 if (pointX < (rRect.left + rRect.tlRadiusX) && 1671 pointY < (rRect.top + rRect.tlRadiusY)) { 1672 // Top left corner 1673 return _ellipseContains( 1674 pointX, 1675 pointY, 1676 rRect.left + rRect.tlRadiusX, 1677 rRect.top + rRect.tlRadiusY, 1678 rRect.tlRadiusX, 1679 rRect.tlRadiusY); 1680 } else if (pointX >= (rRect.right - rRect.trRadiusX) && 1681 pointY < (rRect.top + rRect.trRadiusY)) { 1682 // Top right corner 1683 return _ellipseContains( 1684 pointX, 1685 pointY, 1686 rRect.right - rRect.trRadiusX, 1687 rRect.top + rRect.trRadiusY, 1688 rRect.trRadiusX, 1689 rRect.trRadiusY); 1690 } else if (pointX >= (rRect.right - rRect.brRadiusX) && 1691 pointY >= (rRect.bottom - rRect.brRadiusY)) { 1692 // Bottom right corner 1693 return _ellipseContains( 1694 pointX, 1695 pointY, 1696 rRect.right - rRect.brRadiusX, 1697 rRect.bottom - rRect.brRadiusY, 1698 rRect.trRadiusX, 1699 rRect.trRadiusY); 1700 } else if (pointX < (rRect.left + rRect.blRadiusX) && 1701 pointY >= (rRect.bottom - rRect.blRadiusY)) { 1702 // Bottom left corner 1703 return _ellipseContains( 1704 pointX, 1705 pointY, 1706 rRect.left + rRect.blRadiusX, 1707 rRect.bottom - rRect.blRadiusY, 1708 rRect.trRadiusX, 1709 rRect.trRadiusY); 1710 } 1711 return true; 1712 } 1713 } 1714 } 1715 final Size size = window.physicalSize / window.devicePixelRatio; 1716 _rawRecorder ??= RawRecordingCanvas(size); 1717 // Account for the shift due to padding. 1718 _rawRecorder.translate(-engine.BitmapCanvas.paddingPixels.toDouble(), 1719 -engine.BitmapCanvas.paddingPixels.toDouble()); 1720 _rawRecorder.drawPath( 1721 this, (Paint()..color = const Color(0xFF000000)).webOnlyPaintData); 1722 final bool result = _rawRecorder.ctx.isPointInPath(pointX, pointY); 1723 _rawRecorder.dispose(); 1724 return result; 1725 } 1726 1727 /// Returns a copy of the path with all the segments of every 1728 /// subpath translated by the given offset. 1729 Path shift(Offset offset) { 1730 assert(engine.offsetIsValid(offset)); 1731 final List<engine.Subpath> shiftedSubpaths = <engine.Subpath>[]; 1732 for (final engine.Subpath subpath in subpaths) { 1733 shiftedSubpaths.add(subpath.shift(offset)); 1734 } 1735 return Path._clone(shiftedSubpaths, fillType); 1736 } 1737 1738 /// Returns a copy of the path with all the segments of every 1739 /// subpath transformed by the given matrix. 1740 Path transform(Float64List matrix4) { 1741 assert(engine.matrix4IsValid(matrix4)); 1742 throw UnimplementedError(); 1743 } 1744 1745 /// Computes the bounding rectangle for this path. 1746 /// 1747 /// A path containing only axis-aligned points on the same straight line will 1748 /// have no area, and therefore `Rect.isEmpty` will return true for such a 1749 /// path. Consider checking `rect.width + rect.height > 0.0` instead, or 1750 /// using the [computeMetrics] API to check the path length. 1751 /// 1752 /// For many more elaborate paths, the bounds may be inaccurate. For example, 1753 /// when a path contains a circle, the points used to compute the bounds are 1754 /// the circle's implied control points, which form a square around the 1755 /// circle; if the circle has a transformation applied using [transform] then 1756 /// that square is rotated, and the (axis-aligned, non-rotated) bounding box 1757 /// therefore ends up grossly overestimating the actual area covered by the 1758 /// circle. 1759 // see https://skia.org/user/api/SkPath_Reference#SkPath_getBounds 1760 Rect getBounds() { 1761 // Sufficiently small number for curve eq. 1762 const double epsilon = 0.000000001; 1763 bool ltrbInitialized = false; 1764 double left = 0.0, top = 0.0, right = 0.0, bottom = 0.0; 1765 double curX = 0.0; 1766 double curY = 0.0; 1767 double minX = 0.0, maxX = 0.0, minY = 0.0, maxY = 0.0; 1768 for (engine.Subpath subpath in subpaths) { 1769 for (engine.PathCommand op in subpath.commands) { 1770 bool skipBounds = false; 1771 switch (op.type) { 1772 case engine.PathCommandTypes.moveTo: 1773 final engine.MoveTo cmd = op; 1774 curX = minX = maxX = cmd.x; 1775 curY = minY = maxY = cmd.y; 1776 break; 1777 case engine.PathCommandTypes.lineTo: 1778 final engine.LineTo cmd = op; 1779 curX = minX = maxX = cmd.x; 1780 curY = minY = maxY = cmd.y; 1781 break; 1782 case engine.PathCommandTypes.ellipse: 1783 final engine.Ellipse cmd = op; 1784 // Rotate 4 corners of bounding box. 1785 final double rx = cmd.radiusX; 1786 final double ry = cmd.radiusY; 1787 final double cosVal = math.cos(cmd.rotation); 1788 final double sinVal = math.sin(cmd.rotation); 1789 final double rxCos = rx * cosVal; 1790 final double ryCos = ry * cosVal; 1791 final double rxSin = rx * sinVal; 1792 final double rySin = ry * sinVal; 1793 1794 final double leftDeltaX = rxCos - rySin; 1795 final double rightDeltaX = -rxCos - rySin; 1796 final double topDeltaY = ryCos + rxSin; 1797 final double bottomDeltaY = ryCos - rxSin; 1798 1799 final double centerX = cmd.x; 1800 final double centerY = cmd.y; 1801 1802 double rotatedX = centerX + leftDeltaX; 1803 double rotatedY = centerY + topDeltaY; 1804 minX = maxX = rotatedX; 1805 minY = maxY = rotatedY; 1806 1807 rotatedX = centerX + rightDeltaX; 1808 rotatedY = centerY + bottomDeltaY; 1809 minX = math.min(minX, rotatedX); 1810 maxX = math.max(maxX, rotatedX); 1811 minY = math.min(minY, rotatedY); 1812 maxY = math.max(maxY, rotatedY); 1813 1814 rotatedX = centerX - leftDeltaX; 1815 rotatedY = centerY - topDeltaY; 1816 minX = math.min(minX, rotatedX); 1817 maxX = math.max(maxX, rotatedX); 1818 minY = math.min(minY, rotatedY); 1819 maxY = math.max(maxY, rotatedY); 1820 1821 rotatedX = centerX - rightDeltaX; 1822 rotatedY = centerY - bottomDeltaY; 1823 minX = math.min(minX, rotatedX); 1824 maxX = math.max(maxX, rotatedX); 1825 minY = math.min(minY, rotatedY); 1826 maxY = math.max(maxY, rotatedY); 1827 1828 curX = centerX + cmd.radiusX; 1829 curY = centerY; 1830 break; 1831 case engine.PathCommandTypes.quadraticCurveTo: 1832 final engine.QuadraticCurveTo cmd = op; 1833 final double x1 = curX; 1834 final double y1 = curY; 1835 final double cpX = cmd.x1; 1836 final double cpY = cmd.y1; 1837 final double x2 = cmd.x2; 1838 final double y2 = cmd.y2; 1839 1840 minX = math.min(x1, x2); 1841 minY = math.min(y1, y2); 1842 maxX = math.max(x1, x2); 1843 maxY = math.max(y1, y2); 1844 1845 // Curve equation : (1-t)(1-t)P1 + 2t(1-t)CP + t*t*P2. 1846 // At extrema's derivative = 0. 1847 // Solve for 1848 // -2x1+2tx1 + 2cpX + 4tcpX + 2tx2 = 0 1849 // -2x1 + 2cpX +2t(x1 + 2cpX + x2) = 0 1850 // t = (x1 - cpX) / (x1 - 2cpX + x2) 1851 1852 double denom = x1 - (2 * cpX) + x2; 1853 if (denom.abs() > epsilon) { 1854 final num t1 = (x1 - cpX) / denom; 1855 if ((t1 >= 0) && (t1 <= 1.0)) { 1856 // Solve (x,y) for curve at t = tx to find extrema 1857 final num tprime = 1.0 - t1; 1858 final num extremaX = (tprime * tprime * x1) + 1859 (2 * t1 * tprime * cpX) + 1860 (t1 * t1 * x2); 1861 final num extremaY = (tprime * tprime * y1) + 1862 (2 * t1 * tprime * cpY) + 1863 (t1 * t1 * y2); 1864 // Expand bounds. 1865 minX = math.min(minX, extremaX); 1866 maxX = math.max(maxX, extremaX); 1867 minY = math.min(minY, extremaY); 1868 maxY = math.max(maxY, extremaY); 1869 } 1870 } 1871 // Now calculate dy/dt = 0 1872 denom = y1 - (2 * cpY) + y2; 1873 if (denom.abs() > epsilon) { 1874 final num t2 = (y1 - cpY) / denom; 1875 if ((t2 >= 0) && (t2 <= 1.0)) { 1876 final num tprime2 = 1.0 - t2; 1877 final num extrema2X = (tprime2 * tprime2 * x1) + 1878 (2 * t2 * tprime2 * cpX) + 1879 (t2 * t2 * x2); 1880 final num extrema2Y = (tprime2 * tprime2 * y1) + 1881 (2 * t2 * tprime2 * cpY) + 1882 (t2 * t2 * y2); 1883 // Expand bounds. 1884 minX = math.min(minX, extrema2X); 1885 maxX = math.max(maxX, extrema2X); 1886 minY = math.min(minY, extrema2Y); 1887 maxY = math.max(maxY, extrema2Y); 1888 } 1889 } 1890 curX = x2; 1891 curY = y2; 1892 break; 1893 case engine.PathCommandTypes.bezierCurveTo: 1894 final engine.BezierCurveTo cmd = op; 1895 final double startX = curX; 1896 final double startY = curY; 1897 final double cpX1 = cmd.x1; 1898 final double cpY1 = cmd.y1; 1899 final double cpX2 = cmd.x2; 1900 final double cpY2 = cmd.y2; 1901 final double endX = cmd.x3; 1902 final double endY = cmd.y3; 1903 // Bounding box is defined by all points on the curve where 1904 // monotonicity changes. 1905 minX = math.min(startX, endX); 1906 minY = math.min(startY, endY); 1907 maxX = math.max(startX, endX); 1908 maxY = math.max(startY, endY); 1909 1910 double extremaX; 1911 double extremaY; 1912 double a, b, c; 1913 1914 // Check for simple case of strong ordering before calculating 1915 // extrema 1916 if (!(((startX < cpX1) && (cpX1 < cpX2) && (cpX2 < endX)) || 1917 ((startX > cpX1) && (cpX1 > cpX2) && (cpX2 > endX)))) { 1918 // The extrema point is dx/dt B(t) = 0 1919 // The derivative of B(t) for cubic bezier is a quadratic equation 1920 // with multiple roots 1921 // B'(t) = a*t*t + b*t + c*t 1922 a = -startX + (3 * (cpX1 - cpX2)) + endX; 1923 b = 2 * (startX - (2 * cpX1) + cpX2); 1924 c = -startX + cpX1; 1925 1926 // Now find roots for quadratic equation with known coefficients 1927 // a,b,c 1928 // The roots are (-b+-sqrt(b*b-4*a*c)) / 2a 1929 num s = (b * b) - (4 * a * c); 1930 // If s is negative, we have no real roots 1931 if ((s >= 0.0) && (a.abs() > epsilon)) { 1932 if (s == 0.0) { 1933 // we have only 1 root 1934 final num t = -b / (2 * a); 1935 final num tprime = 1.0 - t; 1936 if ((t >= 0.0) && (t <= 1.0)) { 1937 extremaX = ((tprime * tprime * tprime) * startX) + 1938 ((3 * tprime * tprime * t) * cpX1) + 1939 ((3 * tprime * t * t) * cpX2) + 1940 (t * t * t * endX); 1941 minX = math.min(extremaX, minX); 1942 maxX = math.max(extremaX, maxX); 1943 } 1944 } else { 1945 // we have 2 roots 1946 s = math.sqrt(s); 1947 num t = (-b - s) / (2 * a); 1948 num tprime = 1.0 - t; 1949 if ((t >= 0.0) && (t <= 1.0)) { 1950 extremaX = ((tprime * tprime * tprime) * startX) + 1951 ((3 * tprime * tprime * t) * cpX1) + 1952 ((3 * tprime * t * t) * cpX2) + 1953 (t * t * t * endX); 1954 minX = math.min(extremaX, minX); 1955 maxX = math.max(extremaX, maxX); 1956 } 1957 // check 2nd root 1958 t = (-b + s) / (2 * a); 1959 tprime = 1.0 - t; 1960 if ((t >= 0.0) && (t <= 1.0)) { 1961 extremaX = ((tprime * tprime * tprime) * startX) + 1962 ((3 * tprime * tprime * t) * cpX1) + 1963 ((3 * tprime * t * t) * cpX2) + 1964 (t * t * t * endX); 1965 1966 minX = math.min(extremaX, minX); 1967 maxX = math.max(extremaX, maxX); 1968 } 1969 } 1970 } 1971 } 1972 1973 // Now calc extremes for dy/dt = 0 just like above 1974 if (!(((startY < cpY1) && (cpY1 < cpY2) && (cpY2 < endY)) || 1975 ((startY > cpY1) && (cpY1 > cpY2) && (cpY2 > endY)))) { 1976 // The extrema point is dy/dt B(t) = 0 1977 // The derivative of B(t) for cubic bezier is a quadratic equation 1978 // with multiple roots 1979 // B'(t) = a*t*t + b*t + c*t 1980 a = -startY + (3 * (cpY1 - cpY2)) + endY; 1981 b = 2 * (startY - (2 * cpY1) + cpY2); 1982 c = -startY + cpY1; 1983 1984 // Now find roots for quadratic equation with known coefficients 1985 // a,b,c 1986 // The roots are (-b+-sqrt(b*b-4*a*c)) / 2a 1987 num s = (b * b) - (4 * a * c); 1988 // If s is negative, we have no real roots 1989 if ((s >= 0.0) && (a.abs() > epsilon)) { 1990 if (s == 0.0) { 1991 // we have only 1 root 1992 final num t = -b / (2 * a); 1993 final num tprime = 1.0 - t; 1994 if ((t >= 0.0) && (t <= 1.0)) { 1995 extremaY = ((tprime * tprime * tprime) * startY) + 1996 ((3 * tprime * tprime * t) * cpY1) + 1997 ((3 * tprime * t * t) * cpY2) + 1998 (t * t * t * endY); 1999 minY = math.min(extremaY, minY); 2000 maxY = math.max(extremaY, maxY); 2001 } 2002 } else { 2003 // we have 2 roots 2004 s = math.sqrt(s); 2005 final num t = (-b - s) / (2 * a); 2006 final num tprime = 1.0 - t; 2007 if ((t >= 0.0) && (t <= 1.0)) { 2008 extremaY = ((tprime * tprime * tprime) * startY) + 2009 ((3 * tprime * tprime * t) * cpY1) + 2010 ((3 * tprime * t * t) * cpY2) + 2011 (t * t * t * endY); 2012 minY = math.min(extremaY, minY); 2013 maxY = math.max(extremaY, maxY); 2014 } 2015 // check 2nd root 2016 final num t2 = (-b + s) / (2 * a); 2017 final num tprime2 = 1.0 - t2; 2018 if ((t2 >= 0.0) && (t2 <= 1.0)) { 2019 extremaY = ((tprime2 * tprime2 * tprime2) * startY) + 2020 ((3 * tprime2 * tprime2 * t2) * cpY1) + 2021 ((3 * tprime2 * t2 * t2) * cpY2) + 2022 (t2 * t2 * t2 * endY); 2023 minY = math.min(extremaY, minY); 2024 maxY = math.max(extremaY, maxY); 2025 } 2026 } 2027 } 2028 } 2029 break; 2030 case engine.PathCommandTypes.rect: 2031 final engine.RectCommand cmd = op; 2032 left = cmd.x; 2033 double width = cmd.width; 2034 if (cmd.width < 0) { 2035 left -= width; 2036 width = -width; 2037 } 2038 double top = cmd.y; 2039 double height = cmd.height; 2040 if (cmd.height < 0) { 2041 top -= height; 2042 height = -height; 2043 } 2044 curX = minX = left; 2045 maxX = left + width; 2046 curY = minY = top; 2047 maxY = top + height; 2048 break; 2049 case engine.PathCommandTypes.rRect: 2050 final engine.RRectCommand cmd = op; 2051 final RRect rRect = cmd.rrect; 2052 curX = minX = rRect.left; 2053 maxX = rRect.left + rRect.width; 2054 curY = minY = rRect.top; 2055 maxY = rRect.top + rRect.height; 2056 break; 2057 case engine.PathCommandTypes.close: 2058 default: 2059 skipBounds = false; 2060 break; 2061 } 2062 if (!skipBounds) { 2063 if (!ltrbInitialized) { 2064 left = minX; 2065 right = maxX; 2066 top = minY; 2067 bottom = maxY; 2068 ltrbInitialized = true; 2069 } else { 2070 left = math.min(left, minX); 2071 right = math.max(right, maxX); 2072 top = math.min(top, minY); 2073 bottom = math.max(bottom, maxY); 2074 } 2075 } 2076 } 2077 } 2078 return ltrbInitialized 2079 ? Rect.fromLTRB(left, top, right, bottom) 2080 : Rect.zero; 2081 } 2082 2083 /// Combines the two paths according to the manner specified by the given 2084 /// `operation`. 2085 /// 2086 /// The resulting path will be constructed from non-overlapping contours. The 2087 /// curve order is reduced where possible so that cubics may be turned into 2088 /// quadratics, and quadratics maybe turned into lines. 2089 static Path combine(PathOperation operation, Path path1, Path path2) { 2090 assert(path1 != null); 2091 assert(path2 != null); 2092 throw UnimplementedError(); 2093 } 2094 2095 /// Creates a [PathMetrics] object for this path. 2096 /// 2097 /// If `forceClosed` is set to true, the contours of the path will be measured 2098 /// as if they had been closed, even if they were not explicitly closed. 2099 PathMetrics computeMetrics({bool forceClosed = false}) { 2100 return PathMetrics._(this, forceClosed); 2101 } 2102 2103 /// Detects if path is rounded rectangle and returns rounded rectangle or 2104 /// null. 2105 /// 2106 /// Used for web optimization of physical shape represented as 2107 /// a persistent div. 2108 RRect get webOnlyPathAsRoundedRect { 2109 if (subpaths.length != 1) { 2110 return null; 2111 } 2112 final engine.Subpath subPath = subpaths[0]; 2113 if (subPath.commands.length != 1) { 2114 return null; 2115 } 2116 final engine.PathCommand command = subPath.commands[0]; 2117 return (command is engine.RRectCommand) ? command.rrect : null; 2118 } 2119 2120 /// Detects if path is simple rectangle and returns rectangle or null. 2121 /// 2122 /// Used for web optimization of physical shape represented as 2123 /// a persistent div. 2124 Rect get webOnlyPathAsRect { 2125 if (subpaths.length != 1) { 2126 return null; 2127 } 2128 final engine.Subpath subPath = subpaths[0]; 2129 if (subPath.commands.length != 1) { 2130 return null; 2131 } 2132 final engine.PathCommand command = subPath.commands[0]; 2133 return (command is engine.RectCommand) 2134 ? Rect.fromLTWH(command.x, command.y, command.width, command.height) 2135 : null; 2136 } 2137 2138 /// Detects if path is simple oval and returns [engine.Ellipse] or null. 2139 /// 2140 /// Used for web optimization of physical shape represented as 2141 /// a persistent div. 2142 engine.Ellipse get webOnlyPathAsCircle { 2143 if (subpaths.length != 1) { 2144 return null; 2145 } 2146 final engine.Subpath subPath = subpaths[0]; 2147 if (subPath.commands.length != 1) { 2148 return null; 2149 } 2150 final engine.PathCommand command = subPath.commands[0]; 2151 if (command is engine.Ellipse) { 2152 final engine.Ellipse ellipse = command; 2153 if ((ellipse.endAngle - ellipse.startAngle) % (2 * math.pi) == 0.0) { 2154 return ellipse; 2155 } 2156 } 2157 return null; 2158 } 2159 2160 /// Serializes this path to a value that's sent to a CSS custom painter for 2161 /// painting. 2162 List<dynamic> webOnlySerializeToCssPaint() { 2163 final List<dynamic> serializedSubpaths = <dynamic>[]; 2164 for (int i = 0; i < subpaths.length; i++) { 2165 serializedSubpaths.add(subpaths[i].serializeToCssPaint()); 2166 } 2167 return serializedSubpaths; 2168 } 2169 2170 @override 2171 String toString() { 2172 if (engine.assertionsEnabled) { 2173 return 'Path(${subpaths.join(', ')})'; 2174 } else { 2175 return super.toString(); 2176 } 2177 } 2178} 2179 2180/// An iterable collection of [PathMetric] objects describing a [Path]. 2181/// 2182/// A [PathMetrics] object is created by using the [Path.computeMetrics] method, 2183/// and represents the path as it stood at the time of the call. Subsequent 2184/// modifications of the path do not affect the [PathMetrics] object. 2185/// 2186/// Each path metric corresponds to a segment, or contour, of a path. 2187/// 2188/// For example, a path consisting of a [Path.lineTo], a [Path.moveTo], and 2189/// another [Path.lineTo] will contain two contours and thus be represented by 2190/// two [PathMetric] objects. 2191/// 2192/// When iterating across a [PathMetrics]' contours, the [PathMetric] objects 2193/// are only valid until the next one is obtained. 2194class PathMetrics extends IterableBase<PathMetric> { 2195 PathMetrics._(Path path, bool forceClosed) 2196 : _iterator = PathMetricIterator._(PathMetric._(path, forceClosed)); 2197 2198 final Iterator<PathMetric> _iterator; 2199 2200 @override 2201 Iterator<PathMetric> get iterator => _iterator; 2202} 2203 2204/// Tracks iteration from one segment of a path to the next for measurement. 2205class PathMetricIterator implements Iterator<PathMetric> { 2206 PathMetricIterator._(this._pathMetric); 2207 2208 PathMetric _pathMetric; 2209 bool _firstTime = true; 2210 2211 @override 2212 PathMetric get current => _firstTime ? null : _pathMetric; 2213 2214 @override 2215 bool moveNext() { 2216 // PathMetric isn't a normal iterable - it's already initialized to its 2217 // first Path. Should only call _moveNext when done with the first one. 2218 if (_firstTime == true) { 2219 _firstTime = false; 2220 return true; 2221 } else if (_pathMetric?._moveNext() == true) { 2222 return true; 2223 } 2224 _pathMetric = null; 2225 return false; 2226 } 2227} 2228 2229/// Utilities for measuring a [Path] and extracting subpaths. 2230/// 2231/// Iterate over the object returned by [Path.computeMetrics] to obtain 2232/// [PathMetric] objects. 2233/// 2234/// Once created, metrics will only be valid while the iterator is at the given 2235/// contour. When the next contour's [PathMetric] is obtained, this object 2236/// becomes invalid. 2237class PathMetric { 2238 final Path path; 2239 final bool forceClosed; 2240 2241 /// Create a new empty [Path] object. 2242 PathMetric._(this.path, this.forceClosed); 2243 2244 /// Return the total length of the current contour. 2245 double get length => throw UnimplementedError(); 2246 2247 /// Computes the position of hte current contour at the given offset, and the 2248 /// angle of the path at that point. 2249 /// 2250 /// For example, calling this method with a distance of 1.41 for a line from 2251 /// 0.0,0.0 to 2.0,2.0 would give a point 1.0,1.0 and the angle 45 degrees 2252 /// (but in radians). 2253 /// 2254 /// Returns null if the contour has zero [length]. 2255 /// 2256 /// The distance is clamped to the [length] of the current contour. 2257 Tangent getTangentForOffset(double distance) { 2258 final Float32List posTan = _getPosTan(distance); 2259 // first entry == 0 indicates that Skia returned false 2260 if (posTan[0] == 0.0) { 2261 return null; 2262 } else { 2263 return Tangent( 2264 Offset(posTan[1], posTan[2]), Offset(posTan[3], posTan[4])); 2265 } 2266 } 2267 2268 Float32List _getPosTan(double distance) => throw UnimplementedError(); 2269 2270 /// Given a start and stop distance, return the intervening segment(s). 2271 /// 2272 /// `start` and `end` are pinned to legal values (0..[length]) 2273 /// Returns null if the segment is 0 length or `start` > `stop`. 2274 /// Begin the segment with a moveTo if `startWithMoveTo` is true. 2275 Path extractPath(double start, double end, {bool startWithMoveTo = true}) => 2276 throw UnimplementedError(); 2277 2278 /// Whether the contour is closed. 2279 /// 2280 /// Returns true if the contour ends with a call to [Path.close] (which may 2281 /// have been implied when using [Path.addRect]) or if `forceClosed` was 2282 /// specified as true in the call to [Path.computeMetrics]. Returns false 2283 /// otherwise. 2284 bool get isClosed => throw UnimplementedError(); 2285 2286 // Move to the next contour in the path. 2287 // 2288 // A path can have a next contour if [Path.moveTo] was called after drawing 2289 // began. Return true if one exists, or false. 2290 // 2291 // This is not exactly congruent with a regular [Iterator.moveNext]. 2292 // Typically, [Iterator.moveNext] should be called before accessing the 2293 // [Iterator.current]. In this case, the [PathMetric] is valid before 2294 // calling `_moveNext` - `_moveNext` should be called after the first 2295 // iteration is done instead of before. 2296 bool _moveNext() => throw UnimplementedError(); 2297 2298 @override 2299 String toString() => 'PathMetric'; 2300} 2301 2302/// The geometric description of a tangent: the angle at a point. 2303/// 2304/// See also: 2305/// * [PathMetric.getTangentForOffset], which returns the tangent of an offset 2306/// along a path. 2307class Tangent { 2308 /// Creates a [Tangent] with the given values. 2309 /// 2310 /// The arguments must not be null. 2311 const Tangent(this.position, this.vector) 2312 : assert(position != null), 2313 assert(vector != null); 2314 2315 /// Creates a [Tangent] based on the angle rather than the vector. 2316 /// 2317 /// The [vector] is computed to be the unit vector at the given angle, 2318 /// interpreted as clockwise radians from the x axis. 2319 factory Tangent.fromAngle(Offset position, double angle) { 2320 return Tangent(position, Offset(math.cos(angle), math.sin(angle))); 2321 } 2322 2323 /// Position of the tangent. 2324 /// 2325 /// When used with [PathMetric.getTangentForOffset], this represents the 2326 /// precise position that the given offset along the path corresponds to. 2327 final Offset position; 2328 2329 /// The vector of the curve at [position]. 2330 /// 2331 /// When used with [PathMetric.getTangentForOffset], this is the vector of the 2332 /// curve that is at the given offset along the path (i.e. the direction of 2333 /// the curve at [position]). 2334 final Offset vector; 2335 2336 /// The direction of the curve at [position]. 2337 /// 2338 /// When used with [PathMetric.getTangentForOffset], this is the angle of the 2339 /// curve that is the given offset along the path (i.e. the direction of the 2340 /// curve at [position]). 2341 /// 2342 /// This value is in radians, with 0.0 meaning pointing along the x axis in 2343 /// the positive x-axis direction, positive numbers pointing downward toward 2344 /// the negative y-axis, i.e. in a clockwise direction, and negative numbers 2345 /// pointing upward toward the positive y-axis, i.e. in a counter-clockwise 2346 /// direction. 2347 // flip the sign to be consistent with [Path.arcTo]'s `sweepAngle` 2348 double get angle => -math.atan2(vector.dy, vector.dx); 2349} 2350 2351class RawRecordingCanvas extends engine.BitmapCanvas 2352 implements PictureRecorder { 2353 RawRecordingCanvas(Size size) : super(Offset.zero & size); 2354 2355 @override 2356 void dispose() { 2357 clear(); 2358 } 2359 2360 @override 2361 engine.RecordingCanvas beginRecording(Rect bounds) => 2362 throw UnsupportedError(''); 2363 @override 2364 Picture endRecording() => throw UnsupportedError(''); 2365 2366 @override 2367 engine.RecordingCanvas _canvas; 2368 2369 @override 2370 bool _isRecording = true; 2371 2372 @override 2373 bool get isRecording => true; 2374 2375 @override 2376 Rect cullRect; 2377} 2378 2379// Returns true if point is inside ellipse. 2380bool _ellipseContains(double px, double py, double centerX, double centerY, 2381 double radiusX, double radiusY) { 2382 final double dx = px - centerX; 2383 final double dy = py - centerY; 2384 return ((dx * dx) / (radiusX * radiusX)) + ((dy * dy) / (radiusY * radiusY)) < 2385 1.0; 2386} 2387