mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-07-25 15:04:57 +00:00
Implemented adaptive drawing algorithm with smoothing in image editor.
- Adaptive point filtering - zoom-based minimum distance (3px / zoom) prevents point overflow during fast movements. - Speed-based pressure simulation - exponential smoothing (decay 0.95) creates variable line thickness: pressure = clamp(1.0 - speed * 0.1). - Two-pass weighted averaging - smoothed = current * 0.5 + neighbors * 0.25 eliminates jagged lines while preserving initial direction. - Incremental rendering with 3-point overlap - draws only new segments, reduces complexity from O(n^2) to O(n). - QPainterPath with quadratic Bezier curves - single fillPath() call instead of multiple drawEllipse().
This commit is contained in:
@@ -125,6 +125,7 @@ std::shared_ptr<float64> Scene::lastZ() const {
|
||||
}
|
||||
|
||||
void Scene::updateZoom(float64 zoom) {
|
||||
_canvas->updateZoom(zoom);
|
||||
for (const auto &item : items()) {
|
||||
if (item->type() >= ItemBase::Type) {
|
||||
static_cast<ItemBase*>(item.get())->updateZoom(zoom);
|
||||
|
||||
@@ -13,66 +13,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
QRectF NormalizedRect(const QPointF& p1, const QPointF& p2) {
|
||||
return QRectF(
|
||||
std::min(p1.x(), p2.x()),
|
||||
std::min(p1.y(), p2.y()),
|
||||
std::abs(p2.x() - p1.x()) + 1,
|
||||
std::abs(p2.y() - p1.y()) + 1);
|
||||
}
|
||||
constexpr auto kMinPointDistanceBase = 2.0;
|
||||
constexpr auto kMaxPointDistance = 15.0;
|
||||
constexpr auto kSmoothingStrength = 0.5;
|
||||
constexpr auto kPressureDecay = 0.95;
|
||||
constexpr auto kMinPressure = 0.3;
|
||||
constexpr auto kBatchUpdateInterval = 16;
|
||||
constexpr auto kSegmentOverlap = 3;
|
||||
constexpr auto kHalfStrength = kSmoothingStrength / 2.0;
|
||||
constexpr auto kInvStrength = 1.0 - kSmoothingStrength;
|
||||
|
||||
std::vector<QPointF> InterpolatedPoints(
|
||||
const QPointF &startPoint,
|
||||
const QPointF &endPoint) {
|
||||
std::vector<QPointF> points;
|
||||
|
||||
const auto x1 = startPoint.x();
|
||||
const auto y1 = startPoint.y();
|
||||
const auto x2 = endPoint.x();
|
||||
const auto y2 = endPoint.y();
|
||||
|
||||
// Difference of x and y values.
|
||||
const auto dx = x2 - x1;
|
||||
const auto dy = y2 - y1;
|
||||
|
||||
// Absolute values of differences.
|
||||
const auto ix = std::abs(dx);
|
||||
const auto iy = std::abs(dy);
|
||||
|
||||
// Larger of the x and y differences.
|
||||
const auto inc = ix > iy ? ix : iy;
|
||||
|
||||
// Plot location.
|
||||
auto plotx = x1;
|
||||
auto ploty = y1;
|
||||
|
||||
auto x = 0;
|
||||
auto y = 0;
|
||||
|
||||
points.push_back(QPointF(plotx, ploty));
|
||||
|
||||
for (auto i = 0; i <= inc; i++) {
|
||||
const auto xInc = x > inc;
|
||||
const auto yInc = y > inc;
|
||||
|
||||
x += ix;
|
||||
y += iy;
|
||||
|
||||
if (xInc) {
|
||||
x -= inc;
|
||||
plotx += 1 * ((dx < 0) ? -1 : 1);
|
||||
}
|
||||
|
||||
if (yInc) {
|
||||
y -= inc;
|
||||
ploty += 1 * ((dy < 0) ? -1 : 1);
|
||||
}
|
||||
|
||||
if (xInc || yInc) {
|
||||
points.push_back(QPointF(plotx, ploty));
|
||||
}
|
||||
}
|
||||
return points;
|
||||
[[nodiscard]] float64 PointDistance(const QPointF &a, const QPointF &b) {
|
||||
const auto dx = a.x() - b.x();
|
||||
const auto dy = a.y() - b.y();
|
||||
return std::sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -115,8 +69,7 @@ void ItemCanvas::computeContentRect(const QPointF &p) {
|
||||
_contentRect = QRectF(
|
||||
QPointF(
|
||||
std::clamp(p.x() - _brushMargins.left(), 0., _contentRect.x()),
|
||||
std::clamp(p.y() - _brushMargins.top(), 0., _contentRect.y())
|
||||
),
|
||||
std::clamp(p.y() - _brushMargins.top(), 0., _contentRect.y())),
|
||||
QPointF(
|
||||
std::clamp(
|
||||
p.x() + _brushMargins.right(),
|
||||
@@ -125,27 +78,154 @@ void ItemCanvas::computeContentRect(const QPointF &p) {
|
||||
std::clamp(
|
||||
p.y() + _brushMargins.bottom(),
|
||||
_contentRect.y() + _contentRect.height(),
|
||||
sceneSize.height())
|
||||
));
|
||||
sceneSize.height())));
|
||||
}
|
||||
|
||||
void ItemCanvas::drawLine(
|
||||
const QPointF ¤tPoint,
|
||||
const QPointF &lastPoint) {
|
||||
const auto halfBrushSize = _brushData.size / 2.;
|
||||
const auto points = InterpolatedPoints(lastPoint, currentPoint);
|
||||
|
||||
_rectToUpdate |= NormalizedRect(currentPoint, lastPoint) + _brushMargins;
|
||||
|
||||
for (const auto &point : points) {
|
||||
_p->drawEllipse(point, halfBrushSize, halfBrushSize);
|
||||
std::vector<ItemCanvas::StrokePoint> ItemCanvas::smoothStroke(
|
||||
const std::vector<StrokePoint> &points) const {
|
||||
if (points.size() < 4) {
|
||||
return points;
|
||||
}
|
||||
auto result = std::vector<StrokePoint>();
|
||||
result.reserve(points.size());
|
||||
result.push_back(points[0]);
|
||||
result.push_back(points[1]);
|
||||
for (auto i = 2; i < int(points.size()) - 1; ++i) {
|
||||
const auto &prev = points[i - 1].pos;
|
||||
const auto &curr = points[i].pos;
|
||||
const auto &next = points[i + 1].pos;
|
||||
const auto smoothed = curr * kInvStrength
|
||||
+ (prev + next) * kHalfStrength;
|
||||
result.push_back({
|
||||
.pos = smoothed,
|
||||
.pressure = points[i].pressure,
|
||||
.time = points[i].time,
|
||||
});
|
||||
}
|
||||
result.push_back(points.back());
|
||||
return result;
|
||||
}
|
||||
|
||||
void ItemCanvas::renderSegment(
|
||||
const std::vector<StrokePoint> &points,
|
||||
int startIdx) {
|
||||
if (points.size() < 2 || startIdx >= int(points.size()) - 1) {
|
||||
return;
|
||||
}
|
||||
auto path = QPainterPath();
|
||||
const auto effectiveStart = std::max(0, startIdx);
|
||||
path.moveTo(points[effectiveStart].pos);
|
||||
for (auto i = effectiveStart; i < int(points.size()) - 1; ++i) {
|
||||
const auto &p0 = points[i].pos;
|
||||
const auto &p1 = points[i + 1].pos;
|
||||
const auto ctrl = (p0 + p1) / 2.0;
|
||||
if (i == effectiveStart) {
|
||||
path.lineTo(ctrl);
|
||||
} else {
|
||||
path.quadTo(p0, ctrl);
|
||||
}
|
||||
}
|
||||
path.lineTo(points.back().pos);
|
||||
const auto avgPressure = std::accumulate(
|
||||
points.begin() + std::max(0, startIdx),
|
||||
points.end(),
|
||||
0.0,
|
||||
[](float64 sum, const StrokePoint &p) {
|
||||
return sum + p.pressure;
|
||||
}) / (points.size() - std::max(0, startIdx));
|
||||
const auto width = _brushData.size * avgPressure;
|
||||
auto stroker = QPainterPathStroker();
|
||||
stroker.setWidth(width);
|
||||
stroker.setCapStyle(Qt::RoundCap);
|
||||
stroker.setJoinStyle(Qt::RoundJoin);
|
||||
const auto outline = stroker.createStroke(path);
|
||||
_p->fillPath(outline, _brushData.color);
|
||||
_rectToUpdate |= outline.boundingRect() + _brushMargins;
|
||||
}
|
||||
|
||||
void ItemCanvas::drawIncrementalStroke() {
|
||||
if (_currentStroke.size() < 2) {
|
||||
return;
|
||||
}
|
||||
const auto startIdx = std::max(
|
||||
0,
|
||||
_lastRenderedIndex - kSegmentOverlap);
|
||||
auto segment = std::vector<StrokePoint>(
|
||||
_currentStroke.begin() + startIdx,
|
||||
_currentStroke.end());
|
||||
if (segment.size() < 2) {
|
||||
return;
|
||||
}
|
||||
if (segment.size() >= 4) {
|
||||
for (auto i = 0; i < 2; ++i) {
|
||||
segment = smoothStroke(segment);
|
||||
}
|
||||
}
|
||||
renderSegment(
|
||||
segment,
|
||||
std::min(kSegmentOverlap, int(segment.size()) - 1));
|
||||
_lastRenderedIndex = _currentStroke.size() - kSegmentOverlap;
|
||||
}
|
||||
|
||||
void ItemCanvas::addStrokePoint(const QPointF &point, int64 time) {
|
||||
if (!_currentStroke.empty()) {
|
||||
const auto distance = PointDistance(
|
||||
point,
|
||||
_currentStroke.back().pos);
|
||||
const auto minDistance = kMinPointDistanceBase * std::min(1.0, _zoom);
|
||||
if (distance < minDistance) {
|
||||
return;
|
||||
}
|
||||
if (distance > kMaxPointDistance) {
|
||||
const auto steps = int(std::ceil(distance / kMaxPointDistance));
|
||||
const auto &lastPos = _currentStroke.back().pos;
|
||||
const auto &lastPressure = _currentStroke.back().pressure;
|
||||
for (auto i = 1; i < steps; ++i) {
|
||||
const auto t = float64(i) / steps;
|
||||
const auto interpolated = lastPos * (1.0 - t) + point * t;
|
||||
const auto interpTime = _lastPointTime
|
||||
+ int64((time - _lastPointTime) * t);
|
||||
_currentStroke.push_back({
|
||||
.pos = interpolated,
|
||||
.pressure = lastPressure,
|
||||
.time = interpTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto timeDelta = _lastPointTime
|
||||
? (time - _lastPointTime)
|
||||
: kBatchUpdateInterval;
|
||||
const auto speed = !_currentStroke.empty()
|
||||
? PointDistance(point, _currentStroke.back().pos) / timeDelta
|
||||
: 0.0;
|
||||
const auto pressureFromSpeed = std::clamp(
|
||||
1.0 - speed * 0.1,
|
||||
kMinPressure,
|
||||
1.0);
|
||||
const auto pressure = _currentStroke.empty()
|
||||
? 1.0
|
||||
: _currentStroke.back().pressure * kPressureDecay
|
||||
+ pressureFromSpeed * (1.0 - kPressureDecay);
|
||||
_currentStroke.push_back({
|
||||
.pos = point,
|
||||
.pressure = pressure,
|
||||
.time = time,
|
||||
});
|
||||
_lastPointTime = time;
|
||||
computeContentRect(point);
|
||||
}
|
||||
|
||||
void ItemCanvas::handleMousePressEvent(
|
||||
not_null<QGraphicsSceneMouseEvent*> e) {
|
||||
_lastPoint = e->scenePos();
|
||||
_contentRect = QRectF(_lastPoint, _lastPoint);
|
||||
_rectToUpdate = QRectF();
|
||||
_currentStroke.clear();
|
||||
_lastRenderedIndex = 0;
|
||||
_lastPointTime = 0;
|
||||
const auto now = crl::now();
|
||||
addStrokePoint(_lastPoint, now);
|
||||
_contentRect = QRectF(_lastPoint, _lastPoint) + _brushMargins;
|
||||
_drawing = true;
|
||||
}
|
||||
|
||||
@@ -155,10 +235,13 @@ void ItemCanvas::handleMouseMoveEvent(
|
||||
return;
|
||||
}
|
||||
const auto scenePos = e->scenePos();
|
||||
drawLine(scenePos, _lastPoint);
|
||||
update(_rectToUpdate);
|
||||
computeContentRect(scenePos);
|
||||
const auto now = crl::now();
|
||||
addStrokePoint(scenePos, now);
|
||||
_lastPoint = scenePos;
|
||||
if (_currentStroke.size() - _lastRenderedIndex >= 3) {
|
||||
drawIncrementalStroke();
|
||||
update(_rectToUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemCanvas::handleMouseReleaseEvent(
|
||||
@@ -167,19 +250,23 @@ void ItemCanvas::handleMouseReleaseEvent(
|
||||
return;
|
||||
}
|
||||
_drawing = false;
|
||||
|
||||
drawIncrementalStroke();
|
||||
update(_rectToUpdate);
|
||||
if (_contentRect.isValid()) {
|
||||
const auto scaledContentRect = QRectF(
|
||||
_contentRect.x() * style::DevicePixelRatio(),
|
||||
_contentRect.y() * style::DevicePixelRatio(),
|
||||
_contentRect.width() * style::DevicePixelRatio(),
|
||||
_contentRect.height() * style::DevicePixelRatio());
|
||||
|
||||
_grabContentRequests.fire({
|
||||
.pixmap = _pixmap.copy(scaledContentRect.toRect()),
|
||||
.position = _contentRect.topLeft(),
|
||||
});
|
||||
}
|
||||
_currentStroke.clear();
|
||||
_lastRenderedIndex = 0;
|
||||
_lastPointTime = 0;
|
||||
_currentPath = QPainterPath();
|
||||
clearPixmap();
|
||||
update();
|
||||
}
|
||||
@@ -211,11 +298,19 @@ bool ItemCanvas::collidesWithPath(
|
||||
|
||||
void ItemCanvas::cancelDrawing() {
|
||||
_drawing = false;
|
||||
_currentStroke.clear();
|
||||
_lastRenderedIndex = 0;
|
||||
_lastPointTime = 0;
|
||||
_currentPath = QPainterPath();
|
||||
_contentRect = QRectF();
|
||||
clearPixmap();
|
||||
update();
|
||||
}
|
||||
|
||||
void ItemCanvas::updateZoom(float64 zoom) {
|
||||
_zoom = zoom;
|
||||
}
|
||||
|
||||
ItemCanvas::~ItemCanvas() {
|
||||
_hq = nullptr;
|
||||
_p = nullptr;
|
||||
|
||||
@@ -28,6 +28,7 @@ public:
|
||||
void applyBrush(const QColor &color, float size);
|
||||
void clearPixmap();
|
||||
void cancelDrawing();
|
||||
void updateZoom(float64 zoom);
|
||||
|
||||
QRectF boundingRect() const override;
|
||||
void paint(
|
||||
@@ -50,10 +51,26 @@ protected:
|
||||
const QPainterPath &,
|
||||
Qt::ItemSelectionMode) const override;
|
||||
private:
|
||||
struct StrokePoint {
|
||||
QPointF pos;
|
||||
float64 pressure = 1.0;
|
||||
int64 time = 0;
|
||||
};
|
||||
|
||||
void computeContentRect(const QPointF &p);
|
||||
void drawLine(const QPointF ¤tPoint, const QPointF &lastPoint);
|
||||
void addStrokePoint(const QPointF &point, int64 time);
|
||||
void drawIncrementalStroke();
|
||||
std::vector<StrokePoint> smoothStroke(
|
||||
const std::vector<StrokePoint> &points) const;
|
||||
void renderSegment(
|
||||
const std::vector<StrokePoint> &points,
|
||||
int startIdx);
|
||||
|
||||
bool _drawing = false;
|
||||
std::vector<StrokePoint> _currentStroke;
|
||||
int _lastRenderedIndex = 0;
|
||||
float64 _zoom = 1.0;
|
||||
int64 _lastPointTime = 0;
|
||||
|
||||
std::unique_ptr<PainterHighQualityEnabler> _hq;
|
||||
std::unique_ptr<Painter> _p;
|
||||
@@ -65,6 +82,7 @@ private:
|
||||
QPointF _lastPoint;
|
||||
|
||||
QPixmap _pixmap;
|
||||
QPainterPath _currentPath;
|
||||
|
||||
struct {
|
||||
float size = 1.;
|
||||
|
||||
Reference in New Issue
Block a user