From dc7f440902c2f5b16b3262670cb68337276e4039 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 16 Feb 2021 06:45:05 +0300 Subject: [PATCH] Added color picker to photo editor. --- Telegram/CMakeLists.txt | 2 + Telegram/SourceFiles/editor/color_picker.cpp | 304 ++++++++++++++++++ Telegram/SourceFiles/editor/color_picker.h | 63 ++++ Telegram/SourceFiles/editor/editor.style | 11 + Telegram/SourceFiles/editor/editor_paint.cpp | 12 +- Telegram/SourceFiles/editor/editor_paint.h | 1 + Telegram/SourceFiles/editor/photo_editor.cpp | 17 +- Telegram/SourceFiles/editor/photo_editor.h | 2 + .../SourceFiles/editor/photo_editor_common.h | 5 + .../editor/photo_editor_content.cpp | 7 + .../SourceFiles/editor/photo_editor_content.h | 1 + .../editor/photo_editor_controls.cpp | 8 +- 12 files changed, 426 insertions(+), 7 deletions(-) create mode 100644 Telegram/SourceFiles/editor/color_picker.cpp create mode 100644 Telegram/SourceFiles/editor/color_picker.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 46d5e7ac72..060c8d306b 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -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 diff --git a/Telegram/SourceFiles/editor/color_picker.cpp b/Telegram/SourceFiles/editor/color_picker.cpp new file mode 100644 index 0000000000..cd61efff7d --- /dev/null +++ b/Telegram/SourceFiles/editor/color_picker.cpp @@ -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 + +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 ¤t = stops[i]; + if (current.second == color) { + const auto prev = ((i - 1) < 0) + ? std::nullopt + : std::make_optional(stops[i - 1].first * width); + const auto next = ((i + 1) >= stops.size()) + ? std::nullopt + : std::make_optional(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 parent) +: _circleColor(Qt::white) +, _width(st::photoEditorColorPickerWidth) +, _lineHeight(st::photoEditorColorPickerLineHeight) +, _colorLine(base::make_unique_q(parent)) +, _canvasForCircle(base::make_unique_q(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 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(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 ¤t = _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 ColorPicker::brushValue() const { + return _brush.value(); +} + +} // namespace Editor diff --git a/Telegram/SourceFiles/editor/color_picker.h b/Telegram/SourceFiles/editor/color_picker.h new file mode 100644 index 0000000000..46f11b500a --- /dev/null +++ b/Telegram/SourceFiles/editor/color_picker.h @@ -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 stopPos = std::nullopt; + std::optional prevStopPos = std::nullopt; + std::optional nextStopPos = std::nullopt; + }; + + ColorPicker(not_null parent); + + void moveLine(const QPoint &position); + void setVisible(bool visible); + + rpl::producer 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 _colorLine; + const base::unique_qptr _canvasForCircle; + + const QGradientStops _gradientStops; + const OutlinedStop _outlinedStop; + const QBrush _gradientBrush; + + struct { + QPoint pos; + bool pressed = false; + } _down; + rpl::variable _brush; + + Ui::Animations::Simple _circleAnimation; + +}; + +} // namespace Editor diff --git a/Telegram/SourceFiles/editor/editor.style b/Telegram/SourceFiles/editor/editor.style index 1484cb3a1f..b24cc28e53 100644 --- a/Telegram/SourceFiles/editor/editor.style +++ b/Telegram/SourceFiles/editor/editor.style @@ -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; + diff --git a/Telegram/SourceFiles/editor/editor_paint.cpp b/Telegram/SourceFiles/editor/editor_paint.cpp index 49625ed012..e52017e09d 100644 --- a/Telegram/SourceFiles/editor/editor_paint.cpp +++ b/Telegram/SourceFiles/editor/editor_paint.cpp @@ -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 event) { const auto type = event->type(); const auto isPress = (type == QEvent::GraphicsSceneMousePress); @@ -249,4 +249,10 @@ std::vector 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 diff --git a/Telegram/SourceFiles/editor/editor_paint.h b/Telegram/SourceFiles/editor/editor_paint.h index da12053eb4..d0294b3fc9 100644 --- a/Telegram/SourceFiles/editor/editor_paint.h +++ b/Telegram/SourceFiles/editor/editor_paint.h @@ -30,6 +30,7 @@ public: [[nodiscard]] std::shared_ptr saveScene() const; void applyTransform(QRect geometry, int angle, bool flipped); + void applyBrush(const Brush &brush); void cancel(); void keepResult(); void updateUndoState(); diff --git a/Telegram/SourceFiles/editor/photo_editor.cpp b/Telegram/SourceFiles/editor/photo_editor.cpp index 0ef1ecabe4..4a0aa55995 100644 --- a/Telegram/SourceFiles/editor/photo_editor.cpp +++ b/Telegram/SourceFiles/editor/photo_editor.cpp @@ -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(this, _undoController)) { +, _controls(base::make_unique_q(this, _undoController)) +, _colorPicker(std::make_unique(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() { diff --git a/Telegram/SourceFiles/editor/photo_editor.h b/Telegram/SourceFiles/editor/photo_editor.h index 6d133aeffc..a0bcf2195a 100644 --- a/Telegram/SourceFiles/editor/photo_editor.h +++ b/Telegram/SourceFiles/editor/photo_editor.h @@ -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 _content; base::unique_qptr _controls; + const std::unique_ptr _colorPicker; rpl::variable _mode = PhotoEditorMode{ .mode = PhotoEditorMode::Mode::Transform, diff --git a/Telegram/SourceFiles/editor/photo_editor_common.h b/Telegram/SourceFiles/editor/photo_editor_common.h index c0d5c42811..b459913155 100644 --- a/Telegram/SourceFiles/editor/photo_editor_common.h +++ b/Telegram/SourceFiles/editor/photo_editor_common.h @@ -36,6 +36,11 @@ struct PhotoModifications { }; +struct Brush { + float sizeRatio = 0.; + QColor color; +}; + [[nodiscard]] QImage ImageModified( QImage image, const PhotoModifications &mods); diff --git a/Telegram/SourceFiles/editor/photo_editor_content.cpp b/Telegram/SourceFiles/editor/photo_editor_content.cpp index 464134b091..99758345b2 100644 --- a/Telegram/SourceFiles/editor/photo_editor_content.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_content.cpp @@ -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 diff --git a/Telegram/SourceFiles/editor/photo_editor_content.h b/Telegram/SourceFiles/editor/photo_editor_content.h index d9610d51ac..bf4a69da6e 100644 --- a/Telegram/SourceFiles/editor/photo_editor_content.h +++ b/Telegram/SourceFiles/editor/photo_editor_content.h @@ -27,6 +27,7 @@ public: void applyModifications(PhotoModifications modifications); void applyMode(const PhotoEditorMode &mode); + void applyBrush(const Brush &brush); void save(PhotoModifications &modifications); private: diff --git a/Telegram/SourceFiles/editor/photo_editor_controls.cpp b/Telegram/SourceFiles/editor/photo_editor_controls.cpp index 69cde03abc..38dc36d9cd 100644 --- a/Telegram/SourceFiles/editor/photo_editor_controls.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_controls.cpp @@ -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());