Added color picker to photo editor.

This commit is contained in:
23rd 2021-02-16 06:45:05 +03:00
parent 4849376347
commit dc7f440902
12 changed files with 426 additions and 7 deletions

View File

@ -508,6 +508,8 @@ PRIVATE
dialogs/dialogs_search_from_controllers.h
dialogs/dialogs_widget.cpp
dialogs/dialogs_widget.h
editor/color_picker.cpp
editor/color_picker.h
editor/editor_crop.cpp
editor/editor_crop.h
editor/editor_paint.cpp

View File

@ -0,0 +1,304 @@
/*
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/color_picker.h"
#include "ui/rp_widget.h"
#include "styles/style_editor.h"
#include <QtGui/QLinearGradient>
namespace Editor {
namespace {
constexpr auto kPrecision = 1000;
constexpr auto kMinBrushSize = 0.1;
constexpr auto kMouseSkip = 1.4;
constexpr auto kMinInnerHeight = 0.2;
constexpr auto kMaxInnerHeight = 0.8;
constexpr auto kCircleDuration = crl::time(200);
constexpr auto kMax = 1.0;
ColorPicker::OutlinedStop FindOutlinedStop(
const QColor &color,
const QGradientStops &stops,
int width) {
for (auto i = 0; i < stops.size(); i++) {
const auto &current = stops[i];
if (current.second == color) {
const auto prev = ((i - 1) < 0)
? std::nullopt
: std::make_optional<int>(stops[i - 1].first * width);
const auto next = ((i + 1) >= stops.size())
? std::nullopt
: std::make_optional<int>(stops[i + 1].first * width);
return ColorPicker::OutlinedStop{
.stopPos = (current.first * width),
.prevStopPos = prev,
.nextStopPos = next,
};
}
}
return ColorPicker::OutlinedStop();
}
QGradientStops Colors() {
return QGradientStops{
{ 0.00f, QColor(234, 39, 57) },
{ 0.14f, QColor(219, 58, 210) },
{ 0.24f, QColor(48, 81, 227) },
{ 0.39f, QColor(73, 197, 237) },
{ 0.49f, QColor(128, 200, 100) },
{ 0.62f, QColor(252, 222, 101) },
{ 0.73f, QColor(252, 150, 77) },
{ 0.85f, QColor(0, 0, 0) },
{ 1.00f, QColor(255, 255, 255) } };
}
QBrush GradientBrush(const QPoint &p, const QGradientStops &stops) {
auto gradient = QLinearGradient(0, p.y(), p.x(), p.y());
gradient.setStops(stops);
return QBrush(std::move(gradient));
}
float RatioPrecise(float a) {
return int(a * kPrecision) / float(kPrecision);
}
inline float64 InterpolateF(float a, float b, float64 b_ratio) {
return a + float64(b - a) * b_ratio;
};
inline float64 InterpolationRatio(int from, int to, int result) {
return (result - from) / float64(to - from);
};
} // namespace
ColorPicker::ColorPicker(not_null<Ui::RpWidget*> parent)
: _circleColor(Qt::white)
, _width(st::photoEditorColorPickerWidth)
, _lineHeight(st::photoEditorColorPickerLineHeight)
, _colorLine(base::make_unique_q<Ui::RpWidget>(parent))
, _canvasForCircle(base::make_unique_q<Ui::RpWidget>(parent))
, _gradientStops(Colors())
, _outlinedStop(FindOutlinedStop(_circleColor, _gradientStops, _width))
, _gradientBrush(
GradientBrush(QPoint(_width, _lineHeight / 2), _gradientStops))
, _brush(Brush{ .sizeRatio = kMinBrushSize, .color = QColor() }) {
_colorLine->resize(_width, _lineHeight);
_canvasForCircle->resize(
_width + circleHeight(kMax),
st::photoEditorColorPickerCanvasHeight);
_canvasForCircle->setAttribute(Qt::WA_TransparentForMouseEvents);
_colorLine->paintRequest(
) | rpl::start_with_next([=] {
Painter p(_colorLine);
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(_gradientBrush);
const auto radius = _colorLine->height() / 2.;
p.drawRoundedRect(_colorLine->rect(), radius, radius);
}, _colorLine->lifetime());
_canvasForCircle->paintRequest(
) | rpl::start_with_next([=] {
Painter p(_canvasForCircle);
paintCircle(p);
}, _canvasForCircle->lifetime());
_colorLine->events(
) | rpl::start_with_next([=](not_null<QEvent*> event) {
const auto type = event->type();
const auto isPress = (type == QEvent::MouseButtonPress)
|| (type == QEvent::MouseButtonDblClick);
const auto isMove = (type == QEvent::MouseMove);
const auto isRelease = (type == QEvent::MouseButtonRelease);
if (!isPress && !isMove && !isRelease) {
return;
}
_down.pressed = !isRelease;
const auto progress = _circleAnimation.value(isPress ? 0. : 1.);
if (!isMove) {
const auto from = progress;
const auto to = isPress ? 1. : 0.;
_circleAnimation.stop();
_circleAnimation.start(
[=] { _canvasForCircle->update(); },
from,
to,
kCircleDuration * std::abs(to - from),
anim::easeOutCirc);
}
const auto e = static_cast<QMouseEvent*>(event.get());
updateMousePosition(e->pos(), progress);
_canvasForCircle->update();
}, _colorLine->lifetime());
}
void ColorPicker::updateMousePosition(const QPoint &pos, float64 progress) {
const auto mapped = _canvasForCircle->mapFromParent(
_colorLine->mapToParent(pos));
const auto height = circleHeight(progress);
const auto mappedY = int(mapped.y() - height * kMouseSkip);
const auto bottom = _canvasForCircle->height() - circleHeight(kMax);
const auto &skip = st::photoEditorColorPickerCircleSkip;
_down.pos = QPoint(
std::clamp(pos.x(), 0, _width),
std::clamp(mappedY, 0, bottom - skip));
// Convert Y to the brush size.
const auto from = 0;
const auto to = bottom - skip;
const auto size = (mappedY > to)
? _brush.current().sizeRatio // Don't change value.
: std::clamp(
1. - InterpolationRatio(from, to, _down.pos.y()),
kMinBrushSize,
1.);
const auto color = positionToColor(_down.pos.x());
_brush = Brush{
.sizeRatio = float(size),
.color = color,
};
}
void ColorPicker::moveLine(const QPoint &position) {
_colorLine->move(position
- QPoint(_colorLine->width() / 2, _colorLine->height() / 2));
_canvasForCircle->move(
_colorLine->x() - circleHeight(kMax) / 2,
_colorLine->y()
+ _colorLine->height()
+ ((circleHeight() - _colorLine->height()) / 2)
- _canvasForCircle->height());
}
QColor ColorPicker::positionToColor(int x) const {
const auto from = 0;
const auto to = _width;
const auto gradientRatio = InterpolationRatio(from, to, x);
for (auto i = 1; i < _gradientStops.size(); i++) {
const auto &previous = _gradientStops[i - 1];
const auto &current = _gradientStops[i];
const auto &fromStop = previous.first;
const auto &toStop = current.first;
const auto &fromColor = previous.second;
const auto &toColor = current.second;
if ((fromStop <= gradientRatio) && (toStop >= gradientRatio)) {
const auto stopRatio = RatioPrecise(
(gradientRatio - fromStop) / float64(toStop - fromStop));
return anim::color(fromColor, toColor, stopRatio);
}
}
return QColor();
}
void ColorPicker::paintCircle(Painter &p) {
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(_circleColor);
const auto progress = _circleAnimation.value(_down.pressed ? 1. : 0.);
const auto h = circleHeight(progress);
const auto bottom = _canvasForCircle->height() - h;
const auto circleX = _down.pos.x() + (circleHeight(kMax) - h) / 2;
const auto circleY = _circleAnimation.animating()
? anim::interpolate(bottom, _down.pos.y(), progress)
: _down.pressed
? _down.pos.y()
: bottom;
const auto r = QRect(circleX, circleY, h, h);
p.drawEllipse(r);
const auto innerH = InterpolateF(
h * kMinInnerHeight,
h * kMaxInnerHeight,
_brush.current().sizeRatio);
p.setBrush(_brush.current().color);
const auto innerRect = QRectF(
r.x() + (r.width() - innerH) / 2.,
r.y() + (r.height() - innerH) / 2.,
innerH,
innerH);
paintOutline(p, innerRect);
p.drawEllipse(innerRect);
}
void ColorPicker::paintOutline(Painter &p, const QRectF &rect) {
const auto &s = _outlinedStop;
if (!s.stopPos) {
return;
}
const auto draw = [&](float opacity) {
const auto was = p.opacity();
p.save();
p.setOpacity(opacity);
p.setPen(Qt::lightGray);
p.setPen(Qt::NoBrush);
p.drawEllipse(rect);
p.restore();
};
const auto x = _down.pos.x();
if (s.prevStopPos && (x >= s.prevStopPos && x <= s.stopPos)) {
const auto from = *s.prevStopPos;
const auto to = *s.stopPos;
const auto ratio = InterpolationRatio(from, to, x);
if (ratio >= 0. && ratio <= 1.) {
draw(ratio);
}
} else if (s.nextStopPos && (x >= s.stopPos && x <= s.nextStopPos)) {
const auto from = *s.stopPos;
const auto to = *s.nextStopPos;
const auto ratio = InterpolationRatio(from, to, x);
if (ratio >= 0. && ratio <= 1.) {
draw(1. - ratio);
}
}
}
int ColorPicker::circleHeight(float64 progress) const {
return anim::interpolate(
st::photoEditorColorPickerCircleSize,
st::photoEditorColorPickerCircleBigSize,
progress);
}
void ColorPicker::setVisible(bool visible) {
_colorLine->setVisible(visible);
_canvasForCircle->setVisible(visible);
}
rpl::producer<Brush> ColorPicker::brushValue() const {
return _brush.value();
}
} // namespace Editor

View File

@ -0,0 +1,63 @@
/*
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
*/
#pragma once
#include "base/unique_qptr.h"
#include "editor/photo_editor_common.h"
#include "ui/effects/animations.h"
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Editor {
class ColorPicker final {
public:
struct OutlinedStop {
std::optional<int> stopPos = std::nullopt;
std::optional<int> prevStopPos = std::nullopt;
std::optional<int> nextStopPos = std::nullopt;
};
ColorPicker(not_null<Ui::RpWidget*> parent);
void moveLine(const QPoint &position);
void setVisible(bool visible);
rpl::producer<Brush> brushValue() const;
private:
void paintCircle(Painter &p);
void paintOutline(Painter &p, const QRectF &rect);
QColor positionToColor(int x) const;
int circleHeight(float64 progress = 0.) const;
void updateMousePosition(const QPoint &pos, float64 progress);
const QColor _circleColor;
const int _width;
const int _lineHeight;
const base::unique_qptr<Ui::RpWidget> _colorLine;
const base::unique_qptr<Ui::RpWidget> _canvasForCircle;
const QGradientStops _gradientStops;
const OutlinedStop _outlinedStop;
const QBrush _gradientBrush;
struct {
QPoint pos;
bool pressed = false;
} _down;
rpl::variable<Brush> _brush;
Ui::Animations::Simple _circleAnimation;
};
} // namespace Editor

