From 23c2bce1bb94cab3869811b89df2be2d54b80f21 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 4 Jun 2021 19:42:02 +0400 Subject: [PATCH] Animated zoom+rotate in OpenGL media viewer. --- Telegram/Resources/langs/lang.strings | 2 +- .../media/view/media_view_overlay_opengl.cpp | 59 ++++----- .../media/view/media_view_overlay_opengl.h | 9 +- .../media/view/media_view_overlay_raster.cpp | 30 ++++- .../media/view/media_view_overlay_raster.h | 7 +- .../media/view/media_view_overlay_renderer.h | 5 +- .../media/view/media_view_overlay_widget.cpp | 116 ++++++++++++------ .../media/view/media_view_overlay_widget.h | 14 ++- 8 files changed, 156 insertions(+), 86 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 9ff1a7ca3..c5d01d8c9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -451,7 +451,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_system_integration" = "System integration"; "lng_settings_performance" = "Performance"; "lng_settings_enable_animations" = "Enable animations"; -"lng_settings_enable_opengl" = "Enable OpenGL rendering for video"; +"lng_settings_enable_opengl" = "Enable OpenGL rendering for media"; "lng_settings_sensitive_title" = "Sensitive content"; "lng_settings_sensitive_disable_filtering" = "Disable filtering"; "lng_settings_sensitive_about" = "Display sensitive media in public channels on all your Telegram devices."; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp index 4fd05b967..8ba007cf1 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp @@ -185,9 +185,6 @@ void OverlayWidget::RendererGL::paintBackground() { ? st::mediaviewVideoBg : st::mediaviewBg; auto fill = QRegion(QRect(QPoint(), _viewport)); - if (_owner->opaqueContentShown()) { - fill -= _owner->contentRect(); - } toggleBlending(false); _background.fill( *_f, @@ -199,19 +196,14 @@ void OverlayWidget::RendererGL::paintBackground() { } void OverlayWidget::RendererGL::paintTransformedVideoFrame( - QRect rect, - int rotation) { + ContentGeometry geometry) { const auto data = _owner->videoFrameWithInfo(); if (data.format == Streaming::FrameFormat::None) { return; } if (data.format == Streaming::FrameFormat::ARGB32) { Assert(!data.original.isNull()); - paintTransformedStaticContent( - data.original, - rect, - rotation, - false); + paintTransformedStaticContent(data.original, geometry, false); return; } Assert(data.format == Streaming::FrameFormat::YUV420); @@ -265,13 +257,12 @@ void OverlayWidget::RendererGL::paintTransformedVideoFrame( _yuv420Program->setUniformValue("u_texture", GLint(1)); _yuv420Program->setUniformValue("v_texture", GLint(2)); - paintTransformedContent(&*_yuv420Program, rect, rotation); + paintTransformedContent(&*_yuv420Program, geometry); } void OverlayWidget::RendererGL::paintTransformedStaticContent( const QImage &image, - QRect rect, - int rotation, + ContentGeometry geometry, bool fillTransparentBackground) { auto &program = fillTransparentBackground ? _withTransparencyProgram @@ -307,38 +298,46 @@ void OverlayWidget::RendererGL::paintTransformedStaticContent( _rgbaSize = image.size(); } - paintTransformedContent(&*program, rect, rotation); + paintTransformedContent(&*program, geometry); } void OverlayWidget::RendererGL::paintTransformedContent( not_null program, - QRect rect, - int rotation) { + ContentGeometry geometry) { auto texCoords = std::array, 4> { { { { 0.f, 1.f } }, { { 1.f, 1.f } }, { { 1.f, 0.f } }, { { 0.f, 0.f } }, } }; - if (const auto shift = (rotation / 90); shift > 0) { - std::rotate( - texCoords.begin(), - texCoords.begin() + shift, - texCoords.end()); - } - - const auto geometry = transformRect(rect); + const auto rect = transformRect(geometry.rect); + const auto centerx = rect.x() + rect.width() / 2; + const auto centery = rect.y() + rect.height() / 2; + const auto rsin = std::sinf(geometry.rotation * M_PI / 180.); + const auto rcos = std::cosf(geometry.rotation * M_PI / 180.); + const auto rotated = [&](float x, float y) -> std::array { + x -= centerx; + y -= centery; + return { + centerx + (x * rcos + y * rsin), + centery + (y * rcos - x * rsin) + }; + }; + const auto topleft = rotated(rect.left(), rect.top()); + const auto topright = rotated(rect.right(), rect.top()); + const auto bottomright = rotated(rect.right(), rect.bottom()); + const auto bottomleft = rotated(rect.left(), rect.bottom()); const GLfloat coords[] = { - geometry.left(), geometry.top(), + topleft[0], topleft[1], texCoords[0][0], texCoords[0][1], - geometry.right(), geometry.top(), + topright[0], topright[1], texCoords[1][0], texCoords[1][1], - geometry.right(), geometry.bottom(), + bottomright[0], bottomright[1], texCoords[2][0], texCoords[2][1], - geometry.left(), geometry.bottom(), + bottomleft[0], bottomleft[1], texCoords[3][0], texCoords[3][1], }; @@ -661,6 +660,10 @@ Rect OverlayWidget::RendererGL::transformRect(const Rect &raster) const { return TransformRect(raster, _viewport, _factor); } +Rect OverlayWidget::RendererGL::transformRect(const QRectF &raster) const { + return TransformRect(raster, _viewport, _factor); +} + Rect OverlayWidget::RendererGL::transformRect(const QRect &raster) const { return TransformRect(Rect(raster), _viewport, _factor); } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h index e1abb7248..a04253622 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h @@ -47,16 +47,14 @@ private: void setDefaultViewport(QOpenGLFunctions &f); void paintBackground(); - void paintTransformedVideoFrame(QRect rect, int rotation) override; + void paintTransformedVideoFrame(ContentGeometry geometry) override; void paintTransformedStaticContent( const QImage &image, - QRect rect, - int rotation, + ContentGeometry geometry, bool fillTransparentBackground) override; void paintTransformedContent( not_null program, - QRect rect, - int rotation); + ContentGeometry geometry); void paintRadialLoading( QRect inner, bool radial, @@ -90,6 +88,7 @@ private: void toggleBlending(bool enabled); [[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const; + [[nodiscard]] Ui::GL::Rect transformRect(const QRectF &raster) const; [[nodiscard]] Ui::GL::Rect transformRect( const Ui::GL::Rect &raster) const; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp index 039f10882..2e0e7e624 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp @@ -30,7 +30,7 @@ void OverlayWidget::RendererSW::paintFallback( void OverlayWidget::RendererSW::paintBackground() { const auto region = _owner->opaqueContentShown() - ? (*_clip - _owner->contentRect()) + ? (*_clip - _owner->finalContentRect()) : *_clip; const auto m = _p->compositionMode(); @@ -44,11 +44,30 @@ void OverlayWidget::RendererSW::paintBackground() { _p->setCompositionMode(m); } -void OverlayWidget::RendererSW::paintTransformedVideoFrame( - QRect rect, +QRect OverlayWidget::RendererSW::TransformRect( + QRectF geometry, int rotation) { + const auto center = geometry.center(); + const auto rect = ((rotation % 180) == 90) + ? QRectF( + center.x() - geometry.height() / 2., + center.y() - geometry.width() / 2., + geometry.height(), + geometry.width()) + : geometry; + return QRect( + int(rect.x()), + int(rect.y()), + int(rect.width()), + int(rect.height())); +} + +void OverlayWidget::RendererSW::paintTransformedVideoFrame( + ContentGeometry geometry) { Expects(_owner->_streamed != nullptr); + const auto rotation = int(geometry.rotation); + const auto rect = TransformRect(geometry.rect, rotation); if (!rect.intersects(_clipOuter)) { return; } @@ -57,9 +76,10 @@ void OverlayWidget::RendererSW::paintTransformedVideoFrame( void OverlayWidget::RendererSW::paintTransformedStaticContent( const QImage &image, - QRect rect, - int rotation, + ContentGeometry geometry, bool fillTransparentBackground) { + const auto rotation = int(geometry.rotation); + const auto rect = TransformRect(geometry.rect, rotation); if (!rect.intersects(_clipOuter)) { return; } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.h b/Telegram/SourceFiles/media/view/media_view_overlay_raster.h index b31a47425..7b0462bb8 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_raster.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.h @@ -23,11 +23,10 @@ public: private: void paintBackground() override; - void paintTransformedVideoFrame(QRect rect, int rotation) override; + void paintTransformedVideoFrame(ContentGeometry geometry) override; void paintTransformedStaticContent( const QImage &image, - QRect rect, - int rotation, + ContentGeometry geometry, bool fillTransparentBackground) override; void paintTransformedImage( const QImage &image, @@ -54,6 +53,8 @@ private: void invalidate() override; + [[nodiscard]] static QRect TransformRect(QRectF geometry, int rotation); + const not_null _owner; QBrush _transparentBrush; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_renderer.h b/Telegram/SourceFiles/media/view/media_view_overlay_renderer.h index d258772c6..d57ef8f6f 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_renderer.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_renderer.h @@ -14,11 +14,10 @@ namespace Media::View { class OverlayWidget::Renderer : public Ui::GL::Renderer { public: virtual void paintBackground() = 0; - virtual void paintTransformedVideoFrame(QRect rect, int rotation) = 0; + virtual void paintTransformedVideoFrame(ContentGeometry geometry) = 0; virtual void paintTransformedStaticContent( const QImage &image, - QRect rect, - int rotation, + ContentGeometry geometry, bool fillTransparentBackground) = 0; virtual void paintRadialLoading( QRect inner, diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 67705918a..1df3cec68 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -963,23 +963,62 @@ void OverlayWidget::updateCursor() { : (_over == OverNone ? style::cur_default : style::cur_pointer)); } -int OverlayWidget::contentRotation() const { - if (!_streamed) { - return _rotation; - } - return (_rotation + (_streamed - ? _streamed->instance.info().video.rotation - : 0)) % 360; +int OverlayWidget::finalContentRotation() const { + return _streamed + ? ((_rotation + (_streamed + ? _streamed->instance.info().video.rotation + : 0)) % 360) + : _rotation; } -QRect OverlayWidget::contentRect() const { - const auto progress = _zoomAnimation.value(1.); - return { - anim::interpolate(_xOld, _x, progress), - anim::interpolate(_yOld, _y, progress), - anim::interpolate(_wOld, _w, progress), - anim::interpolate(_hOld, _h, progress), - }; +QRect OverlayWidget::finalContentRect() const { + return { _x, _y, _w, _h }; +} + +OverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const { + const auto toRotation = qreal(finalContentRotation()); + const auto toRectRotated = QRectF(finalContentRect()); + const auto toRectCenter = toRectRotated.center(); + const auto toRect = ((int(toRotation) % 180) == 90) + ? QRectF( + toRectCenter.x() - toRectRotated.height() / 2., + toRectCenter.y() - toRectRotated.width() / 2., + toRectRotated.height(), + toRectRotated.width()) + : toRectRotated; + if (!_geometryAnimation.animating()) { + return { toRect, toRotation }; + } + const auto fromRect = _oldGeometry.rect; + const auto fromRotation = _oldGeometry.rotation; + const auto progress = _geometryAnimation.value(1.); + const auto rotationDelta = (toRotation - fromRotation); + const auto useRotationDelta = (rotationDelta > 180.) + ? (rotationDelta - 360.) + : (rotationDelta <= -180.) + ? (rotationDelta + 360.) + : rotationDelta; + const auto rotation = fromRotation + useRotationDelta * progress; + const auto useRotation = (rotation > 360.) + ? (rotation - 360.) + : (rotation < 0.) + ? (rotation + 360.) + : rotation; + const auto useRect = QRectF( + fromRect.x() + (toRect.x() - fromRect.x()) * progress, + fromRect.y() + (toRect.y() - fromRect.y()) * progress, + fromRect.width() + (toRect.width() - fromRect.width()) * progress, + fromRect.height() + (toRect.height() - fromRect.height()) * progress + ); + return { useRect, useRotation }; +} + +void OverlayWidget::updateContentRect() { + if (_opengl) { + update(); + } else { + update(finalContentRect()); + } } void OverlayWidget::contentSizeChanged() { @@ -1035,7 +1074,7 @@ void OverlayWidget::resizeContentByScreenSize() { } _x = (width() - _w) / 2; _y = (height() - _h) / 2; - _zoomAnimation.stop(); + _geometryAnimation.stop(); } float64 OverlayWidget::radialProgress() const { @@ -2612,13 +2651,13 @@ void OverlayWidget::streamingReady(Streaming::Information &&info) { if (videoShown()) { applyVideoSize(); } - update(contentRect()); + updateContentRect(); } void OverlayWidget::applyVideoSize() { const auto contentSize = style::ConvertScale(videoSize()); if (contentSize != QSize(_width, _height)) { - update(contentRect()); + updateContentRect(); _w = contentSize.width(); _h = contentSize.height(); contentSizeChanged(); @@ -2668,7 +2707,7 @@ bool OverlayWidget::createStreamingObjects() { QImage OverlayWidget::transformedShownContent() const { return transformShownContent( videoShown() ? currentVideoFrameImage() : _staticContent, - contentRotation()); + finalContentRotation()); } QImage OverlayWidget::transformShownContent( @@ -2697,7 +2736,7 @@ void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) { }, [&](const PreloadedVideo &update) { updatePlaybackState(); }, [&](const UpdateVideo &update) { - this->update(contentRect()); + updateContentRect(); Core::App().updateNonIdle(); updatePlaybackState(); }, [&](const PreloadedAudio &update) { @@ -2874,6 +2913,8 @@ void OverlayWidget::playbackControlsToPictureInPicture() { } void OverlayWidget::playbackControlsRotate() { + _oldGeometry = contentGeometry(); + _geometryAnimation.stop(); if (_photo) { auto &storage = _photo->owner().mediaRotation(); storage.set(_photo, storage.get(_photo) - 90); @@ -2885,11 +2926,19 @@ void OverlayWidget::playbackControlsRotate() { _rotation = storage.get(_document); if (videoShown()) { applyVideoSize(); - update(contentRect()); + updateContentRect(); } else { redisplayContent(); } } + if (_opengl) { + _geometryAnimation.start( + [=] { update(); }, + 0., + 1., + st::widgetFadeDuration/*, + st::easeOutCirc*/); + } } void OverlayWidget::playbackPauseResume() { @@ -2923,7 +2972,7 @@ void OverlayWidget::restartAtSeekPosition(crl::time position) { const auto saved = base::take(_rotation); _staticContent = transformedShownContent(); _rotation = saved; - update(contentRect()); + updateContentRect(); } auto options = Streaming::PlaybackOptions(); options.position = position; @@ -3168,10 +3217,8 @@ Ui::GL::ChosenRenderer OverlayWidget::chooseRenderer( void OverlayWidget::paint(not_null renderer) { renderer->paintBackground(); if (contentShown()) { - const auto rect = contentRect(); - const auto rotation = contentRotation(); if (videoShown()) { - renderer->paintTransformedVideoFrame(rect, rotation); + renderer->paintTransformedVideoFrame(contentGeometry()); if (_streamed->instance.player().ready()) { _streamed->instance.markFrameShown(); } @@ -3183,8 +3230,7 @@ void OverlayWidget::paint(not_null renderer) { || _staticContent.hasAlphaChannel()); renderer->paintTransformedStaticContent( _staticContent, - rect, - rotation, + contentGeometry(), fillTransparentBackground); } paintRadialLoading(renderer); @@ -3736,11 +3782,8 @@ void OverlayWidget::setZoomLevel(int newZoom, bool force) { const auto contentSize = videoShown() ? style::ConvertScale(videoSize()) : QSize(_width, _height); - const auto current = contentRect(); - _xOld = current.x(); - _yOld = current.y(); - _wOld = current.width(); - _hOld = current.height(); + _oldGeometry = contentGeometry(); + _geometryAnimation.stop(); _w = contentSize.width(); _h = contentSize.height(); @@ -3764,15 +3807,14 @@ void OverlayWidget::setZoomLevel(int newZoom, bool force) { _x = qRound(nx / (-z + 1) + width() / 2.); _y = qRound(ny / (-z + 1) + height() / 2.); } - _zoomAnimation.stop(); snapXY(); if (_opengl) { - _zoomAnimation.start( + _geometryAnimation.start( [=] { update(); }, 0., 1., - st::widgetFadeDuration, - anim::easeOutCirc); + st::widgetFadeDuration/*, + anim::easeOutCirc*/); } update(); } @@ -4197,7 +4239,7 @@ void OverlayWidget::updateOver(QPoint pos) { updateOverState(OverMore); } else if (_closeNav.contains(pos)) { updateOverState(OverClose); - } else if (documentContentShown() && contentRect().contains(pos)) { + } else if (documentContentShown() && finalContentRect().contains(pos)) { if ((_document->isVideoFile() || _document->isVideoMessage()) && _streamed) { updateOverState(OverVideo); } else if (!_streamed && !_documentMedia->loaded()) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 8ad9edf03..974b43643 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -143,6 +143,10 @@ private: QuickSave, SaveAs, }; + struct ContentGeometry { + QRectF rect; + qreal rotation = 0.; + }; [[nodiscard]] not_null window() const; [[nodiscard]] int width() const; @@ -322,8 +326,10 @@ private: void documentUpdated(DocumentData *doc); void changingMsgId(not_null row, MsgId oldId); - [[nodiscard]] int contentRotation() const; - [[nodiscard]] QRect contentRect() const; + [[nodiscard]] int finalContentRotation() const; + [[nodiscard]] QRect finalContentRect() const; + [[nodiscard]] ContentGeometry contentGeometry() const; + void updateContentRect(); void contentSizeChanged(); // Radial animation interface. @@ -467,14 +473,14 @@ private: int _zoom = 0; // < 0 - out, 0 - none, > 0 - in float64 _zoomToScreen = 0.; // for documents float64 _zoomToDefault = 0.; - int _xOld = 0, _yOld = 0, _wOld = 0, _hOld = 0; - Ui::Animations::Simple _zoomAnimation; QPoint _mStart; bool _pressed = false; int32 _dragging = 0; QImage _staticContent; bool _blurred = true; + ContentGeometry _oldGeometry; + Ui::Animations::Simple _geometryAnimation; rpl::lifetime _screenGeometryLifetime; std::unique_ptr _applicationEventFilter;