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 }