1 /** 2 * Drawing functions. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.utils.graphics.draw; 15 16 import std.algorithm : sort; 17 import std.traits; 18 19 import ae.utils.geometry : TAU; 20 import ae.utils.graphics.view; 21 import ae.utils.graphics.color : solidStorageUnit; 22 import ae.utils.math; 23 import ae.utils.meta : structFields, SignedBitsType, UnsignedBitsType; 24 25 version(unittest) import ae.utils.graphics.image; 26 27 // Constraints could be simpler if this was fixed: 28 // https://issues.dlang.org/show_bug.cgi?id=12386 29 30 /// Get the pixel color at the specified coordinates, 31 /// or fall back to the specified default value if 32 /// the coordinates are out of bounds. 33 COLOR safeGet(V, COLOR)(auto ref V v, xy_t x, xy_t y, COLOR def) 34 if (isView!V && is(COLOR : ViewColor!V)) 35 { 36 if (x>=0 && y>=0 && x<v.w && y<v.h) 37 return v[x, y]; 38 else 39 return def; 40 } 41 42 unittest 43 { 44 auto v = onePixel(7); 45 assert(v.safeGet(0, 0, 0) == 7); 46 assert(v.safeGet(0, 1, 0) == 0); 47 } 48 49 /// Set the pixel color at the specified coordinates 50 /// if the coordinates are not out of bounds. 51 void safePut(V, COLOR)(auto ref V v, xy_t x, xy_t y, COLOR value) 52 if (isWritableView!V && is(COLOR : ViewColor!V)) 53 { 54 if (x>=0 && y>=0 && x<v.w && y<v.h) 55 v[x, y] = value; 56 } 57 58 unittest 59 { 60 auto v = Image!int(1, 1); 61 v.safePut(0, 0, 7); 62 v.safePut(0, 1, 9); 63 assert(v[0, 0] == 7); 64 } 65 66 /// Forwards to safePut or opIndex, depending on the 67 /// CHECKED parameter. Allows propagation of a 68 /// CHECKED parameter from other callers. 69 void putPixel(bool CHECKED, V, COLOR)(auto ref V v, xy_t x, xy_t y, COLOR value) 70 if (isWritableView!V && is(COLOR : ViewColor!V)) 71 { 72 static if (CHECKED) 73 v.safePut(x, y, value); 74 else 75 v[x, y] = value; 76 } 77 78 unittest 79 { 80 auto v = Image!int(1, 1); 81 v.putPixel!false(0, 0, 7); 82 v.putPixel!true(0, 1, 9); 83 assert(v[0, 0] == 7); 84 } 85 86 /// Gets a pixel's address from a direct view. 87 ViewColor!V* pixelPtr(V)(auto ref V v, xy_t x, xy_t y) 88 if (isDirectView!V && is(typeof(&v.scanline(0)[0][0]))) 89 { 90 return &v.scanline(y)[x][0]; 91 } 92 93 unittest 94 { 95 auto v = Image!xy_t(1, 1); 96 v[0, 0] = 7; 97 auto p = v.pixelPtr(0, 0); 98 assert(*p == 7); 99 } 100 101 /// Fills a writable view with a solid color. 102 void fill(V, COLOR)(auto ref V v, COLOR c) 103 if (isWritableView!V 104 && is(COLOR : ViewColor!V)) 105 { 106 static if (isDirectView!V) 107 { 108 auto s = solidStorageUnit!(ViewStorageType!V)(c); 109 foreach (y; 0..v.h) 110 v.scanline(y)[] = s; 111 } 112 else 113 { 114 foreach (y; 0..v.h) 115 foreach (x; 0..v.w) 116 v[x, y] = c; 117 } 118 } 119 deprecated alias clear = fill; 120 121 unittest 122 { 123 auto i = onePixel(0).copy(); 124 i.fill(1); 125 assert(i[0, 0] == 1); 126 auto t = i.tile(10, 10); 127 t.fill(2); 128 assert(i[0, 0] == 2); 129 } 130 131 // *************************************************************************** 132 133 private enum CheckHLine = 134 q{ 135 static if (CHECKED) 136 { 137 if (x1 >= v.w || x2 <= 0 || y < 0 || y >= v.h || x1 >= x2) return; 138 if (x1 < 0) x1 = 0; 139 if (x2 >= v.w) x2 = v.w; 140 } 141 assert(x1 <= x2); 142 }; 143 144 private enum CheckVLine = 145 q{ 146 static if (CHECKED) 147 { 148 if (x < 0 || x >= v.w || y1 >= v.h || y2 <= 0 || y1 >= y2) return; 149 if (y1 < 0) y1 = 0; 150 if (y2 >= v.h) y2 = v.h; 151 } 152 assert(y1 <= y2); 153 }; 154 155 /// Draw a horizontal line. 156 void hline(bool CHECKED=true, V, COLOR)(auto ref V v, xy_t x1, xy_t x2, xy_t y, COLOR c) 157 if (isWritableView!V && is(COLOR : ViewColor!V)) 158 { 159 mixin(CheckHLine); 160 static if (isDirectView!V && ViewStorageType!V.length == 1) 161 { 162 auto s = solidStorageUnit!(ViewStorageType!V)(c); 163 v.scanline(y)[x1..x2] = s; 164 } 165 else 166 foreach (x; x1..x2) 167 v[x, y] = c; 168 } 169 170 /// Draw a vertical line. 171 void vline(bool CHECKED=true, V, COLOR)(auto ref V v, xy_t x, xy_t y1, xy_t y2, COLOR c) 172 if (isWritableView!V && is(COLOR : ViewColor!V)) 173 { 174 mixin(CheckVLine); 175 foreach (y; y1..y2) // TODO: optimize 176 v[x, y] = c; 177 } 178 179 /// Draw a line. 180 void line(bool CHECKED=true, V, COLOR)(auto ref V v, xy_t x1, xy_t y1, xy_t x2, xy_t y2, COLOR c) 181 if (isWritableView!V && is(COLOR : ViewColor!V)) 182 { 183 mixin FixMath; 184 import std.algorithm.mutation : swap; 185 186 enum DrawLine = q{ 187 // Axis-independent part. Mixin context: 188 // a0 .. a1 - longer side 189 // b0 .. b1 - shorter side 190 // DrawPixel - mixin to draw a pixel at coordinates (a, b) 191 192 if (a0 == a1) 193 return; 194 195 if (a0 > a1) 196 { 197 swap(a0, a1); 198 swap(b0, b1); 199 } 200 201 // Use fixed-point for b position and offset per 1 pixel along "a" axis 202 assert(b0 < (1L<<coordinateBits) && b1 < (1L<<coordinateBits)); 203 auto bPos = cast(SignedBitsType!(coordinateBits*2))(b0 << coordinateBits); 204 auto bOff = cast(SignedBitsType!(coordinateBits*2))(((b1-b0) << coordinateBits) / (a1-a0)); 205 206 foreach (a; a0..a1+1) 207 { 208 xy_t b = (bPos += bOff) >> coordinateBits; 209 mixin(DrawPixel); 210 } 211 }; 212 213 import std.math : abs; 214 215 if (abs(x2-x1) > abs(y2-y1)) 216 { 217 alias x1 a0; 218 alias x2 a1; 219 alias y1 b0; 220 alias y2 b1; 221 enum DrawPixel = q{ v.putPixel!CHECKED(a, b, c); }; 222 mixin(DrawLine); 223 } 224 else 225 { 226 alias y1 a0; 227 alias y2 a1; 228 alias x1 b0; 229 alias x2 b1; 230 enum DrawPixel = q{ v.putPixel!CHECKED(b, a, c); }; 231 mixin(DrawLine); 232 } 233 } 234 235 /// Draws a rectangle with a solid line. 236 /// The coordinates represent bounds (open on the right) for the outside of the rectangle. 237 void rect(bool CHECKED=true, V, COLOR)(auto ref V v, xy_t x1, xy_t y1, xy_t x2, xy_t y2, COLOR c) 238 if (isWritableView!V && is(COLOR : ViewColor!V)) 239 { 240 sort2(x1, x2); 241 sort2(y1, y2); 242 v.hline!CHECKED(x1, x2, y1 , c); 243 v.hline!CHECKED(x1, x2, y2-1, c); 244 v.vline!CHECKED(x1 , y1, y2, c); 245 v.vline!CHECKED(x2-1, y1, y2, c); 246 } 247 248 /// Draw a filled rectangle. 249 void fillRect(bool CHECKED=true, V, COLOR)(auto ref V v, xy_t x1, xy_t y1, xy_t x2, xy_t y2, COLOR b) // [) 250 if (isWritableView!V && is(COLOR : ViewColor!V)) 251 { 252 sort2(x1, x2); 253 sort2(y1, y2); 254 static if (CHECKED) 255 { 256 if (x1 >= v.w || y1 >= v.h || x2 <= 0 || y2 <= 0 || x1==x2 || y1==y2) return; 257 if (x1 < 0) x1 = 0; 258 if (y1 < 0) y1 = 0; 259 if (x2 >= v.w) x2 = v.w; 260 if (y2 >= v.h) y2 = v.h; 261 } 262 foreach (y; y1..y2) 263 v.hline!false(x1, x2, y, b); 264 } 265 266 /// Draw a filled rectangle with an outline. 267 void fillRect(bool CHECKED=true, V, COLOR)(auto ref V v, xy_t x1, xy_t y1, xy_t x2, xy_t y2, COLOR c, COLOR b) // [) 268 if (isWritableView!V && is(COLOR : ViewColor!V)) 269 { 270 v.rect!CHECKED(x1, y1, x2, y2, c); 271 if (x2-x1>2 && y2-y1>2) 272 v.fillRect!CHECKED(x1+1, y1+1, x2-1, y2-1, b); 273 } 274 275 /// Recursively replace the color of all adjacent pixels of the same color, 276 /// starting with the given coordinates. 277 /// Unchecked! Make sure area is bounded. 278 void uncheckedFloodFill(V, COLOR)(auto ref V v, xy_t x, xy_t y, COLOR c) 279 if (isDirectView!V && is(COLOR : ViewColor!V)) 280 { 281 v.floodFillPtr(&v[x, y], c, v[x, y]); 282 } 283 284 private void floodFillPtr(V, COLOR)(auto ref V v, COLOR* pp, COLOR c, COLOR f) 285 if (isDirectView!V && is(COLOR : ViewColor!V)) 286 { 287 COLOR* p0 = pp; while (*p0==f) p0--; p0++; 288 COLOR* p1 = pp; while (*p1==f) p1++; p1--; 289 auto stride = v.scanline(1).ptr-v.scanline(0).ptr; 290 for (auto p=p0; p<=p1; p++) 291 *p = c; 292 p0 -= stride; p1 -= stride; 293 for (auto p=p0; p<=p1; p++) 294 if (*p == f) 295 v.floodFillPtr(p, c, f); 296 p0 += stride*2; p1 += stride*2; 297 for (auto p=p0; p<=p1; p++) 298 if (*p == f) 299 v.floodFillPtr(p, c, f); 300 } 301 302 /// Draw a filled circle. 303 void fillCircle(V, COLOR)(auto ref V v, xy_t x, xy_t y, xy_t r, COLOR c) 304 if (isWritableView!V && is(COLOR : ViewColor!V)) 305 { 306 import std.algorithm.comparison : min; 307 308 xy_t x0 = x>r?x-r:0; 309 xy_t y0 = y>r?y-r:0; 310 xy_t x1 = min(x+r, v.w-1); 311 xy_t y1 = min(y+r, v.h-1); 312 xy_t rs = sqr(r); 313 // TODO: optimize 314 foreach (py; y0..y1+1) 315 foreach (px; x0..x1+1) 316 if (sqr(x-px) + sqr(y-py) < rs) 317 v[px, py] = c; 318 } 319 320 /// Draw a filled sector (circle slice). 321 void fillSector(V, COLOR)(auto ref V v, xy_t x, xy_t y, xy_t r0, xy_t r1, real a0, real a1, COLOR c) 322 if (isWritableView!V && is(COLOR : ViewColor!V)) 323 { 324 import std.algorithm.comparison : min; 325 import std.math : atan2; 326 327 xy_t x0 = x>r1?x-r1:0; 328 xy_t y0 = y>r1?y-r1:0; 329 xy_t x1 = min(x+r1, v.w-1); 330 xy_t y1 = min(y+r1, v.h-1); 331 xy_t r0s = sqr(r0); 332 xy_t r1s = sqr(r1); 333 if (a0 > a1) 334 a1 += TAU; 335 foreach (py; y0..y1+1) 336 foreach (px; x0..x1+1) 337 { 338 xy_t dx = px-x; 339 xy_t dy = py-y; 340 xy_t rs = sqr(dx) + sqr(dy); 341 if (r0s <= rs && rs < r1s) 342 { 343 real a = atan2(cast(real)dy, cast(real)dx); 344 if ((a0 <= a && a <= a1) || 345 (a0 <= a+TAU && a+TAU <= a1)) 346 v[px, py] = c; 347 } 348 } 349 } 350 351 /// Polygon point definition. 352 struct Coord 353 { 354 /// 355 xy_t x, y; 356 string toString() { import std.string; return format("%s", [this.tupleof]); } /// 357 } 358 359 /// Draw a filled polygon with a variable number of points. 360 void fillPoly(V, COLOR)(auto ref V v, Coord[] coords, COLOR f) 361 if (isWritableView!V && is(COLOR : ViewColor!V)) 362 { 363 import std.algorithm.comparison : min, max; 364 365 xy_t minY, maxY; 366 minY = maxY = coords[0].y; 367 foreach (c; coords[1..$]) 368 minY = min(minY, c.y), 369 maxY = max(maxY, c.y); 370 371 foreach (y; minY..maxY+1) 372 { 373 xy_t[] intersections; 374 for (uint i=0; i<coords.length; i++) 375 { 376 auto c0=coords[i], c1=coords[i==$-1?0:i+1]; 377 if (y==c0.y) 378 { 379 assert(y == coords[i%$].y); 380 int pi = i-1; xy_t py; 381 while ((py=coords[(pi+$)%$].y)==y) 382 pi--; 383 int ni = i+1; xy_t ny; 384 while ((ny=coords[ni%$].y)==y) 385 ni++; 386 if (ni > coords.length) 387 continue; 388 if ((py>y) == (y>ny)) 389 intersections ~= coords[i%$].x; 390 i = ni-1; 391 } 392 else 393 if (c0.y<y && y<c1.y) 394 intersections ~= itpl(c0.x, c1.x, y, c0.y, c1.y); 395 else 396 if (c1.y<y && y<c0.y) 397 intersections ~= itpl(c1.x, c0.x, y, c1.y, c0.y); 398 } 399 400 assert(intersections.length % 2==0); 401 intersections.sort(); 402 for (uint i=0; i<intersections.length; i+=2) 403 v.hline!true(intersections[i], intersections[i+1], y, f); 404 } 405 } 406 407 /// Draw a line of a given thickness. 408 /// Does not draw caps. 409 void thickLine(V, COLOR)(auto ref V v, xy_t x1, xy_t y1, xy_t x2, xy_t y2, xy_t r, COLOR c) 410 if (isWritableView!V && is(COLOR : ViewColor!V)) 411 { 412 import std.math : sqrt; 413 414 xy_t dx = x2-x1; 415 xy_t dy = y2-y1; 416 xy_t d = cast(xy_t)sqrt(cast(float)(sqr(dx)+sqr(dy))); 417 if (d==0) return; 418 419 xy_t nx = dx*r/d; 420 xy_t ny = dy*r/d; 421 422 v.fillPoly([ 423 Coord(x1-ny, y1+nx), 424 Coord(x1+ny, y1-nx), 425 Coord(x2+ny, y2-nx), 426 Coord(x2-ny, y2+nx), 427 ], c); 428 } 429 430 /// Draw a polygon of a given thickness. 431 /// Does not draw caps. 432 void thickLinePoly(V, COLOR)(auto ref V v, Coord[] coords, xy_t r, COLOR c) 433 if (isWritableView!V && is(COLOR : ViewColor!V)) 434 { 435 foreach (i; 0..coords.length) 436 thickLine(coords[i].tupleof, coords[(i+1)%$].tupleof, r, c); 437 } 438 439 // ************************************************************************************************************************************ 440 441 private mixin template FixMath(ubyte coordinateBitsParam = 16) 442 { 443 import ae.utils.meta : SignedBitsType, UnsignedBitsType; 444 445 enum coordinateBits = coordinateBitsParam; 446 447 static assert(COLOR.homogeneous, "Asymmetric color types not supported, fix me!"); 448 /// Fixed-point type, big enough to hold a coordinate, with fractionary precision corresponding to channel precision. 449 alias fix = SignedBitsType!(COLOR.channelBits + coordinateBits); 450 /// Type to hold temporary values for multiplication and division 451 alias fix2 = SignedBitsType!(COLOR.channelBits*2 + coordinateBits); 452 453 static assert(COLOR.channelBits < 32, "Shift operators are broken for shifts over 32 bits, fix me!"); 454 fix tofix(T:int )(T x) { return cast(fix) (x<<COLOR.channelBits); } 455 fix tofix(T:float)(T x) { return cast(fix) (x*(1<<COLOR.channelBits)); } 456 T fixto(T:int)(fix x) { return cast(T)(x>>COLOR.channelBits); } 457 458 fix fixsqr(fix x) { return cast(fix)((cast(fix2)x*x) >> COLOR.channelBits); } 459 fix fixmul(fix x, fix y) { return cast(fix)((cast(fix2)x*y) >> COLOR.channelBits); } 460 fix fixdiv(fix x, fix y) { return cast(fix)((cast(fix2)x << COLOR.channelBits)/y); } 461 462 static assert(COLOR.ChannelType.sizeof*8 == COLOR.channelBits, "COLORs with ChannelType not corresponding to native type not currently supported, fix me!"); 463 /// Type only large enough to hold a fractionary part of a "fix" (i.e. color channel precision). Used for alpha values, etc. 464 alias COLOR.ChannelType frac; 465 /// Type to hold temporary values for multiplication and division 466 alias UnsignedBitsType!(COLOR.channelBits*2) frac2; 467 468 frac tofrac(T:float)(T x) { return cast(frac) (x*(1<<COLOR.channelBits)); } 469 frac fixfpart(fix x) { return cast(frac)x; } 470 frac fracsqr(frac x ) { return cast(frac)((cast(frac2)x*x) >> COLOR.channelBits); } 471 frac fracmul(frac x, frac y) { return cast(frac)((cast(frac2)x*y) >> COLOR.channelBits); } 472 473 frac tofracBounded(T:float)(T x) { return cast(frac) bound(tofix(x), 0, frac.max); } 474 } 475 476 // ************************************************************************************************************************************ 477 478 /// Fill `v` with white noise. 479 void whiteNoise(V)(V v) 480 if (isWritableView!V) 481 { 482 import std.random; 483 alias COLOR = ViewColor!V; 484 485 for (xy_t y=0;y<v.h/2;y++) 486 for (xy_t x=0;x<v.w/2;x++) 487 v[x*2, y*2] = COLOR.monochrome(uniform!(COLOR.ChannelType)()); 488 489 // interpolate 490 enum AVERAGE = q{(a+b)/2}; 491 492 for (xy_t y=0;y<v.h/2;y++) 493 for (xy_t x=0;x<v.w/2-1;x++) 494 v[x*2+1, y*2 ] = COLOR.op!AVERAGE(v[x*2 , y*2], v[x*2+2, y*2 ]); 495 for (xy_t y=0;y<v.h/2-1;y++) 496 for (xy_t x=0;x<v.w/2;x++) 497 v[x*2 , y*2+1] = COLOR.op!AVERAGE(v[x*2 , y*2], v[x*2 , y*2+2]); 498 for (xy_t y=0;y<v.h/2-1;y++) 499 for (xy_t x=0;x<v.w/2-1;x++) 500 v[x*2+1, y*2+1] = COLOR.op!AVERAGE(v[x*2+1, y*2], v[x*2+2, y*2+2]); 501 } 502 503 private template softRoundShape(bool RING) 504 { 505 void softRoundShape(T, V, COLOR)(auto ref V v, T x, T y, T r0, T r1, T r2, COLOR color) 506 if (isWritableView!V && isNumeric!T && is(COLOR : ViewColor!V)) 507 { 508 mixin FixMath; 509 510 assert(r0 <= r1); 511 assert(r1 <= r2); 512 assert(r2 < 256); // precision constraint - see SqrType 513 //xy_t ix = cast(xy_t)x; 514 //xy_t iy = cast(xy_t)y; 515 //xy_t ir1 = cast(xy_t)sqr(r1-1); 516 //xy_t ir2 = cast(xy_t)sqr(r2+1); 517 xy_t x1 = cast(xy_t)(x-r2-1); if (x1<0) x1=0; 518 xy_t y1 = cast(xy_t)(y-r2-1); if (y1<0) y1=0; 519 xy_t x2 = cast(xy_t)(x+r2+1); if (x2>v.w) x2 = v.w; 520 xy_t y2 = cast(xy_t)(y+r2+1); if (y2>v.h) y2 = v.h; 521 522 static if (RING) 523 auto r0s = r0*r0; 524 auto r1s = r1*r1; 525 auto r2s = r2*r2; 526 //float rds = r2s - r1s; 527 528 fix fx = tofix(x); 529 fix fy = tofix(y); 530 531 static if (RING) 532 fix fr0s = tofix(r0s); 533 fix fr1s = tofix(r1s); 534 fix fr2s = tofix(r2s); 535 536 static if (RING) 537 fix fr10 = fr1s - fr0s; 538 fix fr21 = fr2s - fr1s; 539 540 for (xy_t cy=y1;cy<y2;cy++) 541 { 542 auto row = v[cy]; 543 for (xy_t cx=x1;cx<x2;cx++) 544 { 545 alias SignedBitsType!(2*(8 + COLOR.channelBits)) SqrType; // fit the square of radius expressed as fixed-point 546 fix frs = cast(fix)((sqr(cast(SqrType)fx-tofix(cx)) + sqr(cast(SqrType)fy-tofix(cy))) >> COLOR.channelBits); // shift-right only once instead of once-per-sqr 547 548 //static frac alphafunc(frac x) { return fracsqr(x); } 549 static frac alphafunc(frac x) { return x; } 550 551 static if (RING) 552 { 553 if (frs<fr0s) 554 {} 555 else 556 if (frs<fr2s) 557 { 558 frac alpha; 559 if (frs<fr1s) 560 alpha = alphafunc(cast(frac)fixdiv(frs-fr0s, fr10)); 561 else 562 alpha = alphafunc(cast(frac)fixdiv(frs-fr1s, fr21)).flipBits; 563 row[cx] = COLOR.op!q{.blend(a, b, c)}(color, row[cx], alpha); 564 } 565 } 566 else 567 { 568 if (frs<fr1s) 569 row[cx] = color; 570 else 571 if (frs<fr2s) 572 { 573 frac alpha = alphafunc(cast(frac)fixdiv(frs-fr1s, fr21)).flipBits; 574 row[cx] = COLOR.op!q{.blend(a, b, c)}(color, row[cx], alpha); 575 } 576 } 577 } 578 } 579 } 580 } 581 582 /// Draw a circle using a smooth interpolated line. 583 void softRing(T, V, COLOR)(auto ref V v, T x, T y, T r0, T r1, T r2, COLOR color) 584 if (isWritableView!V && isNumeric!T && is(COLOR : ViewColor!V)) 585 { 586 v.softRoundShape!true(x, y, r0, r1, r2, color); 587 } 588 589 /// ditto 590 void softCircle(T, V, COLOR)(auto ref V v, T x, T y, T r1, T r2, COLOR color) 591 if (isWritableView!V && isNumeric!T && is(COLOR : ViewColor!V)) 592 { 593 v.softRoundShape!false(x, y, cast(T)0, r1, r2, color); 594 } 595 596 /// Draw a 1x1 rectangle at fractional coordinates. 597 /// Affects up to 4 pixels in the image. 598 template aaPutPixel(bool CHECKED=true, bool USE_ALPHA=true) 599 { 600 void aaPutPixel(F:float, V, COLOR, frac)(auto ref V v, F x, F y, COLOR color, frac alpha) 601 if (isWritableView!V && is(COLOR : ViewColor!V)) 602 { 603 mixin FixMath; 604 605 void plot(bool CHECKED2)(xy_t x, xy_t y, frac f) 606 { 607 static if (CHECKED2) 608 if (x<0 || x>=v.w || y<0 || y>=v.h) 609 return; 610 611 COLOR* p = v.pixelPtr(x, y); 612 static if (USE_ALPHA) f = fracmul(f, cast(frac)alpha); 613 *p = COLOR.op!q{.blend(a, b, c)}(color, *p, f); 614 } 615 616 fix fx = tofix(x); 617 fix fy = tofix(y); 618 int ix = fixto!int(fx); 619 int iy = fixto!int(fy); 620 static if (CHECKED) 621 if (ix>=0 && iy>=0 && ix+1<v.w && iy+1<v.h) 622 { 623 plot!false(ix , iy , fracmul(fixfpart(fx).flipBits, fixfpart(fy).flipBits)); 624 plot!false(ix , iy+1, fracmul(fixfpart(fx).flipBits, fixfpart(fy) )); 625 plot!false(ix+1, iy , fracmul(fixfpart(fx) , fixfpart(fy).flipBits)); 626 plot!false(ix+1, iy+1, fracmul(fixfpart(fx) , fixfpart(fy) )); 627 return; 628 } 629 plot!CHECKED(ix , iy , fracmul(fixfpart(fx).flipBits, fixfpart(fy).flipBits)); 630 plot!CHECKED(ix , iy+1, fracmul(fixfpart(fx).flipBits, fixfpart(fy) )); 631 plot!CHECKED(ix+1, iy , fracmul(fixfpart(fx) , fixfpart(fy).flipBits)); 632 plot!CHECKED(ix+1, iy+1, fracmul(fixfpart(fx) , fixfpart(fy) )); 633 } 634 } 635 636 /// ditto 637 void aaPutPixel(bool CHECKED=true, F:float, V, COLOR)(auto ref V v, F x, F y, COLOR color) 638 if (isWritableView!V && is(COLOR : ViewColor!V)) 639 { 640 //aaPutPixel!(false, F)(x, y, color, 0); // doesn't work, wtf 641 alias aaPutPixel!(CHECKED, false) f; 642 f(v, x, y, color, 0); 643 } 644 645 /// Draw a horizontal line 646 void hline(bool CHECKED=true, V, COLOR, frac)(auto ref V v, xy_t x1, xy_t x2, xy_t y, COLOR color, frac alpha) 647 if (isWritableView!V && is(COLOR : ViewColor!V)) 648 { 649 mixin(CheckHLine); 650 651 if (alpha==0) 652 return; 653 else 654 if (alpha==frac.max) 655 .hline!CHECKED(v, x1, x2, y, color); 656 else 657 foreach (ref s; v.scanline(y)[x1..x2]) 658 foreach (ref p; s) 659 p = COLOR.op!q{.blend(a, b, c)}(color, p, alpha); 660 } 661 662 /// Draw a vertical line 663 void vline(bool CHECKED=true, V, COLOR, frac)(auto ref V v, xy_t x, xy_t y1, xy_t y2, COLOR color, frac alpha) 664 if (isWritableView!V && is(COLOR : ViewColor!V)) 665 { 666 mixin(CheckVLine); 667 668 if (alpha==0) 669 return; 670 else 671 if (alpha==frac.max) 672 foreach (y; y1..y2) 673 v[x, y] = color; 674 else 675 foreach (y; y1..y2) 676 { 677 auto p = v.pixelPtr(x, y); 678 *p = COLOR.op!q{.blend(a, b, c)}(color, *p, alpha); 679 } 680 } 681 682 /// Draw a filled rectangle at fractional coordinates. 683 /// Edges are anti-aliased. 684 void aaFillRect(bool CHECKED=true, F:float, V, COLOR)(auto ref V v, F x1, F y1, F x2, F y2, COLOR color) 685 if (isWritableView!V && is(COLOR : ViewColor!V)) 686 { 687 mixin FixMath; 688 689 sort2(x1, x2); 690 sort2(y1, y2); 691 fix x1f = tofix(x1); int x1i = fixto!int(x1f); 692 fix y1f = tofix(y1); int y1i = fixto!int(y1f); 693 fix x2f = tofix(x2); int x2i = fixto!int(x2f); 694 fix y2f = tofix(y2); int y2i = fixto!int(y2f); 695 696 v.vline!CHECKED(x1i, y1i+1, y2i, color, fixfpart(x1f).flipBits); 697 v.vline!CHECKED(x2i, y1i+1, y2i, color, fixfpart(x2f) ); 698 v.hline!CHECKED(x1i+1, x2i, y1i, color, fixfpart(y1f).flipBits); 699 v.hline!CHECKED(x1i+1, x2i, y2i, color, fixfpart(y2f) ); 700 v.aaPutPixel!CHECKED(x1i, y1i, color, fracmul(fixfpart(x1f).flipBits, fixfpart(y1f).flipBits)); 701 v.aaPutPixel!CHECKED(x1i, y2i, color, fracmul(fixfpart(x1f).flipBits, fixfpart(y2f) )); 702 v.aaPutPixel!CHECKED(x2i, y1i, color, fracmul(fixfpart(x2f) , fixfpart(y1f).flipBits)); 703 v.aaPutPixel!CHECKED(x2i, y2i, color, fracmul(fixfpart(x2f) , fixfpart(y2f) )); 704 705 v.fillRect!CHECKED(x1i+1, y1i+1, x2i, y2i, color); 706 } 707 708 /// Draw an anti-aliased line. 709 void aaLine(bool CHECKED=true, V, COLOR)(auto ref V v, float x1, float y1, float x2, float y2, COLOR color) 710 if (isWritableView!V && is(COLOR : ViewColor!V)) 711 { 712 import std.math : abs; 713 714 // Simplistic straight-forward implementation. TODO: optimize 715 if (abs(x1-x2) > abs(y1-y2)) 716 for (auto x=x1; sign(x1-x2)!=sign(x2-x); x += sign(x2-x1)) 717 v.aaPutPixel!CHECKED(x, itpl(y1, y2, x, x1, x2), color); 718 else 719 for (auto y=y1; sign(y1-y2)!=sign(y2-y); y += sign(y2-y1)) 720 v.aaPutPixel!CHECKED(itpl(x1, x2, y, y1, y2), y, color); 721 } 722 723 /// ditto 724 void aaLine(bool CHECKED=true, V, COLOR, frac)(auto ref V v, float x1, float y1, float x2, float y2, COLOR color, frac alpha) 725 if (isWritableView!V && is(COLOR : ViewColor!V)) 726 { 727 import std.math : abs; 728 729 // ditto 730 if (abs(x1-x2) > abs(y1-y2)) 731 for (auto x=x1; sign(x1-x2)!=sign(x2-x); x += sign(x2-x1)) 732 v.aaPutPixel!CHECKED(x, itpl(y1, y2, x, x1, x2), color, alpha); 733 else 734 for (auto y=y1; sign(y1-y2)!=sign(y2-y); y += sign(y2-y1)) 735 v.aaPutPixel!CHECKED(itpl(x1, x2, y, y1, y2), y, color, alpha); 736 } 737 738 unittest 739 { 740 // Test instantiation 741 import ae.utils.graphics.color; 742 auto i = Image!RGB(100, 100); 743 auto c = RGB(1, 2, 3); 744 i.whiteNoise(); 745 i.aaLine(10, 10, 20, 20, c); 746 i.aaLine(10f, 10f, 20f, 20f, c, 100); 747 i.rect(10, 10, 20, 20, c); 748 i.fillRect(10, 10, 20, 20, c); 749 i.aaFillRect(10, 10, 20, 20, c); 750 i.vline(10, 10, 20, c); 751 i.vline(10, 10, 20, c); 752 i.line(10, 10, 20, 20, c); 753 i.fillCircle(10, 10, 10, c); 754 i.fillSector(10, 10, 10, 10, 0.0, TAU, c); 755 i.softRing(50, 50, 10, 15, 20, c); 756 i.softCircle(50, 50, 10, 15, c); 757 i.fillPoly([Coord(10, 10), Coord(10, 20), Coord(20, 20)], c); 758 i.uncheckedFloodFill(15, 15, RGB(4, 5, 6)); 759 } 760 761 // ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 762 763 // Outdated code from before the overhaul. 764 // TODO: fix and update 765 766 version(none): 767 768 /// Draws an image. Returns this. 769 typeof(this) draw(bool CHECKED=true, SRCCANVAS)(xy_t x, xy_t y, SRCCANVAS v) 770 if (IsCanvas!SRCCANVAS && is(COLOR == SRCCANVAS.COLOR)) 771 { 772 static if (CHECKED) 773 { 774 if (v.w == 0 || v.h == 0 || 775 x+v.w <= 0 || y+v.h <= 0 || x >= w || y >= h) 776 return this; 777 778 auto r = v.window(0, 0, v.w, v.h); 779 if (x < 0) 780 r = r.window(-x, 0, r.w, r.h), 781 x = 0; 782 if (y < 0) 783 r = r.window(0, -y, r.w, r.h), 784 y = 0; 785 if (x+r.w > w) 786 r = r.window(0, 0, w-x, r.h); 787 if (y+r.h > h) 788 r = r.window(0, 0, r.w, h-y); 789 790 draw!false(x, y, r); 791 } 792 else 793 { 794 assert(v.w > 0 && v.h > 0); 795 assert(x >= 0 && x+v.w <= w && y >= 0 && y+v.h <= h); 796 797 // TODO: alpha blending 798 size_t dstStart = y*stride+x, srcStart = 0; 799 foreach (j; 0..v.h) 800 pixels[dstStart..dstStart+v.w] = v.pixels[srcStart..srcStart+v.w], 801 dstStart += stride, 802 srcStart += v.stride; 803 } 804 805 return this; 806 } 807 808 /// Downscale an image minding subpixel positioning on LCD screens. 809 void subpixelDownscale()() 810 if (structFields!COLOR == ["r","g","b"] || structFields!COLOR == ["b","g","r"]) 811 { 812 Image!COLOR i; 813 i.size(HRX + hr.w*3 + HRX, hr.h); 814 i.draw(0, 0, hr.window(0, 0, HRX, hr.h)); 815 i.window(HRX, 0, HRX+hr.w*3, hr.h).upscaleDraw!(3, 1)(hr); 816 i.draw(HRX + hr.w*3, 0, hr.window(hr.w-HRX, 0, hr.w, hr.h)); 817 alias Color!(COLOR.BaseType, "g") BASE; 818 Image!BASE[3] channels; 819 Image!BASE scratch; 820 scratch.size(hr.w*3, hr.h); 821 822 foreach (xy_t cx, char c; valueTuple!('r', 'g', 'b')) 823 { 824 auto w = i.window(cx*HRX, 0, cx*HRX+hr.w*3, hr.h); 825 scratch.transformDraw!(`COLOR(c.`~c~`)`)(0, 0, w); 826 channels[cx].size(lr.w, lr.h); 827 channels[cx].downscaleDraw!(3*HRX, HRY)(scratch); 828 } 829 830 foreach (y; 0..lr.h) 831 foreach (x; 0..lr.w) 832 { 833 COLOR c; 834 c.r = channels[0][x, y].g; 835 c.g = channels[1][x, y].g; 836 c.b = channels[2][x, y].g; 837 lr[x, y] = c; 838 } 839 }