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 }