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 }