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