1 /** 2 2D software renderer. 3 See an example of a Canvas-enabled UIElement in: 4 `dplug.flatwidgets.windowresizer.UIWindowResizer` 5 6 This is a DPlug-specific rework of dg2d by Cerjones. 7 https://github.com/cerjones/dg2d 8 - removal of truetype functionnality (since covered by dplug:graphics) 9 - nothrow @nogc 10 - rework of the Canvas itself, to resemble more the HTML5 Canvas API 11 - Blitter delegate made explicit with a userData pointer 12 - added html color parsing 13 - no alignment requirements 14 - clipping is done with the ImageRef input 15 - added a few blendmodes 16 However a failure of this fork is that for transforms and stroke() support 17 you do need path abstraction in the end. 18 19 dplug:canvas is pretty fast and writes 4 pixels at once. 20 21 Bug: you can't use it on a widget that is full-size in your plugin. 22 23 Copyright: Copyright Chris Jones 2020. 24 Copyright: Copyright Guillaume Piolat 2020. 25 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 26 */ 27 module dplug.canvas; 28 29 import std.math: cos, sin, tan, PI; 30 31 import dplug.core.vec; 32 import dplug.core.nogc; 33 import dplug.core.math; 34 35 import dplug.graphics.color; 36 import dplug.graphics.image; 37 38 import dplug.canvas.htmlcolors; 39 import dplug.canvas.gradient; 40 import dplug.canvas.colorblit; 41 import dplug.canvas.linearblit; 42 import dplug.canvas.ellipticalblit; 43 import dplug.canvas.rasterizer; 44 45 46 // dplug:canvas whole public API should live here. 47 48 public import dplug.math.vector; 49 public import dplug.math.box; 50 51 /// `dplug:canvas` operates on RGBA 8-bit buffers. 52 alias ImageDest = ImageRef!RGBA; 53 54 /// How to fill pixels. 55 enum FillRule 56 { 57 /// Fill pixels whose scanline intersects a non-zero number of edges. 58 nonZero, 59 60 /// Fill pixels whose scanline intersects an odd number of edges. 61 evenOdd 62 } 63 64 /// How to composite against background (background is always considered opaque in dplug:canvas). 65 enum CompositeOperation 66 { 67 /// (default) Fastest mode, blend source over the background. 68 /// Note that this goes considerable faster than other blend modes. 69 /// This is because this mode skips holes instead of compositing it, 70 /// And traverse pixels only once. 71 sourceOver, 72 73 /// Add source color (weighted by alpha) to background. 74 add, 75 lighter = add,///ditto 76 77 /// Subtract source color (weighted by alpha) from background. 78 subtract, 79 80 /// Choose maximum of color values. Do not mistake for "lighter" which is an add, not a max. 81 lighten, 82 83 /// Choose minimum of color values. 84 darken, 85 } 86 87 88 /// A 2D Canvas able to render complex pathes into an `ImageRef!RGBA` buffer. 89 /// `Canvas` tries to follow loosely the HTML 5 Canvas API. 90 /// 91 /// See_also: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement 92 /// 93 /// Important: 94 /// * Only works on RGBA output. 95 /// * There need at least 12 extra bytes between lines (`trailingSamples = 3` in `OwnedImage`). 96 /// You can use OwnedImage to have that guarantee. See https://github.com/AuburnSounds/Dplug/issues/563 97 /// For now, avoid full-UI controls that use a Canvas. 98 struct Canvas 99 { 100 public: 101 nothrow: 102 @nogc: 103 104 /// Initialize the Canvas object with this target. 105 void initialize(ImageRef!RGBA imageDest) 106 { 107 _imageDest = imageDest; 108 _gradientUsed = 0; 109 fillStyle(RGBA(0, 0, 0, 255)); 110 111 int xmaxRounded4Up = (imageDest.w + 3) & 0xfffffffc; 112 assert((xmaxRounded4Up & 3) == 0); 113 114 // This is a limitation of dplug:canvas 115 // Because this rasterizer writes to 4 pixels at once at all times, 116 // there need up to 3 extra samples (12 bytes) between lines. 117 // You can use OwnedImage to have that guarantee. 118 // Or you can avoid full-UI controls that use a Canvas. 119 assert(xmaxRounded4Up*4 <= imageDest.pitch); 120 121 _stateStack.resize(1); 122 _stateStack[0].transform = Transform2D.identity(); 123 _stateStack[0].fillRule = FillRule.nonZero; 124 _stateStack[0].compositeOp = CompositeOperation.sourceOver; 125 } 126 127 ~this() 128 { 129 // delete all created gradients 130 131 foreach(gradient; _gradients[]) 132 { 133 destroyFree(gradient); 134 } 135 } 136 137 @disable this(this); 138 139 /// Set the fill style. The fill style can be a plain color fill, a `CanvasGradient`, 140 /// or an HTML-compatible text string. 141 void fillStyle(RGBA color) 142 { 143 uint color_as_uint = *cast(uint*)&color; 144 _plainColorBlit.init(color_as_uint); 145 _currentBlitter.userData = &_plainColorBlit; 146 _blitType = BlitType.color; 147 } 148 ///ditto 149 void fillStyle(const(char)[] htmlColorString) 150 { 151 string error; 152 RGBA rgba; 153 if (parseHTMLColor(htmlColorString, rgba, error)) 154 { 155 fillStyle(rgba); 156 } 157 else 158 assert(false); 159 } 160 ///ditto 161 void fillStyle(CanvasGradient gradient) 162 { 163 final switch(gradient.type) 164 { 165 case CanvasGradient.Type.linear: 166 _linearGradientBlit.init(gradient._gradient, 167 gradient.x0, gradient.y0, gradient.x1, gradient.y1); 168 _currentBlitter.userData = &_linearGradientBlit; 169 _blitType = BlitType.linear; 170 break; 171 172 case CanvasGradient.Type.elliptical: 173 _ellipticalGradientBlit.init(gradient._gradient, 174 gradient.x0, gradient.y0, 175 gradient.x1, gradient.y1, gradient.r2); 176 _currentBlitter.userData = &_ellipticalGradientBlit; 177 _blitType = BlitType.elliptical; 178 break; 179 180 case CanvasGradient.Type.radial: 181 case CanvasGradient.Type.angular: 182 assert(false); // not implemented yet 183 } 184 } 185 186 /// Set current fill rule. 187 void fillRule(FillRule rule) 188 { 189 _stateStack[$-1].fillRule = rule; 190 } 191 /// Get current fill rule. 192 FillRule fillRule() 193 { 194 return _stateStack[$-1].fillRule; 195 } 196 197 ///Set global composite operation. 198 void globalCompositeOperation(CompositeOperation op) 199 { 200 _stateStack[$-1].compositeOp = op; 201 } 202 /// Get global composite operation. 203 CompositeOperation globalCompositeOperation() 204 { 205 return _stateStack[$-1].compositeOp; 206 } 207 208 209 // <PATH> functions 210 211 /// Starts a new path by emptying the list of sub-paths. Call this method when you want to create a new path. 212 void beginPath() 213 { 214 int left = 0; 215 int top = 0; 216 int right = _imageDest.w; 217 int bottom = _imageDest.h; 218 _rasterizer.initialise(left, top, right, bottom); 219 } 220 221 /// Adds a straight line to the path, going to the start of the current sub-path. 222 void closePath() 223 { 224 _rasterizer.closePath(); 225 } 226 227 /// Moves the starting point of a new sub-path to the (x, y) coordinates. 228 void moveTo(float x, float y) 229 { 230 vec2f pt = transformPoint(x, y); 231 _rasterizer.moveTo(pt.x, pt.y); 232 } 233 ///ditto 234 void moveTo(vec2f point) 235 { 236 moveTo(point.x, point.y); 237 } 238 239 /// Connects the last point in the current sub-path to the specified (x, y) coordinates with a straight line. 240 /// If several points are provided, it is equivalent to consecutive single-point `lineTo` calls. 241 void lineTo(float x, float y) 242 { 243 vec2f pt = transformPoint(x, y); 244 _rasterizer.lineTo(pt.x, pt.y); 245 } 246 ///ditto 247 void lineTo(vec2f point) 248 { 249 lineTo(point.x, point.y); 250 } 251 ///ditto 252 void lineTo(vec2f[] points...) // an helper for chaining lineTo calls. 253 { 254 Transform2D M = currentTransform(); 255 foreach(pt; points) 256 { 257 float fx = pt.x * M.a + pt.y * M.b + M.c; 258 float fy = pt.x * M.d + pt.y * M.e + M.f; 259 _rasterizer.lineTo(fx, fy); 260 } 261 } 262 263 /// Adds a cubic Bezier curve to the current path. 264 void bezierCurveTo(float cp1x, float cp1y, float cp2x, float cp2y, float x, float y) 265 { 266 vec2f cp1 = transformPoint(cp1x, cp1y); 267 vec2f cp2 = transformPoint(cp2x, cp2y); 268 vec2f pt = transformPoint(x, y); 269 _rasterizer.cubicTo(cp1.x, cp1.y, cp2.x, cp2.y, pt.x, pt.y); 270 } 271 ///ditto 272 void bezierCurveTo(vec2f controlPoint1, vec2f controlPoint2, vec2f dest) 273 { 274 bezierCurveTo(controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, dest.x, dest.y); 275 } 276 277 /// Adds a quadratic Bézier curve to the current path. 278 void quadraticCurveTo(float cpx, float cpy, float x, float y) 279 { 280 vec2f cp = transformPoint(cpx, cpy); 281 vec2f pt = transformPoint(x, y); 282 _rasterizer.quadTo(cp.x, cp.y, pt.x, pt.y); 283 } 284 ///ditto 285 void quadraticCurveTo(vec2f controlPoint, vec2f dest) 286 { 287 quadraticCurveTo(controlPoint.x, controlPoint.y, dest.x, dest.y); 288 } 289 290 /// Add a rect to the current path. 291 void rect(float x, float y, float width, float height) 292 { 293 moveTo(x, y); 294 lineTo(x + width, y); 295 lineTo(x + width, y + height); 296 lineTo(x, y + height); 297 lineTo(x, y); 298 } 299 ///ditto 300 void rect(vec2f topLeftPoint, vec2f dimension) 301 { 302 rect(topLeftPoint.x, topLeftPoint.y, dimension.x, dimension.y); 303 } 304 ///ditto 305 void rect(box2f rectangle) 306 { 307 rect(rectangle.min.x, rectangle.min.y, rectangle.width, rectangle.height); 308 } 309 ///ditto 310 void rect(box2i rectangle) 311 { 312 rect(rectangle.min.x, rectangle.min.y, rectangle.width, rectangle.height); 313 } 314 315 /// Adds an arc to the current path (used to create circles, or parts of circles). 316 void arc(float x, float y, float radius, float startAngle, float endAngle, bool anticlockwise = false) 317 { 318 assert(radius >= 0); 319 320 // See https://github.com/AuburnSounds/Dplug/issues/468 321 // for the complexities of startAngle, endAngle, and clockwise things 322 323 // "If anticlockwise is false and endAngle-startAngle is equal to or 324 // greater than 2π, or, if anticlockwise is true and startAngle-endAngle 325 // is equal to or greater than 2π, then the arc is the whole circumference 326 // of this ellipse, and the point at startAngle along this circle's 327 // circumference, measured in radians clockwise from the ellipse's semi-major 328 // axis, acts as both the start point and the end point." 329 330 // "Otherwise, the points at startAngle and endAngle along this circle's 331 // circumference, measured in radians clockwise from the ellipse's 332 // semi-major axis, are the start and end points respectively, and 333 // the arc is the path along the circumference of this ellipse from 334 // the start point to the end point, going anti-clockwise if 335 // anticlockwise is true, and clockwise otherwise. Since the points 336 // are on the ellipse, as opposed to being simply angles from zero, 337 // the arc can never cover an angle greater than 2π radians. 338 if (!anticlockwise) 339 { 340 float endMinusStart = endAngle - startAngle; 341 if (endMinusStart >= 2 * PI) 342 endMinusStart = 2 * PI; 343 else 344 { 345 endMinusStart = normalizePhase(endMinusStart); 346 if (endMinusStart < 0) endMinusStart += 2 * PI; 347 } 348 349 // Modify endAngle so that startAngle <= endAngle <= startAngle + 2 * PI 350 endAngle = startAngle + endMinusStart; 351 assert(endAngle >= startAngle); 352 } 353 else 354 { 355 float endMinusStart = endAngle - startAngle; 356 if (endMinusStart <= -2 * PI) 357 endMinusStart = -2 * PI; 358 else 359 { 360 endMinusStart = normalizePhase(endMinusStart); 361 if (endMinusStart > 0) endMinusStart -= 2 * PI; 362 } 363 364 // Modify endAngle so that startAngle >= endAngle >= startAngle - 2 * PI 365 endAngle = startAngle + endMinusStart; 366 assert(endAngle <= startAngle); 367 } 368 369 // find tangential start point xt,yt 370 float xt = x + fast_cos(startAngle) * radius; 371 float yt = y + fast_sin(startAngle) * radius; 372 373 // Make a line to there 374 lineTo(xt, yt); 375 376 enum float MAX_ANGLE_FOR_SINGLE_BEZIER_CURVE = PI / 2.0; 377 378 // From https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves 379 // The optimal distance to the control points, in the sense that the 380 // middle of the curve lies on the circle itself, is (4/3)*tan(pi/(2n)). 381 382 float angleDiff = endAngle - startAngle; 383 if (startAngle == endAngle || angleDiff == 0) 384 return; 385 386 // How many bezier curves will we draw? 387 // The angle will be evenly split between those parts. 388 // The 1e-2 offset is to avoid a circle made with 5 curves. 389 int numCurves = cast(int)(fast_ceil( (fast_fabs(angleDiff) - 1e-2f) / MAX_ANGLE_FOR_SINGLE_BEZIER_CURVE)); 390 assert(numCurves >= 0); 391 if (numCurves == 0) 392 numCurves = 1; 393 394 float currentAngle = startAngle; 395 float angleIncr = angleDiff / cast(float)numCurves; 396 397 // Compute where control points should be placed 398 // How many segments does this correspond to for a full 2*pi circle? 399 float numCurvesIfThisWereACircle = (2.0f * PI * numCurves) / angleDiff; 400 401 // Then compute optimal distance of the control points 402 float xx = cast(float)PI / (2.0f * numCurvesIfThisWereACircle); 403 float optimalDistance = (4.0f / 3.0f) * tan(xx); 404 optimalDistance *= radius; 405 406 float angle0 = startAngle; 407 float cos0 = fast_cos(angle0); 408 float sin0 = fast_sin(angle0); 409 410 // Using complex rotation here to save some cos/sin operations. 411 float phasorX = fast_cos(angleIncr); 412 float phasorY = fast_sin(angleIncr); 413 414 foreach(curve; 0..numCurves) 415 { 416 float cos1 = cos0 * phasorX - sin0 * phasorY; 417 float sin1 = cos0 * phasorY + sin0 * phasorX; 418 419 // compute end points of the curve 420 float x0 = x + cos0 * radius; 421 float y0 = y + sin0 * radius; 422 float x1 = x + cos1 * radius; 423 float y1 = y + sin1 * radius; 424 425 // compute control points 426 float cp0x = x0 - sin0 * optimalDistance; 427 float cp0y = y0 + cos0 * optimalDistance; 428 float cp1x = x1 + sin1 * optimalDistance; 429 float cp1y = y1 - cos1 * optimalDistance; 430 bezierCurveTo(cp0x, cp0y, cp1x, cp1y, x1, y1); 431 432 cos0 = cos1; 433 sin0 = sin1; 434 } 435 } 436 ///ditto 437 void arc(vec2f center, float radius, float startAngle, float endAngle, bool anticlockwise = false) 438 { 439 arc(center.x, center.y, radius, startAngle, endAngle, anticlockwise); 440 } 441 442 /// Fills all subpaths of the current path using the current `fillStyle`. 443 /// Open subpaths are implicitly closed when being filled. 444 void fill() 445 { 446 closePath(); 447 448 // Select a particular blitter function here, depending on current state. 449 _currentBlitter.doBlit = getBlitFunction(); 450 451 CompositeOp op; 452 final switch(_stateStack[$-1].compositeOp) 453 { 454 case CompositeOperation.sourceOver: op = CompositeOp.SourceOver; break; 455 case CompositeOperation.add: op = CompositeOp.Add; break; 456 case CompositeOperation.subtract: op = CompositeOp.Subtract; break; 457 case CompositeOperation.lighten: op = CompositeOp.LightenOnly; break; 458 case CompositeOperation.darken: op = CompositeOp.DarkenOnly; break; 459 } 460 461 _rasterizer.rasterize(cast(ubyte*) _imageDest.pixels, 462 _imageDest.pitch, 463 _imageDest.h, 464 _currentBlitter, 465 op); 466 } 467 468 /// Fill a rectangle using the current `fillStyle`. 469 /// Note: affects the current path. 470 void fillRect(float x, float y, float width, float height) 471 { 472 beginPath(); 473 rect(x, y, width, height); 474 fill(); 475 } 476 ///ditto 477 void fillRect(vec2f topLeft, vec2f dimension) 478 { 479 fillRect(topLeft.x, topLeft.y, dimension.x, dimension.y); 480 } 481 ///ditto 482 void fillRect(box2f rect) 483 { 484 fillRect(rect.min.x, rect.min.y, rect.width, rect.height); 485 } 486 ///ditto 487 void fillRect(box2i rect) 488 { 489 fillRect(rect.min.x, rect.min.y, rect.width, rect.height); 490 } 491 492 /// Fill a disc using the current `fillStyle`. 493 /// Note: affects the current path. 494 void fillCircle(float x, float y, float radius) 495 { 496 beginPath(); 497 moveTo(x + radius, y); 498 arc(x, y, radius, 0, 2 * PI); 499 fill(); 500 } 501 ///ditto 502 void fillCircle(vec2f center, float radius) 503 { 504 fillCircle(center.x, center.y, radius); 505 } 506 507 // </PATH> functions 508 509 510 // <GRADIENT> functions 511 512 /// Creates a linear gradient along the line given by the coordinates 513 /// represented by the parameters. 514 CanvasGradient createLinearGradient(float x0, float y0, float x1, float y1) 515 { 516 // TODO: delay this transform upon point of use with CTM 517 vec2f pt0 = transformPoint(x0, y0); 518 vec2f pt1 = transformPoint(x1, y1); 519 520 CanvasGradient result = newOrReuseGradient(); 521 result.type = CanvasGradient.Type.linear; 522 result.x0 = pt0.x; 523 result.y0 = pt0.y; 524 result.x1 = pt1.x; 525 result.y1 = pt1.y; 526 return result; 527 } 528 ///ditto 529 CanvasGradient createLinearGradient(vec2f pt0, vec2f pt1) 530 { 531 return createLinearGradient(pt0.x, pt0.y, pt1.x, pt1.y); 532 } 533 534 535 /// Creates a circular gradient, centered in (x, y) and going from 0 to endRadius. 536 CanvasGradient createCircularGradient(float centerX, float centerY, float endRadius) 537 { 538 float x1 = centerX + endRadius; 539 float y1 = centerY; 540 float r2 = endRadius; 541 return createEllipticalGradient(centerX, centerY, x1, y1, r2); 542 } 543 ///ditto 544 CanvasGradient createCircularGradient(vec2f center, float endRadius) 545 { 546 return createCircularGradient(center.x, center.y, endRadius); 547 } 548 549 /// Creates an elliptical gradient. 550 /// First radius is given by (x1, y1), second radius with a radius at 90° with the first one). 551 CanvasGradient createEllipticalGradient(float x0, float y0, float x1, float y1, float r2) 552 { 553 // TODO: delay this transform upon point of use with CTM 554 vec2f pt0 = transformPoint(x0, y0); 555 vec2f pt1 = transformPoint(x1, y1); 556 557 // Transform r2 radius 558 vec2f diff = vec2f(x1 - x0, y1 - y0).normalized; // TODO: this could crash with radius zero 559 vec2f pt2 = vec2f(x0 - diff.y * r2, y0 + diff.x * r2); 560 pt2 = transformPoint(pt2); 561 float tr2 = pt2.distanceTo(pt0); 562 563 CanvasGradient result = newOrReuseGradient(); 564 result.type = CanvasGradient.Type.elliptical; 565 result.x0 = pt0.x; 566 result.y0 = pt0.y; 567 result.x1 = pt1.x; 568 result.y1 = pt1.y; 569 result.r2 = tr2; 570 return result; 571 } 572 ///ditto 573 CanvasGradient createEllipticalGradient(vec2f pt0, vec2f pt1, float r2) 574 { 575 return createEllipticalGradient(pt0.x, pt0.y, pt1.x, pt1.y, r2); 576 } 577 578 // </GRADIENT> functions 579 580 581 // <STATE> functions 582 583 /// Save: 584 /// - current transform 585 void save() 586 { 587 _stateStack ~= _stateStack[$-1]; // just duplicate current state 588 } 589 590 /// Restores state corresponding to `save()`. 591 void restore() 592 { 593 _stateStack.popBack(); 594 if (_stateStack.length == 0) 595 assert(false); // too many restore() without corresponding save() 596 } 597 598 /// Retrieves the current transformation matrix. 599 Transform2D currentTransform() 600 { 601 return _stateStack[$-1].transform; 602 } 603 alias getTransform = currentTransform; ///ditto 604 605 /// Adds a rotation to the transformation matrix. The angle argument represents 606 /// a clockwise rotation angle and is expressed in radians. 607 void rotate(float angle) 608 { 609 float cosa = cos(angle); 610 float sina = sin(angle); 611 curMatrix() = curMatrix().scaleMulRot(cosa, sina); 612 } 613 614 /// Adds a scaling transformation to the canvas units by x horizontally and by y vertically. 615 void scale(float x, float y) 616 { 617 curMatrix() = curMatrix().scaleMulOpt(x, y); 618 } 619 ///ditto 620 void scale(vec2f xy) 621 { 622 scale(xy.x, xy.y); 623 } 624 ///ditto 625 void scale(float xy) 626 { 627 scale(xy, xy); 628 } 629 630 /// Adds a translation transformation by moving the canvas and its origin `x` 631 /// horizontally and `y` vertically on the grid. 632 void translate(float x, float y) 633 { 634 curMatrix() = curMatrix().translateMulOpt(x, y); 635 } 636 ///ditto 637 void translate(vec2f position) 638 { 639 translate(position.x, position.y); 640 } 641 642 /// Multiplies the current transformation matrix with the matrix described by its arguments. 643 void transform(float a, float b, float c, 644 float d, float e, float f) 645 { 646 curMatrix() *= Transform2D(a, c, e, 647 b, d, f); 648 } 649 650 void setTransform(float a, float b, float c, 651 float d, float e, float f) 652 { 653 curMatrix() = Transform2D(a, c, e, 654 b, d, f); 655 } 656 657 ///ditto 658 void setTransform(Transform2D transform) 659 { 660 curMatrix() = transform; 661 } 662 663 /// Changes the current transformation matrix to the identity matrix. 664 void resetTransform() 665 { 666 curMatrix() = Transform2D.identity(); 667 } 668 669 // </STATE> 670 671 private: 672 673 ImageRef!RGBA _imageDest; 674 675 enum BrushStyle 676 { 677 plainColor 678 } 679 680 enum BlitType 681 { 682 color, // Blit is a ColorBlit 683 linear, 684 elliptical 685 } 686 687 Rasterizer _rasterizer; 688 689 Blitter _currentBlitter; 690 BlitType _blitType; 691 692 // depends upon current fill rule and blit type. 693 auto getBlitFunction() pure 694 { 695 FillRule rule = _stateStack[$-1].fillRule; 696 bool nonZero = (rule == FillRule.nonZero); 697 final switch(_blitType) 698 { 699 case BlitType.color: 700 return nonZero 701 ? &doBlit_ColorBlit_NonZero 702 : &doBlit_ColorBlit_EvenOdd; 703 case BlitType.linear: 704 return nonZero 705 ? &doBlit_LinearBlit_NonZero 706 : &doBlit_LinearBlit_EvenOdd; 707 case BlitType.elliptical: 708 return nonZero 709 ? &doBlit_EllipticalBlit_NonZero 710 : &doBlit_EllipticalBlit_EvenOdd; 711 } 712 713 } 714 715 716 // Blitters (only one used at once) 717 union 718 { 719 ColorBlit _plainColorBlit; 720 LinearBlit _linearGradientBlit; 721 EllipticalBlit _ellipticalGradientBlit; 722 } 723 724 // Gradient cache 725 // You're expected to recreate gradient in draw code. 726 int _gradientUsed; // number of gradients in _gradients in active use 727 Vec!CanvasGradient _gradients; // all gradients here are created on demand, 728 // and possibly reusable after `initialize` 729 730 CanvasGradient newOrReuseGradient() 731 { 732 if (_gradientUsed < _gradients.length) 733 { 734 _gradients[_gradientUsed].reset(); 735 return _gradients[_gradientUsed++]; 736 } 737 else 738 { 739 CanvasGradient result = mallocNew!CanvasGradient(); 740 _gradients.pushBack(result); 741 return result; 742 } 743 } 744 745 // State stack. 746 // Current state is the last element. 747 Vec!State _stateStack; 748 749 // What is saved by `save`. 750 struct State 751 { 752 Transform2D transform; 753 FillRule fillRule; 754 CompositeOperation compositeOp; 755 } 756 757 ref Transform2D curMatrix() 758 { 759 return _stateStack[$-1].transform; 760 } 761 762 vec2f transformPoint(float x, float y) 763 { 764 Transform2D M = currentTransform(); 765 float fx = x * M.a + y * M.b + M.c; 766 float fy = x * M.d + y * M.e + M.f; 767 return vec2f(fx, fy); 768 } 769 770 vec2f transformPoint(vec2f pt) 771 { 772 return transformPoint(pt.x, pt.y); 773 } 774 } 775 776 /// Holds both gradient data/table and positioning information. 777 /// 778 /// You can create a gradient with `createLinearGradient`, `createCircularGradient`, or `createEllipticalGradient`, 779 /// every frame. 780 /// Then use `addColorStop` to set the color information. 781 /// 782 /// The gradient data is managed by the `Canvas` object itself. All gradients are invalidated once 783 /// `initialize` has been called. 784 class CanvasGradient 785 { 786 public: 787 nothrow: 788 @nogc: 789 790 this() 791 { 792 _gradient = mallocNew!Gradient(); 793 } 794 795 /// Adds a new color stop, defined by an offset and a color, to a given canvas gradient. 796 void addColorStop(float offset, RGBA color) 797 { 798 uint color_as_uint = *cast(uint*)(&color); 799 _gradient.addStop(offset, color_as_uint); 800 } 801 802 package: 803 804 enum Type 805 { 806 linear, 807 radial, 808 elliptical, 809 angular, 810 } 811 812 Type type; 813 814 void reset() 815 { 816 _gradient.reset(); 817 } 818 819 float x0, y0, x1, y1, r2; 820 Gradient _gradient; 821 } 822 823 /// The transform type used by dplug:canvas. It's a 3x3 float matrix 824 /// with the form: 825 /// (a b c) 826 /// (d e f) 827 /// (0 0 1) 828 struct Transform2D 829 { 830 pure nothrow @nogc: 831 float a = 1, b = 0, c = 0, 832 d = 0, e = 1, f = 0; 833 834 static Transform2D identity() 835 { 836 return Transform2D.init; 837 } 838 839 void opOpAssign(string op)(Transform2D o) if (op == "*") 840 { 841 // a b c 842 // d e f 843 // 0 0 1 844 // a b c A B C 845 // d e f D E F 846 // 0 0 1 0 0 1 847 848 float A = a * o.a + b * o.d; 849 float B = a * o.b + b * o.e; 850 float C = a * o.c + b * o.f + c; 851 float D = d * o.a + e * o.d; 852 float E = d * o.b + e * o.e; 853 float F = d * o.c + e * o.f + f; 854 this = Transform2D(A, B, C, D, E, F); 855 } 856 857 /// Return this * Transform2D(1, 0, x, 858 /// 0, 1, y); 859 Transform2D translateMulOpt(float x, float y) 860 { 861 // 1 0 x 862 // 0 1 y 863 // 0 0 1 864 // ------- 865 // a b c | a b C 866 // d e f | d e F 867 // 0 0 1 | 0 0 1 868 float C = a * x + b * y + c; 869 float F = d * x + e * y + f; 870 return Transform2D(a, b, C, d, e, F); 871 } 872 873 /// Return this * Transform2D(x, 0, 0, 874 /// 0, y, 0); 875 Transform2D scaleMulOpt(float x, float y) 876 { 877 // x 0 0 878 // 0 y 0 879 // 0 0 1 880 // ------- 881 // a b c | A B c 882 // d e f | D E f 883 // 0 0 1 | 0 0 1 884 float A = x * a; 885 float B = y * b; 886 float D = x * d; 887 float E = y * e; 888 return Transform2D(A, B, c, D, E, f); 889 } 890 891 892 /// Return this * Transform2D(cosa, -sina, 0, 893 /// sina, cosa, 0) 894 Transform2D scaleMulRot(float cosa, float sina) 895 { 896 // g -h 0 897 // h g 0 898 // 0 0 1 899 // ------- 900 // a b c | A B c 901 // d e f | D E f 902 // 0 0 1 | 0 0 1 903 float A = cosa * a + sina * b; 904 float B = cosa * b - sina * a; 905 float D = cosa * d + sina * e; 906 float E = cosa * e - sina * d; 907 return Transform2D(A, B, c, 908 D, E, f); 909 } 910 }