mirror of
https://github.com/aseprite/aseprite.git
synced 2025-02-06 03:39:51 +00:00
Fix brush symmetry
Prior to this fix, asymmetric brushes weren't reflected, instead the brush was simply moved to a mirrored position.
This commit is contained in:
parent
2be11cf2f5
commit
980454eac0
@ -588,7 +588,7 @@ add_library(app-lib
|
||||
tools/pick_ink.cpp
|
||||
tools/point_shape.cpp
|
||||
tools/stroke.cpp
|
||||
tools/symmetries.cpp
|
||||
tools/symmetry.cpp
|
||||
tools/tool_box.cpp
|
||||
tools/tool_loop_manager.cpp
|
||||
tools/velocity.cpp
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2019-2020 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2021 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2017 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -8,6 +8,7 @@
|
||||
#include "app/util/wrap_point.h"
|
||||
|
||||
#include "app/tools/ink.h"
|
||||
#include "doc/algorithm/flip_image.h"
|
||||
#include "render/gradient.h"
|
||||
|
||||
namespace app {
|
||||
@ -42,7 +43,7 @@ class BrushPointShape : public PointShape {
|
||||
bool m_firstPoint;
|
||||
Brush* m_lastBrush;
|
||||
BrushType m_origBrushType;
|
||||
std::shared_ptr<CompressedImage> m_compressedImage;
|
||||
std::array<std::shared_ptr<CompressedImage>, 4> m_compressedImages;
|
||||
// For dynamics
|
||||
DynamicsOptions m_dynamics;
|
||||
bool m_useDynamics;
|
||||
@ -173,9 +174,7 @@ public:
|
||||
// TODO cache compressed images (or remove them completelly)
|
||||
if (m_lastBrush != brush) {
|
||||
m_lastBrush = brush;
|
||||
m_compressedImage.reset(new CompressedImage(brush->image(),
|
||||
brush->maskBitmap(),
|
||||
false));
|
||||
m_compressedImages.fill(nullptr);
|
||||
}
|
||||
|
||||
x += brush->bounds().x;
|
||||
@ -208,7 +207,7 @@ public:
|
||||
|
||||
ink->prepareForPointShape(loop, m_firstPoint, x, y);
|
||||
|
||||
for (auto scanline : *m_compressedImage) {
|
||||
for (auto scanline : getCompressedImage(pt.symmetry)) {
|
||||
int u = x+scanline.x;
|
||||
ink->prepareVForPointShape(loop, y+scanline.y);
|
||||
doInkHline(u, y+scanline.y, u+scanline.w-1, loop);
|
||||
@ -222,6 +221,47 @@ public:
|
||||
area.y += y;
|
||||
}
|
||||
|
||||
private:
|
||||
CompressedImage& getCompressedImage(gen::SymmetryMode symmetryMode) {
|
||||
auto& compressPtr = m_compressedImages[int(symmetryMode)];
|
||||
if (!compressPtr) {
|
||||
switch (symmetryMode) {
|
||||
case gen::SymmetryMode::NONE: {
|
||||
compressPtr.reset(new CompressedImage(m_lastBrush->image(),
|
||||
m_lastBrush->maskBitmap(),
|
||||
false));
|
||||
break;
|
||||
}
|
||||
case gen::SymmetryMode::HORIZONTAL:
|
||||
case gen::SymmetryMode::VERTICAL: {
|
||||
std::unique_ptr<Image> tempImage(Image::createCopy(m_lastBrush->image()));
|
||||
doc::algorithm::FlipType flip =
|
||||
(symmetryMode == gen::SymmetryMode::HORIZONTAL)?
|
||||
doc::algorithm::FlipType::FlipHorizontal:
|
||||
doc::algorithm::FlipType::FlipVertical;
|
||||
doc::algorithm::flip_image(tempImage.get(), tempImage->bounds(), flip);
|
||||
compressPtr.reset(new CompressedImage(tempImage.get(),
|
||||
m_lastBrush->maskBitmap(),
|
||||
false));
|
||||
break;
|
||||
}
|
||||
case gen::SymmetryMode::BOTH: {
|
||||
std::unique_ptr<Image> tempImage(Image::createCopy(m_lastBrush->image()));
|
||||
doc::algorithm::flip_image(tempImage.get(),
|
||||
tempImage->bounds(),
|
||||
doc::algorithm::FlipType::FlipVertical);
|
||||
doc::algorithm::flip_image(tempImage.get(),
|
||||
tempImage->bounds(),
|
||||
doc::algorithm::FlipType::FlipHorizontal);
|
||||
compressPtr.reset(new CompressedImage(tempImage.get(),
|
||||
m_lastBrush->maskBitmap(),
|
||||
false));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return *compressPtr;
|
||||
}
|
||||
};
|
||||
|
||||
class FloodFillPointShape : public PointShape {
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2019-2020 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2021 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2015 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -9,6 +9,7 @@
|
||||
#define APP_TOOLS_STROKE_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "app/pref/preferences.h"
|
||||
#include "gfx/point.h"
|
||||
#include "gfx/rect.h"
|
||||
|
||||
@ -25,6 +26,7 @@ namespace app {
|
||||
float size = 0.0f;
|
||||
float angle = 0.0f;
|
||||
float gradient = 0.0f;
|
||||
gen::SymmetryMode symmetry = gen::SymmetryMode::NONE;
|
||||
Pt() { }
|
||||
Pt(const gfx::Point& point) : x(point.x), y(point.y) { }
|
||||
Pt(int x, int y) : x(x), y(y) { }
|
||||
|
@ -1,94 +0,0 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2020 Igara Studio S.A.
|
||||
// Copyright (C) 2015-2017 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "app/tools/symmetries.h"
|
||||
|
||||
#include "app/tools/point_shape.h"
|
||||
#include "app/tools/stroke.h"
|
||||
#include "app/tools/tool_loop.h"
|
||||
#include "doc/brush.h"
|
||||
|
||||
namespace app {
|
||||
namespace tools {
|
||||
|
||||
void HorizontalSymmetry::generateStrokes(const Stroke& mainStroke, Strokes& strokes,
|
||||
ToolLoop* loop)
|
||||
{
|
||||
int brushSize, brushCenter;
|
||||
if (loop->getPointShape()->isFloodFill()) {
|
||||
brushSize = 1;
|
||||
brushCenter = 0;
|
||||
}
|
||||
else {
|
||||
// TODO we should flip the brush center+image+bitmap or just do
|
||||
// the symmetry of all pixels
|
||||
auto brush = loop->getBrush();
|
||||
brushSize = brush->bounds().w;
|
||||
brushCenter = brush->center().x;
|
||||
}
|
||||
|
||||
strokes.push_back(mainStroke);
|
||||
|
||||
Stroke stroke2;
|
||||
const bool isDynamic = loop->getDynamics().isDynamic();
|
||||
for (const auto& pt : mainStroke) {
|
||||
Stroke::Pt pt2 = pt;
|
||||
if (isDynamic) {
|
||||
brushSize = pt2.size;
|
||||
brushCenter = (brushSize - brushSize % 2) / 2;
|
||||
}
|
||||
pt2.x = m_x - ((pt.x-brushCenter) - m_x + 1) - (brushSize - brushCenter - 1);
|
||||
stroke2.addPoint(pt2);
|
||||
}
|
||||
strokes.push_back(stroke2);
|
||||
}
|
||||
|
||||
void VerticalSymmetry::generateStrokes(const Stroke& mainStroke, Strokes& strokes,
|
||||
ToolLoop* loop)
|
||||
{
|
||||
int brushSize, brushCenter;
|
||||
if (loop->getPointShape()->isFloodFill()) {
|
||||
brushSize = 1;
|
||||
brushCenter = 0;
|
||||
}
|
||||
else {
|
||||
auto brush = loop->getBrush();
|
||||
brushSize = brush->bounds().h;
|
||||
brushCenter = brush->center().y;
|
||||
}
|
||||
|
||||
strokes.push_back(mainStroke);
|
||||
|
||||
Stroke stroke2;
|
||||
const bool isDynamic = loop->getDynamics().isDynamic();
|
||||
for (const auto& pt : mainStroke) {
|
||||
Stroke::Pt pt2 = pt;
|
||||
if (isDynamic) {
|
||||
brushSize = pt2.size;
|
||||
brushCenter = (brushSize - brushSize % 2) / 2;
|
||||
}
|
||||
pt2.y = m_y - ((pt.y-brushCenter) - m_y + 1) - (brushSize - brushCenter - 1);
|
||||
stroke2.addPoint(pt2);
|
||||
}
|
||||
strokes.push_back(stroke2);
|
||||
}
|
||||
|
||||
void SymmetryCombo::generateStrokes(const Stroke& mainStroke, Strokes& strokes,
|
||||
ToolLoop* loop)
|
||||
{
|
||||
Strokes strokes0;
|
||||
m_a->generateStrokes(mainStroke, strokes0, loop);
|
||||
for (const Stroke& stroke : strokes0)
|
||||
m_b->generateStrokes(stroke, strokes, loop);
|
||||
}
|
||||
|
||||
} // namespace tools
|
||||
} // namespace app
|
@ -1,50 +0,0 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2015-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#ifndef APP_TOOLS_SYMMETRIES_H_INCLUDED
|
||||
#define APP_TOOLS_SYMMETRIES_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "app/tools/stroke.h"
|
||||
#include "app/tools/symmetry.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace app {
|
||||
namespace tools {
|
||||
|
||||
class HorizontalSymmetry : public Symmetry {
|
||||
public:
|
||||
HorizontalSymmetry(double x) : m_x(x) { }
|
||||
void generateStrokes(const Stroke& mainStroke, Strokes& strokes,
|
||||
ToolLoop* loop) override;
|
||||
private:
|
||||
double m_x;
|
||||
};
|
||||
|
||||
class VerticalSymmetry : public Symmetry {
|
||||
public:
|
||||
VerticalSymmetry(double y) : m_y(y) { }
|
||||
void generateStrokes(const Stroke& mainStroke, Strokes& strokes,
|
||||
ToolLoop* loop) override;
|
||||
private:
|
||||
double m_y;
|
||||
};
|
||||
|
||||
class SymmetryCombo : public Symmetry {
|
||||
public:
|
||||
SymmetryCombo(Symmetry* a, Symmetry* b) : m_a(a), m_b(b) { }
|
||||
void generateStrokes(const Stroke& mainStroke, Strokes& strokes,
|
||||
ToolLoop* loop) override;
|
||||
private:
|
||||
std::unique_ptr<tools::Symmetry> m_a;
|
||||
std::unique_ptr<tools::Symmetry> m_b;
|
||||
};
|
||||
|
||||
} // namespace tools
|
||||
} // namespace app
|
||||
|
||||
#endif
|
91
src/app/tools/symmetry.cpp
Normal file
91
src/app/tools/symmetry.cpp
Normal file
@ -0,0 +1,91 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2021 Igara Studio S.A.
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "app/tools/symmetry.h"
|
||||
|
||||
#include "app/tools/point_shape.h"
|
||||
#include "app/tools/tool_loop.h"
|
||||
|
||||
namespace app {
|
||||
namespace tools {
|
||||
|
||||
void Symmetry::generateStrokes(const Stroke& stroke, Strokes& strokes,
|
||||
ToolLoop* loop)
|
||||
{
|
||||
Stroke stroke2;
|
||||
strokes.push_back(stroke);
|
||||
gen::SymmetryMode symmetryMode = loop->getSymmetry()->mode();
|
||||
switch (symmetryMode) {
|
||||
case gen::SymmetryMode::NONE:
|
||||
ASSERT(false);
|
||||
break;
|
||||
|
||||
case gen::SymmetryMode::HORIZONTAL:
|
||||
case gen::SymmetryMode::VERTICAL:
|
||||
calculateSymmetricalStroke(stroke, stroke2, loop, symmetryMode);
|
||||
strokes.push_back(stroke2);
|
||||
break;
|
||||
|
||||
case gen::SymmetryMode::BOTH: {
|
||||
calculateSymmetricalStroke(stroke, stroke2, loop, gen::SymmetryMode::HORIZONTAL);
|
||||
strokes.push_back(stroke2);
|
||||
|
||||
Stroke stroke3;
|
||||
calculateSymmetricalStroke(stroke, stroke3, loop, gen::SymmetryMode::VERTICAL);
|
||||
strokes.push_back(stroke3);
|
||||
|
||||
Stroke stroke4;
|
||||
calculateSymmetricalStroke(stroke3, stroke4, loop, gen::SymmetryMode::BOTH);
|
||||
strokes.push_back(stroke4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Symmetry::calculateSymmetricalStroke(const Stroke& refStroke, Stroke& stroke,
|
||||
ToolLoop* loop, gen::SymmetryMode symmetryMode)
|
||||
{
|
||||
int brushSize, brushCenter;
|
||||
if (loop->getPointShape()->isFloodFill()) {
|
||||
brushSize = 1;
|
||||
brushCenter = 0;
|
||||
}
|
||||
else {
|
||||
// TODO we should flip the brush center+image+bitmap or just do
|
||||
// the symmetry of all pixels
|
||||
auto brush = loop->getBrush();
|
||||
if (symmetryMode == gen::SymmetryMode::HORIZONTAL || symmetryMode == gen::SymmetryMode::BOTH) {
|
||||
brushSize = brush->bounds().w;
|
||||
brushCenter = brush->center().x;
|
||||
}
|
||||
else {
|
||||
brushSize = brush->bounds().h;
|
||||
brushCenter = brush->center().y;
|
||||
}
|
||||
}
|
||||
|
||||
const bool isDynamic = loop->getDynamics().isDynamic();
|
||||
for (const auto& pt : refStroke) {
|
||||
if (isDynamic) {
|
||||
brushSize = pt.size;
|
||||
brushCenter = (brushSize - brushSize % 2) / 2;
|
||||
}
|
||||
Stroke::Pt pt2 = pt;
|
||||
pt2.symmetry = symmetryMode;
|
||||
if (symmetryMode == gen::SymmetryMode::HORIZONTAL || symmetryMode == gen::SymmetryMode::BOTH)
|
||||
pt2.x = 2 * (m_x + brushCenter) - pt2.x - brushSize;
|
||||
else
|
||||
pt2.y = 2 * (m_y + brushCenter) - pt2.y - brushSize;
|
||||
stroke.addPoint(pt2);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace tools
|
||||
} // namespace app
|
@ -1,4 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2021 Igara Studio S.A.
|
||||
// Copyright (C) 2015 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -9,24 +10,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "app/tools/stroke.h"
|
||||
|
||||
#include <vector>
|
||||
#include "app/pref/preferences.h"
|
||||
|
||||
namespace app {
|
||||
namespace tools {
|
||||
namespace tools {
|
||||
|
||||
class ToolLoop;
|
||||
class ToolLoop;
|
||||
|
||||
// This class controls user input.
|
||||
class Symmetry {
|
||||
public:
|
||||
virtual ~Symmetry() { }
|
||||
class Symmetry {
|
||||
public:
|
||||
Symmetry(gen::SymmetryMode symmetryMode, double x, double y)
|
||||
: m_symmetryMode(symmetryMode)
|
||||
, m_x(x)
|
||||
, m_y(y) {
|
||||
}
|
||||
|
||||
// The "stroke" must be relative to the sprite origin.
|
||||
virtual void generateStrokes(const Stroke& stroke, Strokes& strokes, ToolLoop* loop) = 0;
|
||||
};
|
||||
void generateStrokes(const Stroke& stroke, Strokes& strokes, ToolLoop* loop);
|
||||
|
||||
} // namespace tools
|
||||
gen::SymmetryMode mode() const { return m_symmetryMode; }
|
||||
|
||||
private:
|
||||
void calculateSymmetricalStroke(const Stroke& refStroke, Stroke& stroke,
|
||||
ToolLoop* loop, gen::SymmetryMode symmetryMode);
|
||||
|
||||
gen::SymmetryMode m_symmetryMode;
|
||||
double m_x, m_y;
|
||||
};
|
||||
|
||||
} // namespace tools
|
||||
} // namespace app
|
||||
|
||||
#endif
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2019-2020 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2021 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -29,7 +29,7 @@
|
||||
#include "app/tools/freehand_algorithm.h"
|
||||
#include "app/tools/ink.h"
|
||||
#include "app/tools/point_shape.h"
|
||||
#include "app/tools/symmetries.h"
|
||||
#include "app/tools/symmetry.h"
|
||||
#include "app/tools/tool.h"
|
||||
#include "app/tools/tool_box.h"
|
||||
#include "app/tools/tool_loop.h"
|
||||
@ -204,27 +204,10 @@ public:
|
||||
|
||||
// Symmetry mode
|
||||
if (Preferences::instance().symmetryMode.enabled()) {
|
||||
switch (m_docPref.symmetry.mode()) {
|
||||
|
||||
case app::gen::SymmetryMode::NONE:
|
||||
ASSERT(m_symmetry == nullptr);
|
||||
break;
|
||||
|
||||
case app::gen::SymmetryMode::HORIZONTAL:
|
||||
m_symmetry.reset(new app::tools::HorizontalSymmetry(m_docPref.symmetry.xAxis()));
|
||||
break;
|
||||
|
||||
case app::gen::SymmetryMode::VERTICAL:
|
||||
m_symmetry.reset(new app::tools::VerticalSymmetry(m_docPref.symmetry.yAxis()));
|
||||
break;
|
||||
|
||||
case app::gen::SymmetryMode::BOTH:
|
||||
m_symmetry.reset(
|
||||
new app::tools::SymmetryCombo(
|
||||
new app::tools::HorizontalSymmetry(m_docPref.symmetry.xAxis()),
|
||||
new app::tools::VerticalSymmetry(m_docPref.symmetry.yAxis())));
|
||||
break;
|
||||
}
|
||||
if (m_docPref.symmetry.mode() != gen::SymmetryMode::NONE)
|
||||
m_symmetry.reset(new tools::Symmetry(m_docPref.symmetry.mode(),
|
||||
m_docPref.symmetry.xAxis(),
|
||||
m_docPref.symmetry.yAxis()));
|
||||
}
|
||||
|
||||
// Ignore opacity for these inks
|
||||
|
Loading…
x
Reference in New Issue
Block a user