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