diff --git a/src/app/commands/cmd_change_brush.cpp b/src/app/commands/cmd_change_brush.cpp index 98ed7b755..5430292bd 100644 --- a/src/app/commands/cmd_change_brush.cpp +++ b/src/app/commands/cmd_change_brush.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (c) 2023 Igara Studio S.A. +// Copyright (c) 2023-2024 Igara Studio S.A. // Copyright (C) 2001-2017 David Capello // // This program is distributed under the terms of @@ -156,9 +156,7 @@ void ChangeBrushCommand::onExecute(Context* context) // Create a copy of the brush (to avoid modifying the original // brush from the AppBrushes stock) - BrushRef newBrush = std::make_shared(*brush); - newBrush->setImage(newImg.get(), - newMsk.get()); + BrushRef newBrush = brush->cloneWithExistingImages(newImg, newMsk); contextBar->setActiveBrush(newBrush); } else { @@ -210,9 +208,7 @@ void ChangeBrushCommand::onExecute(Context* context) break; } - BrushRef newBrush = std::make_shared(*brush); - newBrush->setImage(newImg.get(), - newMsk.get()); + BrushRef newBrush = brush->cloneWithExistingImages(newImg, newMsk); contextBar->setActiveBrush(newBrush); } else { @@ -297,10 +293,7 @@ void ChangeBrushCommand::onExecute(Context* context) ImageRef newImg2(crop_image(newImg.get(), cropBounds, bg)); ImageRef newMsk2(crop_image(newMsk.get(), cropBounds, bg)); - - BrushRef newBrush = std::make_shared(*brush); - newBrush->setImage(newImg.get(), - newMsk.get()); + BrushRef newBrush = brush->cloneWithExistingImages(newImg2, newMsk2); contextBar->setActiveBrush(newBrush); } break; diff --git a/src/app/script/brush_class.cpp b/src/app/script/brush_class.cpp index 28fdfaa56..94a3dcab1 100644 --- a/src/app/script/brush_class.cpp +++ b/src/app/script/brush_class.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019-2020 Igara Studio S.A. +// Copyright (C) 2019-2024 Igara Studio S.A. // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -36,7 +36,7 @@ BrushRef Brush_new(lua_State* L, int index) if (auto brush2 = may_get_obj(L, index)) { ASSERT(brush2->brush); if (brush2->brush) - brush.reset(new Brush(*brush2->brush)); + brush = brush2->brush->cloneWithNewImages(); } else if (auto image = may_get_image_from_arg(L, index)) { if (image) { diff --git a/src/app/tools/tool_loop_manager.cpp b/src/app/tools/tool_loop_manager.cpp index 4b660b50c..9238342f1 100644 --- a/src/app/tools/tool_loop_manager.cpp +++ b/src/app/tools/tool_loop_manager.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019-2023 Igara Studio S.A. +// Copyright (C) 2019-2024 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -44,7 +44,8 @@ using namespace filters; ToolLoopManager::ToolLoopManager(ToolLoop* toolLoop) : m_toolLoop(toolLoop) , m_canceled(false) - , m_brush0(*toolLoop->getBrush()) + , m_brushSize0(toolLoop->getBrush()->size()) + , m_brushAngle0(toolLoop->getBrush()->angle()) , m_dynamics(toolLoop->getDynamics()) { } @@ -358,8 +359,8 @@ Stroke::Pt ToolLoopManager::getSpriteStrokePt(const Pointer& pointer) { // Convert the screen point to a sprite point Stroke::Pt spritePoint = pointer.point(); - spritePoint.size = m_brush0.size(); - spritePoint.angle = m_brush0.angle(); + spritePoint.size = m_brushSize0; + spritePoint.angle = m_brushAngle0; // Center the input to some grid point if needed snapToGrid(spritePoint); diff --git a/src/app/tools/tool_loop_manager.h b/src/app/tools/tool_loop_manager.h index 84824a6cc..71be2fcfc 100644 --- a/src/app/tools/tool_loop_manager.h +++ b/src/app/tools/tool_loop_manager.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019-2021 Igara Studio S.A. +// Copyright (C) 2019-2024 Igara Studio S.A. // Copyright (C) 2001-2017 David Capello // // This program is distributed under the terms of @@ -95,7 +95,8 @@ private: Pointer m_lastPointer; gfx::Region m_dirtyArea; gfx::Region m_nextDirtyArea; - doc::Brush m_brush0; + const int m_brushSize0; + const int m_brushAngle0; DynamicsOptions m_dynamics; gfx::PointF m_stabilizerCenter; }; diff --git a/src/app/ui/brush_popup.cpp b/src/app/ui/brush_popup.cpp index b54e35c7b..2c34f74e9 100644 --- a/src/app/ui/brush_popup.cpp +++ b/src/app/ui/brush_popup.cpp @@ -467,7 +467,9 @@ os::SurfaceRef BrushPopup::createSurfaceForBrush(const BrushRef& origBrush, BrushRef brush = origBrush; if (brush) { if (brush->type() != kImageBrushType && brush->size() > kMaxSize) { - brush.reset(new Brush(*brush)); + // Clone with shared images, as setSize() will re-create the + // images and the brush is no kImageBrushType anyway. + brush = brush->cloneWithSharedImages(); brush->setSize(kMaxSize); } // Show the original image in the popup (without the image colors diff --git a/src/app/ui/editor/tool_loop_impl.cpp b/src/app/ui/editor/tool_loop_impl.cpp index 34581998b..deec878ee 100644 --- a/src/app/ui/editor/tool_loop_impl.cpp +++ b/src/app/ui/editor/tool_loop_impl.cpp @@ -190,16 +190,31 @@ public: ASSERT(m_ink); ASSERT(m_controller); - if (m_brush->type() == kImageBrushType && - (m_button == Right || (m_button == Left && m_brush->isMonochromeImage()))) { - m_brush->setImageColor( - Brush::ImageColor::MainColor, + // If the user right-clicks with a custom/image brush we change + // the image's colors of the brush to the background color. + // + // This is different from SwitchColors that makes a new brush + // switching fg <-> bg colors, so here we have some extra + // functionality with custom brushes (quickly convert the custom + // brush with a plain color, or in other words, replace the custom + // brush area with the background color). + if (m_brush->type() == kImageBrushType && m_button == Right) { + // We've to recalculate the background color to use for the + // brush using the specific brush image pixel format/color mode, + // as we cannot use m_primaryColor or m_bgColor here because + // those are in the sprite pixel format/color mode. + const color_t brushColor = color_utils::color_for_target_mask( - (m_button == Left ? Preferences::instance().colorBar.fgColor() : - Preferences::instance().colorBar.bgColor()), + Preferences::instance().colorBar.bgColor(), ColorTarget(ColorTarget::TransparentLayer, m_brush->image()->pixelFormat(), - -1))); + -1)); + + // Clone the brush with new images to avoid modifying the + // current brush used in left-click / brush preview. + BrushRef newBrush = m_brush->cloneWithNewImages(); + newBrush->setImageColor(Brush::ImageColor::BothColors, brushColor); + m_brush = newBrush; } if (m_tilesMode) { diff --git a/src/doc/brush.cpp b/src/doc/brush.cpp index 786bd1a73..b2c2ac5de 100644 --- a/src/doc/brush.cpp +++ b/src/doc/brush.cpp @@ -1,5 +1,5 @@ // Aseprite Document Library -// Copyright (C) 2019-2023 Igara Studio S.A. +// Copyright (C) 2019-2024 Igara Studio S.A. // Copyright (C) 2001-2016 David Capello // // This file is released under the terms of the MIT license. @@ -48,32 +48,43 @@ Brush::Brush(BrushType type, int size, int angle) regenerate(); } -Brush::Brush(const Brush& brush) -{ - m_type = brush.m_type; - m_size = brush.m_size; - m_angle = brush.m_angle; - m_image = brush.m_image; - m_maskBitmap = brush.m_maskBitmap; - m_pattern = brush.m_pattern; - m_patternOrigin = brush.m_patternOrigin; - m_gen = 0; - - regenerate(); -} - Brush::~Brush() { clean(); } -void Brush::setType(BrushType type) +BrushRef Brush::cloneWithSharedImages() const { - m_type = type; - if (m_type != kImageBrushType) - regenerate(); + BrushRef newBrush = std::make_shared(); + newBrush->copyFieldsFromBrush(*this); + return newBrush; +} + +BrushRef Brush::cloneWithNewImages() const +{ + BrushRef newBrush = std::make_shared(); + newBrush->copyFieldsFromBrush(*this); + if (newBrush->m_image) + newBrush->m_image.reset(Image::createCopy(newBrush->m_image.get())); + if (newBrush->m_maskBitmap) + newBrush->m_maskBitmap.reset(Image::createCopy(newBrush->m_maskBitmap.get())); + return newBrush; +} + +BrushRef Brush::cloneWithExistingImages(const ImageRef& image, + const ImageRef& maskBitmap) const +{ + BrushRef newBrush = std::make_shared(); + newBrush->copyFieldsFromBrush(*this); + + newBrush->m_image = image; + if (maskBitmap) + newBrush->m_maskBitmap = maskBitmap; else - clean(); + newBrush->regenerateMaskBitmap(); + + newBrush->resetBounds(); + return newBrush; } void Brush::setSize(int size) @@ -95,16 +106,8 @@ void Brush::setImage(const Image* image, m_image.reset(Image::createCopy(image)); if (maskBitmap) m_maskBitmap.reset(Image::createCopy(maskBitmap)); - else { - int w = image->width(); - int h = image->height(); - m_maskBitmap.reset(Image::create(IMAGE_BITMAP, w, h)); - LockImageBits bits(m_maskBitmap.get()); - auto pos = bits.begin(); - for (int v=0; vmaskColor()); - } + else + regenerateMaskBitmap(); m_backupImage.reset(); m_mainColor.reset(); @@ -234,7 +237,8 @@ static void replace_image_colors_indexed( } } -void Brush::setImageColor(ImageColor imageColor, color_t color) +void Brush::setImageColor(const ImageColor imageColor, + const color_t color) { ASSERT(m_image); if (!m_image) @@ -249,10 +253,13 @@ void Brush::setImageColor(ImageColor imageColor, color_t color) switch (imageColor) { case ImageColor::MainColor: - m_mainColor = color_t(color); + m_mainColor = color; break; case ImageColor::BackgroundColor: - m_bgColor = color_t(color); + m_bgColor = color; + break; + case ImageColor::BothColors: + m_mainColor = m_bgColor = color; break; } @@ -379,6 +386,22 @@ void Brush::regenerate() } } +void Brush::regenerateMaskBitmap() +{ + ASSERT(m_image); + if (!m_image) + return; + + int w = m_image->width(); + int h = m_image->height(); + m_maskBitmap.reset(Image::create(IMAGE_BITMAP, w, h)); + LockImageBits bits(m_maskBitmap.get()); + auto pos = bits.begin(); + for (int v=0; vmaskColor()); +} + void Brush::resetBounds() { m_center = gfx::Point(std::max(0, m_image->width()/2), @@ -388,4 +411,19 @@ void Brush::resetBounds() m_image->height())); } +void Brush::copyFieldsFromBrush(const Brush& brush) +{ + m_type = brush.m_type; + m_size = brush.m_size; + m_angle = brush.m_angle; + m_image = brush.m_image; + m_maskBitmap = brush.m_maskBitmap; + m_bounds = brush.m_bounds; + m_center = brush.m_center; + m_pattern = brush.m_pattern; + m_patternOrigin = brush.m_patternOrigin; + m_patternImage = brush.m_patternImage; + m_gen = 0; +} + } // namespace doc diff --git a/src/doc/brush.h b/src/doc/brush.h index b8b117458..596baddd3 100644 --- a/src/doc/brush.h +++ b/src/doc/brush.h @@ -1,5 +1,5 @@ // Aseprite Document Library -// Copyright (C) 2019-2023 Igara Studio S.A. +// Copyright (C) 2019-2024 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This file is released under the terms of the MIT license. @@ -22,18 +22,34 @@ namespace doc { + class Brush; + using BrushRef = std::shared_ptr; + class Brush { public: static const int kMinBrushSize = 1; static const int kMaxBrushSize = 64; - enum class ImageColor { MainColor, BackgroundColor }; + enum class ImageColor { MainColor, BackgroundColor, BothColors }; Brush(); Brush(BrushType type, int size, int angle); - Brush(const Brush& brush); ~Brush(); + // Don't offer copy constructor/operator, use clone*() functions + // instead. + Brush(const Brush&) = delete; + Brush& operator=(const Brush&) = delete; + + // Cloned brushes can share the same image until + // setSize()/Angle()/etc. (regenerate()) is called for the new + // brush. In that case the original brush and the cloned one will + // have a different image after all. + BrushRef cloneWithSharedImages() const; + BrushRef cloneWithNewImages() const; + BrushRef cloneWithExistingImages(const ImageRef& image, + const ImageRef& maskBitmap) const; + BrushType type() const { return m_type; } int size() const { return m_size; } int angle() const { return m_angle; } @@ -48,7 +64,6 @@ namespace doc { const gfx::Rect& bounds() const { return m_bounds; } const gfx::Point& center() const { return m_center; } - void setType(BrushType type); void setSize(int size); void setAngle(int angle); void setImage(const Image* image, @@ -78,14 +93,12 @@ namespace doc { return m_image.get(); } - const bool isMonochromeImage() const { - return m_mainColor.has_value(); - } - private: void clean(); void regenerate(); + void regenerateMaskBitmap(); void resetBounds(); + void copyFieldsFromBrush(const Brush& brush); BrushType m_type; // Type of brush int m_size; // Size (diameter) @@ -105,8 +118,6 @@ namespace doc { std::optional m_bgColor; // Background color }; - typedef std::shared_ptr BrushRef; - } // namespace doc #endif diff --git a/tests/scripts/brush.lua b/tests/scripts/brush.lua index 736a2c778..b6cbba03d 100644 --- a/tests/scripts/brush.lua +++ b/tests/scripts/brush.lua @@ -1,4 +1,4 @@ --- Copyright (C) 2019-2023 Igara Studio S.A. +-- Copyright (C) 2019-2024 Igara Studio S.A. -- -- This file is released under the terms of the MIT license. -- Read LICENSE.txt for more information. @@ -51,6 +51,7 @@ do assert(b.patternOrigin.y == 0) end +-- Image brush do local rgba = app.pixelColor.rgba local r = rgba(255, 0, 0) @@ -71,6 +72,15 @@ do brush:setBgColor(b) expect_img(brush.image, { b, g, g, b }) + + -- Test copy image brushes + local brush2 = Brush(brush) + expect_img(brush2.image, { b, g, g, b }) + brush2:setFgColor(r) + expect_img(brush2.image, { b, r, r, b }) + brush2:setBgColor(r) + expect_img(brush2.image, { r, r, r, r }) + expect_img(brush.image, { b, g, g, b }) -- First brush wasn't modified end -- Tests with Image Brushes @@ -233,4 +243,4 @@ do expect_img(cel.image, { pal:getColor(1).grayPixel, Color{ gray=222, alpha=222 }.grayPixel, pal:getColor(3).grayPixel, Color{ gray=0, alpha=255 }.grayPixel }) -end \ No newline at end of file +end