mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-07-25 15:04:57 +00:00
473 lines
12 KiB
C++
473 lines
12 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "editor/scene/scene_item_canvas.h"
|
|
#include "styles/style_editor.h"
|
|
|
|
#include <QGraphicsScene>
|
|
#include <QGraphicsSceneMouseEvent>
|
|
#include <QtMath>
|
|
|
|
namespace Editor {
|
|
namespace {
|
|
|
|
constexpr auto kMinPointDistanceBase = 2.0;
|
|
constexpr auto kMaxPointDistance = 15.0;
|
|
constexpr auto kSmoothingStrength = 0.5;
|
|
constexpr auto kSegmentOverlap = 3;
|
|
constexpr auto kOriginStartJumpRatio = 0.25;
|
|
constexpr auto kHalfStrength = kSmoothingStrength / 2.0;
|
|
constexpr auto kInvStrength = 1.0 - kSmoothingStrength;
|
|
|
|
[[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);
|
|
}
|
|
|
|
[[nodiscard]] bool IsValidPoint(const QPointF &point) {
|
|
return std::isfinite(point.x()) && std::isfinite(point.y());
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ItemCanvas::ItemCanvas() {
|
|
setAcceptedMouseButtons({});
|
|
}
|
|
|
|
void ItemCanvas::clearPixmap() {
|
|
_hq = nullptr;
|
|
_p = nullptr;
|
|
|
|
_pixmap = QPixmap(
|
|
(scene()->sceneRect().size() * style::DevicePixelRatio()).toSize());
|
|
_pixmap.setDevicePixelRatio(style::DevicePixelRatio());
|
|
_pixmap.fill(Qt::transparent);
|
|
|
|
_p = std::make_unique<Painter>(&_pixmap);
|
|
_hq = std::make_unique<PainterHighQualityEnabler>(*_p);
|
|
_p->setPen(Qt::NoPen);
|
|
_p->setBrush(_brushData.color);
|
|
}
|
|
|
|
void ItemCanvas::applyBrush(const QColor &color, float size, Brush::Tool tool) {
|
|
_brushData.color = color;
|
|
_brushData.size = size;
|
|
_brushData.tool = tool;
|
|
_p->setBrush(color);
|
|
const auto width = strokeWidth(1.0);
|
|
const auto extra = (tool == Brush::Tool::Arrow)
|
|
? arrowHeadLength()
|
|
: 0.;
|
|
const auto margin = width + extra;
|
|
_brushMargins = QMarginsF(margin, margin, margin, margin);
|
|
}
|
|
|
|
QRectF ItemCanvas::boundingRect() const {
|
|
return scene()->sceneRect();
|
|
}
|
|
|
|
void ItemCanvas::computeContentRect(const QPointF &p) {
|
|
if (!scene() || !IsValidPoint(p)) {
|
|
return;
|
|
}
|
|
const auto sceneSize = scene()->sceneRect().size();
|
|
const auto contentLeft = std::max(0., _contentRect.x());
|
|
const auto contentTop = std::max(0., _contentRect.y());
|
|
_contentRect = QRectF(
|
|
QPointF(
|
|
std::clamp(p.x() - _brushMargins.left(), 0., contentLeft),
|
|
std::clamp(p.y() - _brushMargins.top(), 0., contentTop)),
|
|
QPointF(
|
|
std::clamp(
|
|
p.x() + _brushMargins.right(),
|
|
contentLeft + _contentRect.width(),
|
|
sceneSize.width()),
|
|
std::clamp(
|
|
p.y() + _brushMargins.bottom(),
|
|
contentTop + _contentRect.height(),
|
|
sceneSize.height())));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
float64 ItemCanvas::strokeWidth(float64 pressure) const {
|
|
auto width = _brushData.size * pressure;
|
|
if (_brushData.tool == Brush::Tool::Marker) {
|
|
width *= st::photoEditorMarkerSizeMultiplier;
|
|
} else if (_brushData.tool == Brush::Tool::Blur) {
|
|
width *= st::photoEditorBlurSizeMultiplier;
|
|
}
|
|
return width;
|
|
}
|
|
|
|
QColor ItemCanvas::strokeColor() const {
|
|
if (_brushData.tool == Brush::Tool::Eraser
|
|
|| _brushData.tool == Brush::Tool::Blur) {
|
|
return QColor(0, 0, 0, 255);
|
|
}
|
|
auto color = _brushData.color;
|
|
if (_brushData.tool == Brush::Tool::Marker) {
|
|
color.setAlphaF(color.alphaF() * st::photoEditorMarkerOpacity);
|
|
}
|
|
return color;
|
|
}
|
|
|
|
float64 ItemCanvas::arrowHeadLength() const {
|
|
return _brushData.size * st::photoEditorArrowHeadLengthFactor;
|
|
}
|
|
|
|
void ItemCanvas::renderSegment(
|
|
const std::vector<StrokePoint> &points,
|
|
int startIdx) {
|
|
if (points.size() < 2 || startIdx >= int(points.size()) - 1) {
|
|
return;
|
|
}
|
|
if (!IsValidPoint(points.back().pos)) {
|
|
return;
|
|
}
|
|
auto path = QPainterPath();
|
|
const auto effectiveStart = std::max(0, startIdx);
|
|
if (!IsValidPoint(points[effectiveStart].pos)) {
|
|
return;
|
|
}
|
|
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;
|
|
if (!IsValidPoint(p0) || !IsValidPoint(p1)) {
|
|
return;
|
|
}
|
|
const auto ctrl = (p0 + p1) / 2.0;
|
|
if (!IsValidPoint(ctrl)) {
|
|
return;
|
|
}
|
|
if (i == effectiveStart) {
|
|
path.lineTo(ctrl);
|
|
} else {
|
|
path.quadTo(p0, ctrl);
|
|
}
|
|
}
|
|
path.lineTo(points.back().pos);
|
|
const auto count = points.size() - std::max(0, startIdx);
|
|
const auto avgPressure = count > 0
|
|
? std::accumulate(
|
|
points.begin() + std::max(0, startIdx),
|
|
points.end(),
|
|
0.0,
|
|
[](float64 sum, const StrokePoint &p) {
|
|
return sum + p.pressure;
|
|
}) / count
|
|
: 1.0;
|
|
const auto width = strokeWidth(avgPressure);
|
|
auto stroker = QPainterPathStroker();
|
|
stroker.setWidth(width);
|
|
stroker.setCapStyle(Qt::RoundCap);
|
|
stroker.setJoinStyle(Qt::RoundJoin);
|
|
const auto outline = stroker.createStroke(path);
|
|
const auto color = strokeColor();
|
|
if (_brushData.tool == Brush::Tool::Marker) {
|
|
_p->save();
|
|
_p->setCompositionMode(QPainter::CompositionMode_Source);
|
|
_p->fillPath(outline, color);
|
|
_p->restore();
|
|
} else {
|
|
_p->fillPath(outline, 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 = std::max(
|
|
0,
|
|
int(_currentStroke.size()) - kSegmentOverlap);
|
|
}
|
|
|
|
void ItemCanvas::drawArrowHead() {
|
|
if (_brushData.tool != Brush::Tool::Arrow) {
|
|
return;
|
|
}
|
|
if (_currentStroke.size() < 2) {
|
|
return;
|
|
}
|
|
const auto tip = _currentStroke.back().pos;
|
|
const auto minDistance = _brushData.size
|
|
* st::photoEditorArrowHeadMinDistanceFactor;
|
|
auto base = _currentStroke.front().pos;
|
|
for (auto i = int(_currentStroke.size()) - 2; i >= 0; --i) {
|
|
const auto &p = _currentStroke[i].pos;
|
|
if (PointDistance(tip, p) >= minDistance) {
|
|
base = p;
|
|
break;
|
|
}
|
|
}
|
|
auto direction = tip - base;
|
|
const auto length = std::sqrt(
|
|
direction.x() * direction.x()
|
|
+ direction.y() * direction.y());
|
|
if (length <= 0.) {
|
|
return;
|
|
}
|
|
direction /= length;
|
|
const auto angle = qDegreesToRadians(
|
|
float64(st::photoEditorArrowHeadAngleDegrees));
|
|
const auto sinA = std::sin(angle);
|
|
const auto cosA = std::cos(angle);
|
|
const auto rotate = [&](const QPointF &v, float64 s, float64 c) {
|
|
return QPointF(v.x() * c - v.y() * s, v.x() * s + v.y() * c);
|
|
};
|
|
const auto headLength = arrowHeadLength();
|
|
const auto left = tip - rotate(direction, sinA, cosA) * headLength;
|
|
const auto right = tip - rotate(direction, -sinA, cosA) * headLength;
|
|
|
|
auto arrow = QPainterPath();
|
|
arrow.moveTo(tip);
|
|
arrow.lineTo(left);
|
|
arrow.moveTo(tip);
|
|
arrow.lineTo(right);
|
|
auto stroker = QPainterPathStroker();
|
|
stroker.setWidth(strokeWidth(_currentStroke.back().pressure));
|
|
stroker.setCapStyle(Qt::RoundCap);
|
|
stroker.setJoinStyle(Qt::RoundJoin);
|
|
const auto outline = stroker.createStroke(arrow);
|
|
_p->fillPath(outline, strokeColor());
|
|
_rectToUpdate |= outline.boundingRect() + _brushMargins;
|
|
computeContentRect(left);
|
|
computeContentRect(right);
|
|
}
|
|
|
|
void ItemCanvas::addStrokePoint(const QPointF &point, int64 time) {
|
|
if (!IsValidPoint(point)) {
|
|
return;
|
|
}
|
|
if ((_currentStroke.size() == 1)
|
|
&& (_currentStroke.front().pos == QPointF())
|
|
&& (point != QPointF())) {
|
|
if (const auto currentScene = scene()) {
|
|
const auto size = currentScene->sceneRect().size();
|
|
const auto maxStartJump = std::max(size.width(), size.height())
|
|
* kOriginStartJumpRatio;
|
|
if (PointDistance(_currentStroke.front().pos, point)
|
|
> maxStartJump) {
|
|
_currentStroke.front() = {
|
|
.pos = point,
|
|
.pressure = 1.0,
|
|
.time = time,
|
|
};
|
|
_lastPointTime = time;
|
|
_contentRect = QRectF(point, point) + _brushMargins;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (!_currentStroke.empty()) {
|
|
const auto distance = PointDistance(
|
|
point,
|
|
_currentStroke.back().pos);
|
|
const auto minDistance = (_zoom > 1.0)
|
|
? (kMinPointDistanceBase / _zoom)
|
|
: (kMinPointDistanceBase * _zoom);
|
|
if (distance < minDistance) {
|
|
return;
|
|
}
|
|
const auto maxDistance = (_zoom > 1.0)
|
|
? (kMaxPointDistance / _zoom)
|
|
: kMaxPointDistance;
|
|
if (distance > maxDistance) {
|
|
const auto steps = int(std::ceil(distance / maxDistance));
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
_currentStroke.push_back({
|
|
.pos = point,
|
|
.pressure = 1.0,
|
|
.time = time,
|
|
});
|
|
_lastPointTime = time;
|
|
computeContentRect(point);
|
|
}
|
|
|
|
void ItemCanvas::handleMousePressEvent(
|
|
not_null<QGraphicsSceneMouseEvent*> e) {
|
|
_lastPoint = e->scenePos();
|
|
if (!IsValidPoint(_lastPoint)) {
|
|
_drawing = false;
|
|
return;
|
|
}
|
|
_rectToUpdate = QRectF();
|
|
_contentRect = QRectF();
|
|
_currentStroke.clear();
|
|
_lastRenderedIndex = 0;
|
|
_lastPointTime = 0;
|
|
const auto now = crl::now();
|
|
addStrokePoint(_lastPoint, now);
|
|
_contentRect = QRectF(_lastPoint, _lastPoint) + _brushMargins;
|
|
_drawing = true;
|
|
}
|
|
|
|
void ItemCanvas::handleMouseMoveEvent(
|
|
not_null<QGraphicsSceneMouseEvent*> e) {
|
|
if (!_drawing) {
|
|
return;
|
|
}
|
|
const auto scenePos = e->scenePos();
|
|
if (!IsValidPoint(scenePos)) {
|
|
return;
|
|
}
|
|
const auto now = crl::now();
|
|
addStrokePoint(scenePos, now);
|
|
_lastPoint = scenePos;
|
|
if (_currentStroke.size() - _lastRenderedIndex >= 3) {
|
|
drawIncrementalStroke();
|
|
update(_rectToUpdate);
|
|
}
|
|
}
|
|
|
|
void ItemCanvas::handleMouseReleaseEvent(
|
|
not_null<QGraphicsSceneMouseEvent*> e) {
|
|
if (!_drawing) {
|
|
return;
|
|
}
|
|
_drawing = false;
|
|
drawIncrementalStroke();
|
|
drawArrowHead();
|
|
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(),
|
|
.clear = (_brushData.tool == Brush::Tool::Eraser),
|
|
.blur = (_brushData.tool == Brush::Tool::Blur),
|
|
});
|
|
}
|
|
_currentStroke.clear();
|
|
_lastRenderedIndex = 0;
|
|
_lastPointTime = 0;
|
|
_currentPath = QPainterPath();
|
|
clearPixmap();
|
|
update();
|
|
}
|
|
|
|
void ItemCanvas::paint(
|
|
QPainter *p,
|
|
const QStyleOptionGraphicsItem *,
|
|
QWidget *) {
|
|
p->fillRect(_rectToUpdate, Qt::transparent);
|
|
if (_brushData.tool == Brush::Tool::Eraser) {
|
|
p->save();
|
|
p->setOpacity(st::photoEditorEraserPreviewOpacity);
|
|
p->drawPixmap(0, 0, _pixmap);
|
|
p->restore();
|
|
} else if (_brushData.tool == Brush::Tool::Blur) {
|
|
p->save();
|
|
p->setOpacity(st::photoEditorBlurPreviewOpacity);
|
|
p->drawPixmap(0, 0, _pixmap);
|
|
p->restore();
|
|
} else {
|
|
p->drawPixmap(0, 0, _pixmap);
|
|
}
|
|
_rectToUpdate = QRectF();
|
|
}
|
|
|
|
rpl::producer<ItemCanvas::Content> ItemCanvas::grabContentRequests() const {
|
|
return _grabContentRequests.events();
|
|
}
|
|
|
|
bool ItemCanvas::collidesWithItem(
|
|
const QGraphicsItem *,
|
|
Qt::ItemSelectionMode) const {
|
|
return false;
|
|
}
|
|
|
|
bool ItemCanvas::collidesWithPath(
|
|
const QPainterPath &,
|
|
Qt::ItemSelectionMode) const {
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
} // namespace Editor
|