View File

@ -12,6 +12,7 @@ using "ui/widgets/widgets.style";
using "ui/chat/chat.style";
photoEditorControlsHeight: 100px;
photoEditorControlsTopSkip: 50px;
photoEditorButtonIconFg: historyComposeIconFg;
photoEditorButtonIconFgOver: historyComposeIconFgOver;
@ -44,3 +45,13 @@ photoEditorUndoButtonInactive: icon {{ "photo_editor/undo", photoEditorButtonIco
photoEditorRedoButtonInactive: icon {{ "photo_editor/undo-flip_horizontal", photoEditorButtonIconFgInactive }};
photoEditorTextButtonPadding: margins(10px, 0px, 10px, 0px);
photoEditorColorPickerTopSkip: 20px;
photoEditorColorPickerWidth: 250px;
photoEditorColorPickerLineHeight: 20px;
photoEditorColorPickerCanvasHeight: 300px;
photoEditorColorPickerCircleSize: 24px;
photoEditorColorPickerCircleBigSize: 50px;
photoEditorColorPickerCircleSkip: 50px;

View File

@ -18,6 +18,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Editor {
namespace {
constexpr auto kMaxBrush = 25.;
constexpr auto kMinBrush = 1.;
constexpr auto kViewStyle = "QGraphicsView {\
background-color: transparent;\
border: 0px\
@ -116,9 +119,6 @@ void Paint::applyTransform(QRect geometry, int angle, bool flipped) {
void Paint::initDrawing() {
using Result = base::EventFilterResult;
_brushData.size = 10;
_brushData.color = Qt::red;
auto callback = [=](not_null<QEvent*> event) {
const auto type = event->type();
const auto isPress = (type == QEvent::GraphicsSceneMousePress);
@ -249,4 +249,10 @@ std::vector<QGraphicsItem*> Paint::groups(Qt::SortOrder order) const {
) | ranges::views::filter(GroupsFilter) | ranges::to_vector;
}
void Paint::applyBrush(const Brush &brush) {
_brushData.color = brush.color;
_brushData.size =
(kMinBrush + float64(kMaxBrush - kMinBrush) * brush.sizeRatio);
}
} // namespace Editor

View File

@ -30,6 +30,7 @@ public:
[[nodiscard]] std::shared_ptr<QGraphicsScene> saveScene() const;
void applyTransform(QRect geometry, int angle, bool flipped);
void applyBrush(const Brush &brush);
void cancel();
void keepResult();
void updateUndoState();

View File

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/photo_editor.h"
#include "editor/color_picker.h"
#include "editor/photo_editor_content.h"
#include "editor/photo_editor_controls.h"
#include "editor/undo_controller.h"
@ -26,9 +27,13 @@ PhotoEditor::PhotoEditor(
photo,
_modifications,
_undoController))
, _controls(base::make_unique_q<PhotoEditorControls>(this, _undoController)) {
, _controls(base::make_unique_q<PhotoEditorControls>(this, _undoController))
, _colorPicker(std::make_unique<ColorPicker>(this)) {
sizeValue(
) | rpl::start_with_next([=](const QSize &size) {
if (size.isEmpty()) {
return;
}
const auto geometry = QRect(QPoint(), size);
const auto contentRect = geometry
- style::margins(0, 0, 0, st::photoEditorControlsHeight);
@ -36,12 +41,17 @@ PhotoEditor::PhotoEditor(
const auto controlsRect = geometry
- style::margins(0, contentRect.height(), 0, 0);
_controls->setGeometry(controlsRect);
_colorPicker->moveLine(QPoint(
controlsRect.x() + controlsRect.width() / 2,
controlsRect.y() + st::photoEditorColorPickerTopSkip));
}, lifetime());
_mode.value(
) | rpl::start_with_next([=](const PhotoEditorMode &mode) {
_content->applyMode(mode);
_controls->applyMode(mode);
_colorPicker->setVisible(mode.mode == PhotoEditorMode::Mode::Paint);
}, lifetime());
_controls->rotateRequests(
@ -86,6 +96,11 @@ PhotoEditor::PhotoEditor(
};
}
}, lifetime());
_colorPicker->brushValue(
) | rpl::start_with_next([=](const Brush &brush) {
_content->applyBrush(brush);
}, lifetime());
}
void PhotoEditor::save() {

View File

@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Editor {
class ColorPicker;
class PhotoEditorContent;
class PhotoEditorControls;
class UndoController;
@ -37,6 +38,7 @@ private:
base::unique_qptr<PhotoEditorContent> _content;
base::unique_qptr<PhotoEditorControls> _controls;
const std::unique_ptr<ColorPicker> _colorPicker;
rpl::variable<PhotoEditorMode> _mode = PhotoEditorMode{
.mode = PhotoEditorMode::Mode::Transform,

View File

@ -36,6 +36,11 @@ struct PhotoModifications {
};
struct Brush {
float sizeRatio = 0.;
QColor color;
};
[[nodiscard]] QImage ImageModified(
QImage image,
const PhotoModifications &mods);

View File

@ -94,6 +94,9 @@ void PhotoEditorContent::applyModifications(
void PhotoEditorContent::save(PhotoModifications &modifications) {
modifications.crop = _crop->saveCropRect(_imageRect, _photo->rect());
_paint->keepResult();
if (!modifications.paint) {
modifications.paint = _paint->saveScene();
}
}
void PhotoEditorContent::applyMode(const PhotoEditorMode &mode) {
@ -113,4 +116,8 @@ void PhotoEditorContent::applyMode(const PhotoEditorMode &mode) {
_mode = mode;
}
void PhotoEditorContent::applyBrush(const Brush &brush) {
_paint->applyBrush(brush);
}
} // namespace Editor

View File

@ -27,6 +27,7 @@ public:
void applyModifications(PhotoModifications modifications);
void applyMode(const PhotoEditorMode &mode);
void applyBrush(const Brush &brush);
void save(PhotoModifications &modifications);
private:

View File

@ -192,6 +192,8 @@ PhotoEditorControls::PhotoEditorControls(
}, lifetime());
const auto &buttonsTop = st::photoEditorControlsTopSkip;
rpl::combine(
sizeValue(),
_mode.value()
@ -208,10 +210,10 @@ PhotoEditorControls::PhotoEditorControls(
current->moveToLeft(
(size.width() - current->width()) / 2,
0);
buttonsTop);
_cancel->moveToLeft(current->x() - _cancel->width(), 0);
_done->moveToLeft(current->x() + current->width(), 0);
_cancel->moveToLeft(current->x() - _cancel->width(), buttonsTop);
_done->moveToLeft(current->x() + current->width(), buttonsTop);
}, lifetime());