1 /*
2 Copyright (c) 2018-2021 Timur Gafarov
3 
4 Boost Software License - Version 1.0 - August 17th, 2003
5 
6 Permission is hereby granted, free of charge, to any person or organization
7 obtaining a copy of the software and accompanying documentation covered by
8 this license (the "Software") to use, reproduce, display, distribute,
9 execute, and transmit the Software, and to prepare derivative works of the
10 Software, and to permit third-parties to whom the Software is furnished to
11 do so, all subject to the following:
12 
13 The copyright notices in the Software and this entire statement, including
14 the above license grant, this restriction and the following disclaimer,
15 must be included in all copies of the Software, in whole or in part, and
16 all derivative works of the Software, unless such copies or derivative
17 works are solely in the form of machine-executable object code generated by
18 a source language processor.
19 
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
23 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
24 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
25 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26 DEALINGS IN THE SOFTWARE.
27 */
28 
29 /**
30  * Simple 2D rendering engine
31  *
32  * Copyright: Timur Gafarov 2018-2021.
33  * License: $(LINK2 boost.org/LICENSE_1_0.txt, Boost License 1.0).
34  * Authors: Timur Gafarov
35  */
36 module dlib.image.canvas;
37 
38 import std.math;
39 
40 import dlib.math.vector;
41 import dlib.math.matrix;
42 import dlib.math.transformation;
43 import dlib.math.utils;
44 import dlib.math.interpolation.bezier;
45 import dlib.container.array;
46 import dlib.image.color;
47 import dlib.image.image;
48 import dlib.image.render.shapes;
49 import dlib.core.memory;
50 
51 /// General options for drawing things
52 struct CanvasState
53 {
54     Matrix3x3f transformation;
55     Color4f lineColor;
56     Color4f fillColor;
57     float lineWidth;
58 }
59 
60 /// Type of a path segment
61 enum SegmentType
62 {
63     Line,
64     BezierCubic
65 }
66 
67 /// Path segment
68 struct ContourSegment
69 {
70     Vector2f p1;
71     Vector2f p2;
72     Vector2f p3;
73     Vector2f p4;
74     float radius;
75     SegmentType type;
76 }
77 
78 /**
79  * A simple 2D vector engine inspired by HTML5 canvas.
80  * Supports rendering arbitrary polygons and cubic Bezier paths, filled and outlined.
81  * Not real-time.
82  */
83 class Canvas
84 {
85    protected:
86     SuperImage _image;
87     SuperImage tmpBuffer;
88 
89     // TODO: state stack
90     CanvasState state;
91 
92     Array!ContourSegment contour;
93     Vector2f penPosition;
94 
95     float tesselationStep = 1.0f / 40.0f;
96     uint subpixelResolution = 4;
97 
98    public:
99     this(SuperImage img)
100     {
101         _image = img;
102 
103         tmpBuffer = image.createSameFormat(_image.width, _image.height);
104 
105         state.transformation = Matrix3x3f.identity;
106         state.lineColor = Color4f(0.0f, 0.0f, 0.0f, 1.0f);
107         state.fillColor = Color4f(0.0f, 0.0f, 0.0f, 1.0f);
108         state.lineWidth = 1.0f;
109 
110         penPosition = Vector2f(0.0f, 0.0f);
111     }
112 
113     ~this()
114     {
115         contour.free();
116         tmpBuffer.free();
117     }
118 
119    public:
120     SuperImage image() @property
121     {
122         return _image;
123     }
124 
125     void fillColor(Color4f c) @property
126     {
127         state.fillColor = c;
128     }
129 
130     Color4f fillColor() @property
131     {
132         return state.fillColor;
133     }
134 
135     void lineColor(Color4f c) @property
136     {
137         state.lineColor = c;
138     }
139 
140     Color4f lineColor() @property
141     {
142         return state.lineColor;
143     }
144 
145     void lineWidth(float w) @property
146     {
147         state.lineWidth = w;
148     }
149 
150     float lineWidth() @property
151     {
152         return state.lineWidth;
153     }
154 
155     void resetTransform()
156     {
157         state.transformation = Matrix3x3f.identity;
158     }
159 
160     void transform(Matrix3x3f m)
161     {
162         state.transformation *= m;
163     }
164 
165     void translate(float x, float y)
166     {
167         state.transformation *= translationMatrix2D(Vector2f(x, y));
168     }
169 
170     void rotate(float a)
171     {
172         state.transformation *= rotationMatrix2D(a);
173     }
174 
175     void scale(float x, float y)
176     {
177         state.transformation *= scaleMatrix2D(Vector2f(x, y));
178     }
179 
180     void clear(Color4f c)
181     {
182         dlib.image.render.shapes.fillColor(_image, c);
183     }
184 
185     void beginPath()
186     {
187         penPosition = Vector2f(0.0f, 0.0f);
188     }
189 
190     void endPath()
191     {
192         contour.free();
193     }
194 
195     void pathMoveTo(float x, float y)
196     {
197         penPosition = Vector2f(x, y);
198     }
199 
200     void pathLineTo(float x, float y)
201     {
202         Vector2f p1 = penPosition;
203         Vector2f p2 = Vector2f(x, y);
204         pathAddLine(p1, p2);
205         penPosition = p2;
206     }
207 
208     void pathBezierTo(Vector2f cp1, Vector2f cp2, Vector2f endPoint)
209     {
210         pathAddBezierCubic(penPosition, cp1, cp2, endPoint);
211         penPosition = endPoint;
212     }
213 
214     void pathStroke()
215     {
216         dlib.image.render.shapes.fillColor(tmpBuffer, Color4f(0, 0, 0, 0));
217         drawContour();
218         blitTmpBuffer(state.lineColor);
219     }
220 
221     void pathFill()
222     {
223         fillShape();
224     }
225 
226    protected:
227     void pathAddLine(Vector2f p1, Vector2f p2)
228     {
229         ContourSegment segment;
230         segment.p1 = p1;
231         segment.p2 = p2;
232         segment.type = SegmentType.Line;
233         contour.append(segment);
234     }
235 
236     void pathAddBezierCubic(Vector2f p1, Vector2f p2, Vector2f p3, Vector2f p4)
237     {
238         ContourSegment segment;
239         segment.p1 = p1;
240         segment.p2 = p2;
241         segment.p3 = p3;
242         segment.p4 = p4;
243         segment.type = SegmentType.BezierCubic;
244         contour.append(segment);
245     }
246 
247     void fillShape()
248     {
249         Array!Vector2f poly;
250         Array!size_t polyBounds;
251         Vector2f startP, endP, wayP1, wayP2;
252 
253         foreach(ref p; contour.data)
254         {
255             startP = p.p1.affineTransform2D(state.transformation);
256 
257             if (startP != endP)
258             {
259                 polyBounds.append(poly.length);
260                 poly.append(startP);
261             }
262 
263             if (p.type == SegmentType.Line)
264             {
265                 endP = p.p2.affineTransform2D(state.transformation);
266                 poly.append(endP);
267             }
268             else
269             {
270                 assert(p.type == SegmentType.BezierCubic);
271                 wayP1 = p.p2.affineTransform2D(state.transformation);
272                 wayP2 = p.p3.affineTransform2D(state.transformation);
273                 endP = p.p4.affineTransform2D(state.transformation);
274 
275                 float t = 0.0f;
276                 while(t < 1.0f)
277                 {
278                     t += tesselationStep;
279                     Vector2f tessP = bezierVector2(startP, wayP1, wayP2, endP, t);
280                     poly.append(tessP);
281                 }
282             }
283         }
284 
285         auto alphas = New!(float[])(polyBounds.length);
286         polyBounds.append(poly.length);
287 
288         foreach(y; 0.._image.height)
289         foreach(x; 0.._image.width)
290         {
291             auto p = Vector2f(x, y);
292             bool inside = false;
293 
294             foreach(i, ref alpha; alphas)
295             {
296                 alpha = pointInPolygonAAFast(p, poly.data[polyBounds[i] .. polyBounds[i+1]]);
297                 if (alpha == 1) inside = !inside;
298             }
299 
300             float totalAlpha = inside? 1: 0;
301 
302             if (inside)
303             {
304                 foreach(alpha; alphas)
305                 {
306                     totalAlpha *= alpha == 1? 1: 1 - alpha;
307                 }
308             }
309             else
310             {
311                 foreach(alpha; alphas)
312                 {
313                     totalAlpha += alpha == 1? 0: (1 - totalAlpha) * alpha;
314                 }
315             }
316 
317             Color4f c = state.fillColor;
318             c.a = c.a * totalAlpha;
319 
320             if (c.a > 0.0f)
321             {
322                 _image[x, y] = alphaOver(_image[x, y], c);
323             }
324         }
325 
326         poly.free();
327         polyBounds.free();
328         alphas.Delete();
329     }
330 
331     void drawContour()
332     {
333         Vector2f tp1, tp2, tp3, tp4;
334 
335         foreach(i, ref p; contour.data)
336         {
337             if (p.type == SegmentType.Line)
338             {
339                 tp1 = p.p1.affineTransform2D(state.transformation);
340                 tp2 = p.p2.affineTransform2D(state.transformation);
341                 drawLine(tp1, tp2);
342             }
343             else if (p.type == SegmentType.BezierCubic)
344             {
345                 tp1 = p.p1.affineTransform2D(state.transformation);
346                 tp2 = p.p2.affineTransform2D(state.transformation);
347                 tp3 = p.p3.affineTransform2D(state.transformation);
348                 tp4 = p.p4.affineTransform2D(state.transformation);
349                 drawBezierCurve(tp1, tp2, tp3, tp4);
350             }
351         }
352     }
353 
354     void drawLineTangent(Vector2f p1, Vector2f p2, Vector2f t1, Vector2f t2)
355     {
356         Vector2f n1 = Vector2f(-t1.y, t1.x);
357         Vector2f n2 = Vector2f(-t2.y, t2.x);
358 
359         Vector2f offset1 = n1 * state.lineWidth * 0.5f;
360         Vector2f offset2 = n2 * state.lineWidth * 0.5f;
361 
362         Vector2f[4] poly;
363         poly[0] = p1 - offset1;
364         poly[1] = p1 + offset1;
365         poly[2] = p2 + offset2;
366         poly[3] = p2 - offset2;
367 
368         float subpSize = 1.0f / subpixelResolution;
369         float subpContrib = 1.0f / (subpixelResolution * subpixelResolution);
370 
371         int xmin = cast(int)min2(min2(poly[0].x, poly[1].x), min2(poly[2].x, poly[3].x)) - 1;
372         int ymin = cast(int)min2(min2(poly[0].y, poly[1].y), min2(poly[2].y, poly[3].y)) - 1;
373         int xmax = cast(int)max2(max2(poly[0].x, poly[1].x), max2(poly[2].x, poly[3].x)) + 1;
374         int ymax = cast(int)max2(max2(poly[0].y, poly[1].y), max2(poly[2].y, poly[3].y)) + 1;
375 
376         foreach(y; ymin..ymax)
377         foreach(x; xmin..xmax)
378         {
379             float alpha = 0.0f;
380 
381             foreach(sy; 0..subpixelResolution)
382             foreach(sx; 0..subpixelResolution)
383             {
384                 auto p = Vector2f(x + sx * subpSize, y + sy * subpSize);
385 
386                 if (pointInPolygon(p, poly))
387                     alpha += subpContrib;
388             }
389 
390             float srcAlpha = tmpBuffer[x, y].r;
391             tmpBuffer[x, y] = Color4f(min2(srcAlpha + alpha, 1.0f), 0, 0, 1);
392         }
393     }
394 
395     void drawLine(Vector2f p1, Vector2f p2)
396     {
397         Vector2f dir = p2 - p1;
398         Vector2f ndir = dir.normalized;
399         drawLineTangent(p1, p2, ndir, ndir);
400     }
401 
402     void drawBezierCurve(Vector2f a, Vector2f b, Vector2f c, Vector2f d)
403     {
404         Vector2f p1 = a;
405         Vector2f t1 = bezierTangentVector2(a, b, c, d, 0.0f).normalized;
406 
407         float t = 0.0f;
408         while(t < 1.0f)
409         {
410             t += tesselationStep;
411             Vector2f p2 = bezierVector2(a, b, c, d, t);
412             Vector2f t2 = bezierTangentVector2(a, b, c, d, t).normalized;
413             drawLineTangent(p1, p2, t1, t2);
414             p1 = p2;
415             t1 = t2;
416         }
417     }
418 
419     void blitTmpBuffer(Color4f color)
420     {
421         foreach(y; 0.._image.height)
422         foreach(x; 0.._image.width)
423         {
424             Color4f c1 = _image[x, y];
425             Color4f c2 = color;
426             c2.a = tmpBuffer[x, y].r * color.a;
427             _image[x, y] = alphaOver(c1, c2);
428         }
429     }
430 }
431 
432 bool pointInPolygon(Vector2f p, Vector2f[] poly)
433 {
434     size_t i = 0;
435     size_t j = poly.length - 1;
436     bool inside = false;
437 
438     for (i = 0; i < poly.length; i++)
439     {
440         Vector2f a = poly[i];
441         Vector2f b = poly[j];
442 
443         if ((a.y > p.y) != (b.y > p.y) &&
444             (p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x))
445             inside = !inside;
446 
447         j = i;
448     }
449 
450     return inside;
451 }
452 
453 float sqrDistanceToLineSegment(Vector2f a, Vector2f b, Vector2f p)
454 {
455     Vector2f n = b - a;
456     Vector2f pa = a - p;
457 
458     float c = dot(n, pa);
459 
460     if (c > 0.0f)
461         return dot(pa, pa);
462 
463     Vector2f bp = p - b;
464 
465     if (dot(n, bp) > 0.0f)
466         return dot(bp, bp);
467 
468     Vector2f e = pa - n * (c / dot(n, n));
469 
470     return dot(e, e);
471 }
472 
473 float pointInPolygonAAFast(Vector2f p, Vector2f[] poly)
474 {
475     size_t i = 0;
476     size_t j = poly.length - 1;
477     bool inside = false;
478     float minDistance = float.max;
479 
480     for (i = 0; i < poly.length; i++)
481     {
482         Vector2f a = poly[i];
483         Vector2f b = poly[j];
484 
485         float lx = (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x;
486 
487         if ((a.y > p.y) != (b.y > p.y))
488         {
489             if (p.x < lx)
490                 inside = !inside;
491 
492             float dist = sqrDistanceToLineSegment(a, b, p);
493             if (dist < minDistance)
494                 minDistance = dist;
495         }
496 
497         j = i;
498     }
499 
500     float cd = 1.0f - clamp(sqrt(minDistance), 0.0f, 1.0f);
501     return max2(cast(float)inside, cd);
502 }