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
17namespace app {
18namespace tools {
19
20class NonePointShape : public PointShape {
21public:
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
31class PixelPointShape : public PointShape {
32public:
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
45class TilePointShape : public PointShape {
46public:
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
64class 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
77public:
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
260private:
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
303class FloodFillPointShape : public PointShape {
304public:
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
341private:
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
377class SprayPointShape : public PointShape {
378 BrushPointShape m_subPointShape;
379 float m_pointRemainder = 0;
380
381public:
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