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