Animated zoom+rotate in OpenGL media viewer.

This commit is contained in:
John Preston 2021-06-04 19:42:02 +04:00
parent 5324a626be
commit 23c2bce1bb
8 changed files with 156 additions and 86 deletions

View file

@ -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.";

View file

@ -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<QOpenGLShaderProgram*> program,
QRect rect,
int rotation) {
ContentGeometry geometry) {
auto texCoords = std::array<std::array<GLfloat, 2>, 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<float, 2> {
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);
}

View file

@ -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<QOpenGLShaderProgram*> 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;

View file

@ -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;
}

View file

@ -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<OverlayWidget*> _owner;
QBrush _transparentBrush;

View file

@ -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,

View file

@ -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) {
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*> 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()) {

View file

@ -143,6 +143,10 @@ private:
QuickSave,
SaveAs,
};
struct ContentGeometry {
QRectF rect;
qreal rotation = 0.;
};
[[nodiscard]] not_null<QWindow*> window() const;
[[nodiscard]] int width() const;
@ -322,8 +326,10 @@ private:
void documentUpdated(DocumentData *doc);
void changingMsgId(not_null<HistoryItem*> 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<QObject> _applicationEventFilter;