| 1 | // Aseprite |
| 2 | // Copyright (C) 2019-2022 Igara Studio S.A. |
| 3 | // Copyright (C) 2001-2017 David Capello |
| 4 | // |
| 5 | // This program is distributed under the terms of |
| 6 | // the End-User License Agreement for Aseprite. |
| 7 | |
| 8 | #include "app/util/wrap_point.h" |
| 9 | |
| 10 | #include "app/tools/ink.h" |
| 11 | #include "doc/algorithm/flip_image.h" |
| 12 | #include "render/gradient.h" |
| 13 | |
| 14 | #include <array> |
| 15 | #include <memory> |
| 16 | |
| 17 | namespace app { |
| 18 | namespace tools { |
| 19 | |
| 20 | class NonePointShape : public PointShape { |
| 21 | public: |
| 22 | void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) override { |
| 23 | // Do nothing |
| 24 | } |
| 25 | |
| 26 | void getModifiedArea(ToolLoop* loop, int x, int y, Rect& area) override { |
| 27 | // Do nothing |
| 28 | } |
| 29 | }; |
| 30 | |
| 31 | class PixelPointShape : public PointShape { |
| 32 | public: |
| 33 | bool isPixel() override { return true; } |
| 34 | |
| 35 | void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) override { |
| 36 | loop->getInk()->prepareForPointShape(loop, true, pt.x, pt.y); |
| 37 | doInkHline(pt.x, pt.y, pt.x, loop); |
| 38 | } |
| 39 | |
| 40 | void getModifiedArea(ToolLoop* loop, int x, int y, Rect& area) override { |
| 41 | area = Rect(x, y, 1, 1); |
| 42 | } |
| 43 | }; |
| 44 | |
| 45 | class TilePointShape : public PointShape { |
| 46 | public: |
| 47 | bool isPixel() override { return true; } |
| 48 | bool isTile() override { return true; } |
| 49 | |
| 50 | void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) override { |
| 51 | const doc::Grid& grid = loop->getGrid(); |
| 52 | gfx::Point newPos = grid.canvasToTile(pt.toPoint()); |
| 53 | |
| 54 | loop->getInk()->prepareForPointShape(loop, true, newPos.x, newPos.y); |
| 55 | doInkHline(newPos.x, newPos.y, newPos.x, loop); |
| 56 | } |
| 57 | |
| 58 | void getModifiedArea(ToolLoop* loop, int x, int y, Rect& area) override { |
| 59 | const doc::Grid& grid = loop->getGrid(); |
| 60 | area = grid.alignBounds(Rect(x, y, 1, 1)); |
| 61 | } |
| 62 | }; |
| 63 | |
| 64 | class BrushPointShape : public PointShape { |
| 65 | bool m_firstPoint; |
| 66 | Brush* m_lastBrush; |
| 67 | BrushType m_origBrushType; |
| 68 | std::array<std::shared_ptr<CompressedImage>, 4> m_compressedImages; |
| 69 | // For dynamics |
| 70 | DynamicsOptions m_dynamics; |
| 71 | bool m_useDynamics; |
| 72 | bool m_hasDynamicGradient; |
| 73 | color_t m_primaryColor; |
| 74 | color_t m_secondaryColor; |
| 75 | float m_lastGradientValue; |
| 76 | |
| 77 | public: |
| 78 | |
| 79 | void preparePointShape(ToolLoop* loop) override { |
| 80 | m_firstPoint = true; |
| 81 | m_lastBrush = nullptr; |
| 82 | m_origBrushType = loop->getBrush()->type(); |
| 83 | |
| 84 | m_dynamics = loop->getDynamics(); |
| 85 | m_useDynamics = (m_dynamics.isDynamic() && |
| 86 | // TODO support custom brushes in future versions |
| 87 | m_origBrushType != kImageBrushType); |
| 88 | |
| 89 | // For dynamic gradient |
| 90 | m_hasDynamicGradient = (m_dynamics.gradient != DynamicSensor::Static); |
| 91 | if (m_hasDynamicGradient && |
| 92 | m_dynamics.colorFromTo == ColorFromTo::FgToBg) { |
| 93 | m_primaryColor = loop->getSecondaryColor(); |
| 94 | m_secondaryColor = loop->getPrimaryColor(); |
| 95 | } |
| 96 | else { |
| 97 | m_primaryColor = loop->getPrimaryColor(); |
| 98 | m_secondaryColor = loop->getSecondaryColor(); |
| 99 | } |
| 100 | m_lastGradientValue = -1; |
| 101 | } |
| 102 | |
| 103 | void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) override { |
| 104 | int x = pt.x; |
| 105 | int y = pt.y; |
| 106 | |
| 107 | Ink* ink = loop->getInk(); |
| 108 | Brush* brush = loop->getBrush(); |
| 109 | |
| 110 | // Dynamics |
| 111 | if (m_useDynamics) { |
| 112 | // Dynamic gradient info |
| 113 | if (m_hasDynamicGradient && |
| 114 | m_dynamics.ditheringMatrix.rows() == 1 && |
| 115 | m_dynamics.ditheringMatrix.cols() == 1) { |
| 116 | color_t a = m_secondaryColor; |
| 117 | color_t b = m_primaryColor; |
| 118 | const float t = pt.gradient; |
| 119 | const float ti = 1.0f - pt.gradient; |
| 120 | |
| 121 | auto rgbaGradient = [t, ti](color_t a, color_t b) -> color_t { |
| 122 | if (rgba_geta(a) == 0) |
| 123 | return doc::rgba(rgba_getr(b), |
| 124 | rgba_getg(b), |
| 125 | rgba_getb(b), |
| 126 | int(t*rgba_geta(b))); |
| 127 | else if (rgba_geta(b) == 0) |
| 128 | return doc::rgba(rgba_getr(a), |
| 129 | rgba_getg(a), |
| 130 | rgba_getb(a), |
| 131 | int(ti*rgba_geta(a))); |
| 132 | else |
| 133 | return doc::rgba(int(ti*rgba_getr(a) + t*rgba_getr(b)), |
| 134 | int(ti*rgba_getg(a) + t*rgba_getg(b)), |
| 135 | int(ti*rgba_getb(a) + t*rgba_getb(b)), |
| 136 | int(ti*rgba_geta(a) + t*rgba_geta(b))); |
| 137 | }; |
| 138 | |
| 139 | switch (loop->sprite()->pixelFormat()) { |
| 140 | case IMAGE_RGB: |
| 141 | a = rgbaGradient(a, b); |
| 142 | break; |
| 143 | case IMAGE_GRAYSCALE: |
| 144 | if (graya_geta(a) == 0) |
| 145 | a = doc::graya(graya_getv(b), |
| 146 | int(t*graya_geta(b))); |
| 147 | else if (graya_geta(b) == 0) |
| 148 | a = doc::graya(graya_getv(a), |
| 149 | int(ti*graya_geta(a))); |
| 150 | else |
| 151 | a = doc::graya(int(ti*graya_getv(a) + t*graya_getv(b)), |
| 152 | int(ti*graya_geta(a) + t*graya_geta(b))); |
| 153 | break; |
| 154 | case IMAGE_INDEXED: { |
| 155 | int maskIndex = (loop->getLayer()->isBackground() ? -1: loop->sprite()->transparentColor()); |
| 156 | // Convert index to RGBA |
| 157 | if (a == maskIndex) a = 0; |
| 158 | else a = loop->getPalette()->getEntry(a); |
| 159 | if (b == maskIndex) b = 0; |
| 160 | else b = loop->getPalette()->getEntry(b); |
| 161 | // Same as in RGBA gradient |
| 162 | a = rgbaGradient(a, b); |
| 163 | // Convert RGBA to index |
| 164 | a = loop->getRgbMap()->mapColor(rgba_getr(a), |
| 165 | rgba_getg(a), |
| 166 | rgba_getb(a), |
| 167 | rgba_geta(a)); |
| 168 | break; |
| 169 | } |
| 170 | } |
| 171 | loop->setPrimaryColor(a); |
| 172 | } |
| 173 | |
| 174 | // Dynamic size and angle |
| 175 | int size = std::clamp(int(pt.size), int(Brush::kMinBrushSize), int(Brush::kMaxBrushSize)); |
| 176 | int angle = std::clamp(int(pt.angle), -180, 180); |
| 177 | if ((brush->size() != size) || |
| 178 | (brush->angle() != angle && m_origBrushType != kCircleBrushType) || |
| 179 | (m_hasDynamicGradient && pt.gradient != m_lastGradientValue)) { |
| 180 | // TODO cache brushes |
| 181 | BrushRef newBrush = std::make_shared<Brush>( |
| 182 | m_origBrushType, size, angle); |
| 183 | |
| 184 | // Dynamic gradient with dithering |
| 185 | bool prepareInk = false; |
| 186 | if (m_hasDynamicGradient && !ink->isEraser() && |
| 187 | (m_dynamics.ditheringMatrix.rows() > 1 || |
| 188 | m_dynamics.ditheringMatrix.cols() > 1)) { |
| 189 | convert_bitmap_brush_to_dithering_brush( |
| 190 | newBrush.get(), |
| 191 | loop->sprite()->pixelFormat(), |
| 192 | m_dynamics.ditheringMatrix, |
| 193 | pt.gradient, |
| 194 | m_secondaryColor, |
| 195 | m_primaryColor); |
| 196 | prepareInk = true; |
| 197 | } |
| 198 | m_lastGradientValue = pt.gradient; |
| 199 | |
| 200 | loop->setBrush(newBrush); |
| 201 | brush = loop->getBrush(); |
| 202 | |
| 203 | if (prepareInk) { |
| 204 | // Prepare ink for the new brush |
| 205 | ink->prepareInk(loop); |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | // TODO cache compressed images (or remove them completelly) |
| 211 | if (m_lastBrush != brush) { |
| 212 | m_lastBrush = brush; |
| 213 | m_compressedImages.fill(nullptr); |
| 214 | } |
| 215 | |
| 216 | x += brush->bounds().x; |
| 217 | y += brush->bounds().y; |
| 218 | |
| 219 | if (m_firstPoint) { |
| 220 | if ((brush->type() == kImageBrushType) && |
| 221 | (brush->pattern() == BrushPattern::ALIGNED_TO_DST || |
| 222 | brush->pattern() == BrushPattern::PAINT_BRUSH)) { |
| 223 | brush->setPatternOrigin(gfx::Point(x, y)); |
| 224 | } |
| 225 | } |
| 226 | else { |
| 227 | if (brush->type() == kImageBrushType && |
| 228 | brush->pattern() == BrushPattern::PAINT_BRUSH) { |
| 229 | brush->setPatternOrigin(gfx::Point(x, y)); |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | if (int(loop->getTiledMode()) & int(TiledMode::X_AXIS)) { |
| 234 | int wrappedPatternOriginX = wrap_value(brush->patternOrigin().x, loop->sprite()->width()) % brush->bounds().w; |
| 235 | brush->setPatternOrigin(gfx::Point(wrappedPatternOriginX, brush->patternOrigin().y)); |
| 236 | x = wrap_value(x, loop->sprite()->width()); |
| 237 | } |
| 238 | if (int(loop->getTiledMode()) & int(TiledMode::Y_AXIS)) { |
| 239 | int wrappedPatternOriginY = wrap_value(brush->patternOrigin().y, loop->sprite()->height()) % brush->bounds().h; |
| 240 | brush->setPatternOrigin(gfx::Point(brush->patternOrigin().x, wrappedPatternOriginY)); |
| 241 | y = wrap_value(y, loop->sprite()->height()); |
| 242 | } |
| 243 | |
| 244 | ink->prepareForPointShape(loop, m_firstPoint, x, y); |
| 245 | |
| 246 | for (auto scanline : getCompressedImage(pt.symmetry)) { |
| 247 | int u = x+scanline.x; |
| 248 | ink->prepareVForPointShape(loop, y+scanline.y); |
| 249 | doInkHline(u, y+scanline.y, u+scanline.w-1, loop); |
| 250 | } |
| 251 | m_firstPoint = false; |
| 252 | } |
| 253 | |
| 254 | void getModifiedArea(ToolLoop* loop, int x, int y, Rect& area) override { |
| 255 | area = loop->getBrush()->bounds(); |
| 256 | area.x += x; |
| 257 | area.y += y; |
| 258 | } |
| 259 | |
| 260 | private: |
| 261 | CompressedImage& getCompressedImage(gen::SymmetryMode symmetryMode) { |
| 262 | auto& compressPtr = m_compressedImages[int(symmetryMode)]; |
| 263 | if (!compressPtr) { |
| 264 | switch (symmetryMode) { |
| 265 | case gen::SymmetryMode::NONE: { |
| 266 | compressPtr.reset(new CompressedImage(m_lastBrush->image(), |
| 267 | m_lastBrush->maskBitmap(), |
| 268 | false)); |
| 269 | break; |
| 270 | } |
| 271 | case gen::SymmetryMode::HORIZONTAL: |
| 272 | case gen::SymmetryMode::VERTICAL: { |
| 273 | std::unique_ptr<Image> tempImage(Image::createCopy(m_lastBrush->image())); |
| 274 | doc::algorithm::FlipType flip = |
| 275 | (symmetryMode == gen::SymmetryMode::HORIZONTAL)? |
| 276 | doc::algorithm::FlipType::FlipHorizontal: |
| 277 | doc::algorithm::FlipType::FlipVertical; |
| 278 | doc::algorithm::flip_image(tempImage.get(), tempImage->bounds(), flip); |
| 279 | compressPtr.reset(new CompressedImage(tempImage.get(), |
| 280 | m_lastBrush->maskBitmap(), |
| 281 | false)); |
| 282 | break; |
| 283 | } |
| 284 | case gen::SymmetryMode::BOTH: { |
| 285 | std::unique_ptr<Image> tempImage(Image::createCopy(m_lastBrush->image())); |
| 286 | doc::algorithm::flip_image(tempImage.get(), |
| 287 | tempImage->bounds(), |
| 288 | doc::algorithm::FlipType::FlipVertical); |
| 289 | doc::algorithm::flip_image(tempImage.get(), |
| 290 | tempImage->bounds(), |
| 291 | doc::algorithm::FlipType::FlipHorizontal); |
| 292 | compressPtr.reset(new CompressedImage(tempImage.get(), |
| 293 | m_lastBrush->maskBitmap(), |
| 294 | false)); |
| 295 | break; |
| 296 | } |
| 297 | } |
| 298 | } |
| 299 | return *compressPtr; |
| 300 | } |
| 301 | }; |
| 302 | |
| 303 | class FloodFillPointShape : public PointShape { |
| 304 | public: |
| 305 | bool isFloodFill() override { return true; } |
| 306 | |
| 307 | void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) override { |
| 308 | const doc::Image* srcImage = loop->getFloodFillSrcImage(); |
| 309 | const bool tilesMode = (srcImage->pixelFormat() == IMAGE_TILEMAP); |
| 310 | gfx::Point wpt = pt.toPoint(); |
| 311 | if (tilesMode) { // Tiles mode |
| 312 | const doc::Grid& grid = loop->getGrid(); |
| 313 | wpt = grid.canvasToTile(wpt); |
| 314 | } |
| 315 | else { |
| 316 | wpt = wrap_point(loop->getTiledMode(), |
| 317 | gfx::Size(srcImage->width(), |
| 318 | srcImage->height()), |
| 319 | wpt, true); |
| 320 | } |
| 321 | |
| 322 | loop->getInk()->prepareForPointShape(loop, true, wpt.x, wpt.y); |
| 323 | |
| 324 | doc::algorithm::floodfill( |
| 325 | srcImage, |
| 326 | (loop->useMask() ? loop->getMask(): nullptr), |
| 327 | wpt.x, wpt.y, |
| 328 | (tilesMode ? srcImage->bounds(): |
| 329 | floodfillBounds(loop, wpt.x, wpt.y)), |
| 330 | get_pixel(srcImage, wpt.x, wpt.y), |
| 331 | loop->getTolerance(), |
| 332 | loop->getContiguous(), |
| 333 | loop->isPixelConnectivityEightConnected(), |
| 334 | loop, (AlgoHLine)doInkHline); |
| 335 | } |
| 336 | |
| 337 | void getModifiedArea(ToolLoop* loop, int x, int y, Rect& area) override { |
| 338 | area = floodfillBounds(loop, x, y); |
| 339 | } |
| 340 | |
| 341 | private: |
| 342 | gfx::Rect floodfillBounds(ToolLoop* loop, int x, int y) const { |
| 343 | const doc::Image* srcImage = loop->getFloodFillSrcImage(); |
| 344 | gfx::Rect bounds = loop->sprite()->bounds(); |
| 345 | bounds &= srcImage->bounds(); |
| 346 | |
| 347 | if (srcImage->pixelFormat() == IMAGE_TILEMAP) { // Tiles mode |
| 348 | const doc::Grid& grid = loop->getGrid(); |
| 349 | bounds = grid.tileToCanvas(bounds); |
| 350 | } |
| 351 | // Limit the flood-fill to the current tile if the grid is visible. |
| 352 | else if (loop->getStopAtGrid()) { |
| 353 | gfx::Rect grid = loop->getGridBounds(); |
| 354 | if (!grid.isEmpty()) { |
| 355 | div_t d, dx, dy; |
| 356 | |
| 357 | dx = div(grid.x, grid.w); |
| 358 | dy = div(grid.y, grid.h); |
| 359 | |
| 360 | if (dx.rem > 0) dx.rem -= grid.w; |
| 361 | if (dy.rem > 0) dy.rem -= grid.h; |
| 362 | |
| 363 | d = div(x-dx.rem, grid.w); |
| 364 | x = dx.rem + d.quot*grid.w; |
| 365 | |
| 366 | d = div(y-dy.rem, grid.h); |
| 367 | y = dy.rem + d.quot*grid.h; |
| 368 | |
| 369 | bounds = bounds.createIntersection(gfx::Rect(x, y, grid.w, grid.h)); |
| 370 | } |
| 371 | } |
| 372 | |
| 373 | return bounds; |
| 374 | } |
| 375 | }; |
| 376 | |
| 377 | class SprayPointShape : public PointShape { |
| 378 | BrushPointShape m_subPointShape; |
| 379 | float m_pointRemainder = 0; |
| 380 | |
| 381 | public: |
| 382 | |
| 383 | bool isSpray() override { return true; } |
| 384 | |
| 385 | void preparePointShape(ToolLoop* loop) override { |
| 386 | m_subPointShape.preparePointShape(loop); |
| 387 | } |
| 388 | |
| 389 | void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) override { |
| 390 | loop->getInk()->prepareForPointShape(loop, true, pt.x, pt.y); |
| 391 | |
| 392 | int spray_width = loop->getSprayWidth(); |
| 393 | int spray_speed = loop->getSpraySpeed(); |
| 394 | |
| 395 | // The number of points to spray is proportional to the spraying area, and |
| 396 | // we calculate it as a float to handle very low spray rates properly. |
| 397 | float points_to_spray = (spray_width * spray_width / 4.0f) * spray_speed / 100.0f; |
| 398 | |
| 399 | // We add the fractional points from last time to get |
| 400 | // the total number of points to paint this time. |
| 401 | points_to_spray += m_pointRemainder; |
| 402 | int integral_points = (int)points_to_spray; |
| 403 | |
| 404 | // Save any leftover fraction of a point for next time. |
| 405 | m_pointRemainder = points_to_spray - integral_points; |
| 406 | ASSERT(m_pointRemainder >= 0 && m_pointRemainder < 1.0f); |
| 407 | |
| 408 | double angle, radius; |
| 409 | |
| 410 | for (int c=0; c<integral_points; c++) { |
| 411 | angle = 360.0 * rand() / RAND_MAX; |
| 412 | radius = double(spray_width) * rand() / RAND_MAX; |
| 413 | |
| 414 | Stroke::Pt pt2(pt); |
| 415 | pt2.x += double(radius * std::cos(angle)); |
| 416 | pt2.y += double(radius * std::sin(angle)); |
| 417 | m_subPointShape.transformPoint(loop, pt2); |
| 418 | } |
| 419 | } |
| 420 | |
| 421 | void getModifiedArea(ToolLoop* loop, int x, int y, Rect& area) override { |
| 422 | int spray_width = loop->getSprayWidth(); |
| 423 | Point p1(x-spray_width, y-spray_width); |
| 424 | Point p2(x+spray_width, y+spray_width); |
| 425 | |
| 426 | Rect area1; |
| 427 | Rect area2; |
| 428 | m_subPointShape.getModifiedArea(loop, p1.x, p1.y, area1); |
| 429 | m_subPointShape.getModifiedArea(loop, p2.x, p2.y, area2); |
| 430 | |
| 431 | area = area1.createUnion(area2); |
| 432 | } |
| 433 | }; |
| 434 | |
| 435 | } // namespace tools |
| 436 | } // namespace app |
| 437 | |