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 | |