You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1027 lines
38 KiB
1027 lines
38 KiB
/* |
|
KWin - the KDE window manager |
|
This file is part of the KDE project. |
|
|
|
SPDX-FileCopyrightText: 2011 Thomas Lübking <thomas.luebking@web.de> |
|
SPDX-FileCopyrightText: 2018 Vlad Zahorodnii <vlad.zahorodnii@kde.org> |
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later |
|
*/ |
|
|
|
#include "effect/animationeffect.h" |
|
#include "effect/anidata_p.h" |
|
#include "effect/effecthandler.h" |
|
#include "opengl/glshader.h" |
|
#include "opengl/glshadermanager.h" |
|
|
|
#include <QDateTime> |
|
#include <QTimer> |
|
#include <QVector3D> |
|
#include <QtDebug> |
|
|
|
namespace KWin |
|
{ |
|
|
|
QDebug operator<<(QDebug dbg, const KWin::FPx2 &fpx2) |
|
{ |
|
dbg.nospace() << fpx2[0] << "," << fpx2[1] << QString(fpx2.isValid() ? QStringLiteral(" (valid)") : QStringLiteral(" (invalid)")); |
|
return dbg.space(); |
|
} |
|
|
|
QElapsedTimer AnimationEffect::s_clock; |
|
|
|
class AnimationEffectPrivate |
|
{ |
|
public: |
|
AnimationEffectPrivate() |
|
{ |
|
m_animationsTouched = m_isInitialized = false; |
|
m_justEndedAnimation = 0; |
|
} |
|
AnimationEffect::AniMap m_animations; |
|
static quint64 m_animCounter; |
|
quint64 m_justEndedAnimation; // protect against cancel |
|
std::weak_ptr<FullScreenEffectLock> m_fullScreenEffectLock; |
|
bool m_needSceneRepaint, m_animationsTouched, m_isInitialized; |
|
}; |
|
|
|
quint64 AnimationEffectPrivate::m_animCounter = 0; |
|
|
|
AnimationEffect::AnimationEffect() |
|
: CrossFadeEffect() |
|
, d_ptr(std::make_unique<AnimationEffectPrivate>()) |
|
{ |
|
if (!s_clock.isValid()) { |
|
s_clock.start(); |
|
} |
|
/* this is the same as the QTimer::singleShot(0, SLOT(init())) kludge |
|
* defering the init and esp. the connection to the windowClosed slot */ |
|
QMetaObject::invokeMethod(this, &AnimationEffect::init, Qt::QueuedConnection); |
|
} |
|
|
|
AnimationEffect::~AnimationEffect() |
|
{ |
|
Q_D(AnimationEffect); |
|
if (d->m_isInitialized) { |
|
disconnect(effects, &EffectsHandler::windowDeleted, this, &AnimationEffect::_windowDeleted); |
|
} |
|
d->m_animations.clear(); |
|
} |
|
|
|
void AnimationEffect::init() |
|
{ |
|
Q_D(AnimationEffect); |
|
if (d->m_isInitialized) { |
|
return; // not more than once, please |
|
} |
|
d->m_isInitialized = true; |
|
/* by connecting the signal from a slot AFTER the inheriting class constructor had the chance to |
|
* connect it we can provide auto-referencing of animated and closed windows, since at the time |
|
* our slot will be called, the slot of the subclass has been (SIGNAL/SLOT connections are FIFO) |
|
* and has pot. started an animation so we have the window in our hash :) */ |
|
connect(effects, &EffectsHandler::windowClosed, this, &AnimationEffect::_windowClosed); |
|
connect(effects, &EffectsHandler::windowDeleted, this, &AnimationEffect::_windowDeleted); |
|
} |
|
|
|
bool AnimationEffect::isActive() const |
|
{ |
|
Q_D(const AnimationEffect); |
|
return !d->m_animations.isEmpty() && !effects->isScreenLocked(); |
|
} |
|
|
|
#define RELATIVE_XY(_FIELD_) const bool relative[2] = {static_cast<bool>(metaData(Relative##_FIELD_##X, meta)), \ |
|
static_cast<bool>(metaData(Relative##_FIELD_##Y, meta))} |
|
|
|
void AnimationEffect::validate(Attribute a, uint &meta, FPx2 *from, FPx2 *to, const EffectWindow *w) const |
|
{ |
|
if (a < NonFloatBase) { |
|
if (a == Scale) { |
|
QRectF area = effects->clientArea(ScreenArea, w); |
|
if (from && from->isValid()) { |
|
RELATIVE_XY(Source); |
|
from->set(relative[0] ? (*from)[0] * area.width() / w->width() : (*from)[0], |
|
relative[1] ? (*from)[1] * area.height() / w->height() : (*from)[1]); |
|
} |
|
if (to && to->isValid()) { |
|
RELATIVE_XY(Target); |
|
to->set(relative[0] ? (*to)[0] * area.width() / w->width() : (*to)[0], |
|
relative[1] ? (*to)[1] * area.height() / w->height() : (*to)[1]); |
|
} |
|
} else if (a == Rotation) { |
|
if (from && !from->isValid()) { |
|
setMetaData(SourceAnchor, metaData(TargetAnchor, meta), meta); |
|
from->set(0.0, 0.0); |
|
} |
|
if (to && !to->isValid()) { |
|
setMetaData(TargetAnchor, metaData(SourceAnchor, meta), meta); |
|
to->set(0.0, 0.0); |
|
} |
|
} |
|
if (from && !from->isValid()) { |
|
from->set(1.0, 1.0); |
|
} |
|
if (to && !to->isValid()) { |
|
to->set(1.0, 1.0); |
|
} |
|
|
|
} else if (a == Position) { |
|
QRectF area = effects->clientArea(ScreenArea, w); |
|
QPointF pt = w->frameGeometry().bottomRight(); // cannot be < 0 ;-) |
|
if (from) { |
|
if (from->isValid()) { |
|
RELATIVE_XY(Source); |
|
from->set(relative[0] ? area.x() + (*from)[0] * area.width() : (*from)[0], |
|
relative[1] ? area.y() + (*from)[1] * area.height() : (*from)[1]); |
|
} else { |
|
from->set(pt.x(), pt.y()); |
|
setMetaData(SourceAnchor, AnimationEffect::Bottom | AnimationEffect::Right, meta); |
|
} |
|
} |
|
|
|
if (to) { |
|
if (to->isValid()) { |
|
RELATIVE_XY(Target); |
|
to->set(relative[0] ? area.x() + (*to)[0] * area.width() : (*to)[0], |
|
relative[1] ? area.y() + (*to)[1] * area.height() : (*to)[1]); |
|
} else { |
|
to->set(pt.x(), pt.y()); |
|
setMetaData(TargetAnchor, AnimationEffect::Bottom | AnimationEffect::Right, meta); |
|
} |
|
} |
|
|
|
} else if (a == Size) { |
|
QRectF area = effects->clientArea(ScreenArea, w); |
|
if (from) { |
|
if (from->isValid()) { |
|
RELATIVE_XY(Source); |
|
from->set(relative[0] ? (*from)[0] * area.width() : (*from)[0], |
|
relative[1] ? (*from)[1] * area.height() : (*from)[1]); |
|
} else { |
|
from->set(w->width(), w->height()); |
|
} |
|
} |
|
|
|
if (to) { |
|
if (to->isValid()) { |
|
RELATIVE_XY(Target); |
|
to->set(relative[0] ? (*to)[0] * area.width() : (*to)[0], |
|
relative[1] ? (*to)[1] * area.height() : (*to)[1]); |
|
} else { |
|
to->set(w->width(), w->height()); |
|
} |
|
} |
|
|
|
} else if (a == Translation) { |
|
QRect area = w->rect().toRect(); |
|
if (from) { |
|
if (from->isValid()) { |
|
RELATIVE_XY(Source); |
|
from->set(relative[0] ? (*from)[0] * area.width() : (*from)[0], |
|
relative[1] ? (*from)[1] * area.height() : (*from)[1]); |
|
} else { |
|
from->set(0.0, 0.0); |
|
} |
|
} |
|
|
|
if (to) { |
|
if (to->isValid()) { |
|
RELATIVE_XY(Target); |
|
to->set(relative[0] ? (*to)[0] * area.width() : (*to)[0], |
|
relative[1] ? (*to)[1] * area.height() : (*to)[1]); |
|
} else { |
|
to->set(0.0, 0.0); |
|
} |
|
} |
|
|
|
} else if (a == Clip) { |
|
if (from && !from->isValid()) { |
|
from->set(1.0, 1.0); |
|
setMetaData(SourceAnchor, metaData(TargetAnchor, meta), meta); |
|
} |
|
if (to && !to->isValid()) { |
|
to->set(1.0, 1.0); |
|
setMetaData(TargetAnchor, metaData(SourceAnchor, meta), meta); |
|
} |
|
|
|
} else if (a == CrossFadePrevious) { |
|
if (from && !from->isValid()) { |
|
from->set(0.0); |
|
} |
|
if (to && !to->isValid()) { |
|
to->set(1.0); |
|
} |
|
} |
|
} |
|
|
|
quint64 AnimationEffect::p_animate(EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, const QEasingCurve &curve, int delay, FPx2 from, bool keepAtTarget, bool fullScreenEffect, bool keepAlive, GLShader *shader) |
|
{ |
|
const bool waitAtSource = from.isValid(); |
|
validate(a, meta, &from, &to, w); |
|
|
|
Q_D(AnimationEffect); |
|
if (!d->m_isInitialized) { |
|
init(); // needs to ensure the window gets removed if deleted in the same event cycle |
|
} |
|
AniMap::iterator it = d->m_animations.find(w); |
|
if (it == d->m_animations.end()) { |
|
connect(w, &EffectWindow::windowExpandedGeometryChanged, |
|
this, &AnimationEffect::_windowExpandedGeometryChanged); |
|
it = d->m_animations.insert(w, QPair<QList<AniData>, QRect>(QList<AniData>(), QRect())); |
|
} |
|
|
|
std::shared_ptr<FullScreenEffectLock> fullscreen; |
|
if (fullScreenEffect) { |
|
fullscreen = d->m_fullScreenEffectLock.lock(); |
|
if (!fullscreen) { |
|
fullscreen = std::make_shared<FullScreenEffectLock>(this); |
|
d->m_fullScreenEffectLock = fullscreen; |
|
} |
|
} |
|
|
|
if (a == CrossFadePrevious) { |
|
CrossFadeEffect::redirect(w); |
|
} |
|
|
|
it->first.append(AniData( |
|
a, // Attribute |
|
meta, // Metadata |
|
to, // Target |
|
delay, // Delay |
|
from, // Source |
|
waitAtSource, // Whether the animation should be kept at source |
|
fullscreen, // Full screen effect lock |
|
keepAlive, // Keep alive flag |
|
shader)); |
|
|
|
const quint64 ret_id = ++d->m_animCounter; |
|
AniData &animation = it->first.last(); |
|
animation.id = ret_id; |
|
|
|
animation.visibleRef = EffectWindowVisibleRef(w, EffectWindow::PAINT_DISABLED_BY_MINIMIZE | EffectWindow::PAINT_DISABLED_BY_DESKTOP | EffectWindow::PAINT_DISABLED); |
|
animation.timeLine.setDirection(TimeLine::Forward); |
|
animation.timeLine.setDuration(std::chrono::milliseconds(ms)); |
|
animation.timeLine.setEasingCurve(curve); |
|
animation.timeLine.setSourceRedirectMode(TimeLine::RedirectMode::Strict); |
|
animation.timeLine.setTargetRedirectMode(TimeLine::RedirectMode::Relaxed); |
|
|
|
animation.terminationFlags = TerminateAtSource; |
|
if (!keepAtTarget) { |
|
animation.terminationFlags |= TerminateAtTarget; |
|
} |
|
|
|
it->second = QRect(); |
|
|
|
d->m_animationsTouched = true; |
|
|
|
if (delay > 0) { |
|
QTimer::singleShot(delay, this, &AnimationEffect::triggerRepaint); |
|
const QSize &s = effects->virtualScreenSize(); |
|
if (waitAtSource) { |
|
w->addLayerRepaint(0, 0, s.width(), s.height()); |
|
} |
|
} else { |
|
triggerRepaint(); |
|
} |
|
if (shader) { |
|
CrossFadeEffect::redirect(w); |
|
} |
|
return ret_id; |
|
} |
|
|
|
bool AnimationEffect::retarget(quint64 animationId, FPx2 newTarget, int newRemainingTime) |
|
{ |
|
Q_D(AnimationEffect); |
|
if (animationId == d->m_justEndedAnimation) { |
|
return false; // this is just ending, do not try to retarget it |
|
} |
|
for (AniMap::iterator entry = d->m_animations.begin(), |
|
mapEnd = d->m_animations.end(); |
|
entry != mapEnd; ++entry) { |
|
for (QList<AniData>::iterator anim = entry->first.begin(), |
|
animEnd = entry->first.end(); |
|
anim != animEnd; ++anim) { |
|
if (anim->id == animationId) { |
|
anim->from.set(interpolated(*anim, 0), interpolated(*anim, 1)); |
|
validate(anim->attribute, anim->meta, nullptr, &newTarget, entry.key()); |
|
anim->to.set(newTarget[0], newTarget[1]); |
|
|
|
anim->timeLine.setDirection(TimeLine::Forward); |
|
anim->timeLine.setDuration(std::chrono::milliseconds(newRemainingTime)); |
|
anim->timeLine.reset(); |
|
|
|
if (anim->attribute == CrossFadePrevious) { |
|
CrossFadeEffect::redirect(entry.key()); |
|
} |
|
return true; |
|
} |
|
} |
|
} |
|
return false; // no animation found |
|
} |
|
|
|
bool AnimationEffect::freezeInTime(quint64 animationId, qint64 frozenTime) |
|
{ |
|
Q_D(AnimationEffect); |
|
|
|
if (animationId == d->m_justEndedAnimation) { |
|
return false; // this is just ending, do not try to retarget it |
|
} |
|
for (AniMap::iterator entry = d->m_animations.begin(), |
|
mapEnd = d->m_animations.end(); |
|
entry != mapEnd; ++entry) { |
|
for (QList<AniData>::iterator anim = entry->first.begin(), |
|
animEnd = entry->first.end(); |
|
anim != animEnd; ++anim) { |
|
if (anim->id == animationId) { |
|
if (frozenTime >= 0) { |
|
anim->timeLine.setElapsed(std::chrono::milliseconds(frozenTime)); |
|
} |
|
anim->frozenTime = frozenTime; |
|
return true; |
|
} |
|
} |
|
} |
|
return false; // no animation found |
|
} |
|
|
|
bool AnimationEffect::redirect(quint64 animationId, Direction direction, TerminationFlags terminationFlags) |
|
{ |
|
Q_D(AnimationEffect); |
|
if (animationId == d->m_justEndedAnimation) { |
|
return false; |
|
} |
|
|
|
for (auto entryIt = d->m_animations.begin(); entryIt != d->m_animations.end(); ++entryIt) { |
|
auto animIt = std::find_if(entryIt->first.begin(), entryIt->first.end(), |
|
[animationId](AniData &anim) { |
|
return anim.id == animationId; |
|
}); |
|
if (animIt == entryIt->first.end()) { |
|
continue; |
|
} |
|
|
|
switch (direction) { |
|
case Backward: |
|
animIt->timeLine.setDirection(TimeLine::Backward); |
|
break; |
|
|
|
case Forward: |
|
animIt->timeLine.setDirection(TimeLine::Forward); |
|
break; |
|
} |
|
|
|
animIt->terminationFlags = terminationFlags & ~TerminateAtTarget; |
|
|
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
bool AnimationEffect::complete(quint64 animationId) |
|
{ |
|
Q_D(AnimationEffect); |
|
|
|
if (animationId == d->m_justEndedAnimation) { |
|
return false; |
|
} |
|
|
|
for (auto entryIt = d->m_animations.begin(); entryIt != d->m_animations.end(); ++entryIt) { |
|
auto animIt = std::find_if(entryIt->first.begin(), entryIt->first.end(), |
|
[animationId](AniData &anim) { |
|
return anim.id == animationId; |
|
}); |
|
if (animIt == entryIt->first.end()) { |
|
continue; |
|
} |
|
|
|
animIt->timeLine.setElapsed(animIt->timeLine.duration()); |
|
unredirect(entryIt.key()); |
|
|
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
bool AnimationEffect::cancel(quint64 animationId) |
|
{ |
|
Q_D(AnimationEffect); |
|
if (animationId == d->m_justEndedAnimation) { |
|
return true; // this is just ending, do not try to cancel it but fake success |
|
} |
|
for (AniMap::iterator entry = d->m_animations.begin(), mapEnd = d->m_animations.end(); entry != mapEnd; ++entry) { |
|
for (QList<AniData>::iterator anim = entry->first.begin(), animEnd = entry->first.end(); anim != animEnd; ++anim) { |
|
if (anim->id == animationId) { |
|
EffectWindowDeletedRef ref = std::move(anim->deletedRef); // delete window once we're done updating m_animations |
|
if (anim->shader && std::none_of(entry->first.begin(), entry->first.end(), [animationId](const auto &anim) { |
|
return anim.id != animationId && anim.shader; |
|
})) { |
|
unredirect(entry.key()); |
|
} |
|
entry->first.erase(anim); // remove the animation |
|
if (entry->first.isEmpty()) { // no other animations on the window, release it. |
|
disconnect(entry.key(), &EffectWindow::windowExpandedGeometryChanged, |
|
this, &AnimationEffect::_windowExpandedGeometryChanged); |
|
d->m_animations.erase(entry); |
|
} |
|
d->m_animationsTouched = true; // could be called from animationEnded |
|
return true; |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
void AnimationEffect::animationEnded(EffectWindow *w, Attribute a, uint meta) |
|
{ |
|
} |
|
|
|
void AnimationEffect::genericAnimation(EffectWindow *w, WindowPaintData &data, float progress, uint meta) |
|
{ |
|
} |
|
|
|
static qreal xCoord(const QRectF &r, int flag) |
|
{ |
|
if (flag & AnimationEffect::Left) { |
|
return r.x(); |
|
} else if (flag & AnimationEffect::Right) { |
|
return r.right(); |
|
} else { |
|
return r.x() + r.width() / 2; |
|
} |
|
} |
|
|
|
static qreal yCoord(const QRectF &r, int flag) |
|
{ |
|
if (flag & AnimationEffect::Top) { |
|
return r.y(); |
|
} else if (flag & AnimationEffect::Bottom) { |
|
return r.bottom(); |
|
} else { |
|
return r.y() + r.height() / 2; |
|
} |
|
} |
|
|
|
QRect AnimationEffect::clipRect(const QRect &geo, const AniData &anim) const |
|
{ |
|
QRect clip = geo; |
|
FPx2 ratio = anim.from + progress(anim) * (anim.to - anim.from); |
|
if (anim.from[0] < 1.0 || anim.to[0] < 1.0) { |
|
clip.setWidth(clip.width() * ratio[0]); |
|
} |
|
if (anim.from[1] < 1.0 || anim.to[1] < 1.0) { |
|
clip.setHeight(clip.height() * ratio[1]); |
|
} |
|
const QRect center = geo.adjusted(clip.width() / 2, clip.height() / 2, |
|
-(clip.width() + 1) / 2, -(clip.height() + 1) / 2); |
|
const qreal x[2] = {xCoord(center, metaData(SourceAnchor, anim.meta)), |
|
xCoord(center, metaData(TargetAnchor, anim.meta))}; |
|
const qreal y[2] = {yCoord(center, metaData(SourceAnchor, anim.meta)), |
|
yCoord(center, metaData(TargetAnchor, anim.meta))}; |
|
const QPoint d(x[0] + ratio[0] * (x[1] - x[0]), y[0] + ratio[1] * (y[1] - y[0])); |
|
clip.moveTopLeft(QPoint(d.x() - clip.width() / 2, d.y() - clip.height() / 2)); |
|
return clip; |
|
} |
|
|
|
void AnimationEffect::prePaintWindow(EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) |
|
{ |
|
Q_D(AnimationEffect); |
|
auto entry = d->m_animations.find(w); |
|
if (entry != d->m_animations.end()) { |
|
for (auto anim = entry->first.begin(); anim != entry->first.end(); ++anim) { |
|
if (anim->startTime > clock() && !anim->waitAtSource) { |
|
continue; |
|
} |
|
|
|
if (anim->frozenTime < 0) { |
|
anim->timeLine.advance(presentTime); |
|
} |
|
|
|
if (anim->attribute == Opacity || anim->attribute == CrossFadePrevious) { |
|
data.setTranslucent(); |
|
} else if (!(anim->attribute == Brightness || anim->attribute == Saturation)) { |
|
data.setTransformed(); |
|
} |
|
} |
|
} |
|
effects->prePaintWindow(w, data, presentTime); |
|
} |
|
|
|
static inline float geometryCompensation(int flags, float v) |
|
{ |
|
if (flags & (AnimationEffect::Left | AnimationEffect::Top)) { |
|
return 0.0; // no compensation required |
|
} |
|
if (flags & (AnimationEffect::Right | AnimationEffect::Bottom)) { |
|
return 1.0 - v; // full compensation |
|
} |
|
return 0.5 * (1.0 - v); // half compensation |
|
} |
|
|
|
void AnimationEffect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, QRegion region, WindowPaintData &data) |
|
{ |
|
Q_D(AnimationEffect); |
|
AniMap::const_iterator entry = d->m_animations.constFind(w); |
|
|
|
if (entry != d->m_animations.constEnd()) { |
|
for (QList<AniData>::const_iterator anim = entry->first.constBegin(); anim != entry->first.constEnd(); ++anim) { |
|
|
|
if (anim->startTime > clock() && !anim->waitAtSource) { |
|
continue; |
|
} |
|
|
|
switch (anim->attribute) { |
|
case Opacity: |
|
data.multiplyOpacity(interpolated(*anim)); |
|
break; |
|
case Brightness: |
|
data.multiplyBrightness(interpolated(*anim)); |
|
break; |
|
case Saturation: |
|
data.multiplySaturation(interpolated(*anim)); |
|
break; |
|
case Scale: { |
|
const QSizeF sz = w->frameGeometry().size(); |
|
float f1(1.0), f2(0.0); |
|
if (anim->from[0] >= 0.0 && anim->to[0] >= 0.0) { // scale x |
|
f1 = interpolated(*anim, 0); |
|
f2 = geometryCompensation(anim->meta & AnimationEffect::Horizontal, f1); |
|
data.translate(f2 * sz.width()); |
|
data.setXScale(data.xScale() * f1); |
|
} |
|
if (anim->from[1] >= 0.0 && anim->to[1] >= 0.0) { // scale y |
|
if (!anim->isOneDimensional()) { |
|
f1 = interpolated(*anim, 1); |
|
f2 = geometryCompensation(anim->meta & AnimationEffect::Vertical, f1); |
|
} else if (((anim->meta & AnimationEffect::Vertical) >> 1) != (anim->meta & AnimationEffect::Horizontal)) { |
|
f2 = geometryCompensation(anim->meta & AnimationEffect::Vertical, f1); |
|
} |
|
data.translate(0.0, f2 * sz.height()); |
|
data.setYScale(data.yScale() * f1); |
|
} |
|
break; |
|
} |
|
case Clip: |
|
region = clipRect(w->expandedGeometry().toAlignedRect(), *anim); |
|
break; |
|
case Translation: |
|
data += QPointF(interpolated(*anim, 0), interpolated(*anim, 1)); |
|
break; |
|
case Size: { |
|
FPx2 dest = anim->from + progress(*anim) * (anim->to - anim->from); |
|
const QSizeF sz = w->frameGeometry().size(); |
|
float f; |
|
if (anim->from[0] >= 0.0 && anim->to[0] >= 0.0) { // resize x |
|
f = dest[0] / sz.width(); |
|
data.translate(geometryCompensation(anim->meta & AnimationEffect::Horizontal, f) * sz.width()); |
|
data.setXScale(data.xScale() * f); |
|
} |
|
if (anim->from[1] >= 0.0 && anim->to[1] >= 0.0) { // resize y |
|
f = dest[1] / sz.height(); |
|
data.translate(0.0, geometryCompensation(anim->meta & AnimationEffect::Vertical, f) * sz.height()); |
|
data.setYScale(data.yScale() * f); |
|
} |
|
break; |
|
} |
|
case Position: { |
|
const QRectF geo = w->frameGeometry(); |
|
const float prgrs = progress(*anim); |
|
if (anim->from[0] >= 0.0 && anim->to[0] >= 0.0) { |
|
float dest = interpolated(*anim, 0); |
|
const qreal x[2] = {xCoord(geo, metaData(SourceAnchor, anim->meta)), |
|
xCoord(geo, metaData(TargetAnchor, anim->meta))}; |
|
data.translate(dest - (x[0] + prgrs * (x[1] - x[0]))); |
|
} |
|
if (anim->from[1] >= 0.0 && anim->to[1] >= 0.0) { |
|
float dest = interpolated(*anim, 1); |
|
const qreal y[2] = {yCoord(geo, metaData(SourceAnchor, anim->meta)), |
|
yCoord(geo, metaData(TargetAnchor, anim->meta))}; |
|
data.translate(0.0, dest - (y[0] + prgrs * (y[1] - y[0]))); |
|
} |
|
break; |
|
} |
|
case Rotation: { |
|
data.setRotationAxis((Qt::Axis)metaData(Axis, anim->meta)); |
|
const float prgrs = progress(*anim); |
|
data.setRotationAngle(anim->from[0] + prgrs * (anim->to[0] - anim->from[0])); |
|
|
|
const QRect geo = w->rect().toRect(); |
|
const uint sAnchor = metaData(SourceAnchor, anim->meta), |
|
tAnchor = metaData(TargetAnchor, anim->meta); |
|
QPointF pt(xCoord(geo, sAnchor), yCoord(geo, sAnchor)); |
|
|
|
if (tAnchor != sAnchor) { |
|
QPointF pt2(xCoord(geo, tAnchor), yCoord(geo, tAnchor)); |
|
pt += static_cast<qreal>(prgrs) * (pt2 - pt); |
|
} |
|
data.setRotationOrigin(QVector3D(pt)); |
|
break; |
|
} |
|
case Generic: |
|
genericAnimation(w, data, progress(*anim), anim->meta); |
|
break; |
|
case CrossFadePrevious: |
|
data.setCrossFadeProgress(progress(*anim)); |
|
break; |
|
case Shader: |
|
if (anim->shader && anim->shader->isValid()) { |
|
ShaderBinder binder{anim->shader}; |
|
anim->shader->setUniform("animationProgress", progress(*anim)); |
|
setShader(w, anim->shader); |
|
} |
|
break; |
|
case ShaderUniform: |
|
if (anim->shader && anim->shader->isValid()) { |
|
ShaderBinder binder{anim->shader}; |
|
anim->shader->setUniform("animationProgress", progress(*anim)); |
|
anim->shader->setUniform(anim->meta, interpolated(*anim)); |
|
setShader(w, anim->shader); |
|
} |
|
break; |
|
default: |
|
break; |
|
} |
|
} |
|
} |
|
|
|
effects->paintWindow(renderTarget, viewport, w, mask, region, data); |
|
} |
|
|
|
void AnimationEffect::postPaintScreen() |
|
{ |
|
Q_D(AnimationEffect); |
|
d->m_animationsTouched = false; |
|
bool damageDirty = false; |
|
std::vector<EffectWindowDeletedRef> zombies; |
|
|
|
for (auto entry = d->m_animations.begin(); entry != d->m_animations.end();) { |
|
bool invalidateLayerRect = false; |
|
int animCounter = 0; |
|
for (auto anim = entry->first.begin(); anim != entry->first.end();) { |
|
if (anim->isActive() || (anim->startTime > clock() && !anim->waitAtSource)) { |
|
++anim; |
|
++animCounter; |
|
continue; |
|
} |
|
EffectWindow *window = entry.key(); |
|
d->m_justEndedAnimation = anim->id; |
|
if (anim->shader && std::none_of(entry->first.begin(), entry->first.end(), [anim](const auto &other) { |
|
return anim->id != other.id && other.shader; |
|
})) { |
|
unredirect(window); |
|
} |
|
unredirect(window); |
|
animationEnded(window, anim->attribute, anim->meta); |
|
d->m_justEndedAnimation = 0; |
|
// NOTICE animationEnded is an external call and might have called "::animate" |
|
// as a result our iterators could now point random junk on the heap |
|
// so we've to restore the former states, ie. find our window list and animation |
|
if (d->m_animationsTouched) { |
|
d->m_animationsTouched = false; |
|
entry = d->m_animations.begin(); |
|
while (entry.key() != window && entry != d->m_animations.end()) { |
|
++entry; |
|
} |
|
Q_ASSERT(entry != d->m_animations.end()); // usercode should not delete animations from animationEnded (not even possible atm.) |
|
anim = entry->first.begin(); |
|
Q_ASSERT(animCounter < entry->first.count()); |
|
for (int i = 0; i < animCounter; ++i) { |
|
++anim; |
|
} |
|
} |
|
// If it's a closed window, keep it alive for a little bit longer until we're done |
|
// updating m_animations. Otherwise our windowDeleted slot can access m_animations |
|
// while we still modify it. |
|
if (!anim->deletedRef.isNull()) { |
|
zombies.emplace_back(std::move(anim->deletedRef)); |
|
} |
|
anim = entry->first.erase(anim); |
|
invalidateLayerRect = damageDirty = true; |
|
} |
|
if (entry->first.isEmpty()) { |
|
disconnect(entry.key(), &EffectWindow::windowExpandedGeometryChanged, |
|
this, &AnimationEffect::_windowExpandedGeometryChanged); |
|
effects->addRepaint(entry->second); |
|
entry = d->m_animations.erase(entry); |
|
} else { |
|
if (invalidateLayerRect) { |
|
*const_cast<QRect *>(&(entry->second)) = QRect(); // invalidate |
|
} |
|
++entry; |
|
} |
|
} |
|
|
|
if (damageDirty) { |
|
updateLayerRepaints(); |
|
} |
|
if (d->m_needSceneRepaint) { |
|
effects->addRepaintFull(); |
|
} else { |
|
for (auto entry = d->m_animations.constBegin(); entry != d->m_animations.constEnd(); ++entry) { |
|
for (auto anim = entry->first.constBegin(); anim != entry->first.constEnd(); ++anim) { |
|
if (anim->startTime > clock()) { |
|
continue; |
|
} |
|
if (!anim->timeLine.done()) { |
|
entry.key()->addLayerRepaint(entry->second); |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
effects->postPaintScreen(); |
|
} |
|
|
|
float AnimationEffect::interpolated(const AniData &a, int i) const |
|
{ |
|
return a.from[i] + a.timeLine.value() * (a.to[i] - a.from[i]); |
|
} |
|
|
|
float AnimationEffect::progress(const AniData &a) const |
|
{ |
|
return a.startTime < clock() ? a.timeLine.value() : 0.0; |
|
} |
|
|
|
// TODO - get this out of the header - the functionpointer usage of QEasingCurve somehow sucks ;-) |
|
// qreal AnimationEffect::qecGaussian(qreal progress) // exp(-5*(2*x-1)^2) |
|
// { |
|
// progress = 2*progress - 1; |
|
// progress *= -5*progress; |
|
// return qExp(progress); |
|
// } |
|
|
|
int AnimationEffect::metaData(MetaType type, uint meta) |
|
{ |
|
switch (type) { |
|
case SourceAnchor: |
|
return ((meta >> 5) & 0x1f); |
|
case TargetAnchor: |
|
return (meta & 0x1f); |
|
case RelativeSourceX: |
|
case RelativeSourceY: |
|
case RelativeTargetX: |
|
case RelativeTargetY: { |
|
const int shift = 10 + type - RelativeSourceX; |
|
return ((meta >> shift) & 1); |
|
} |
|
case Axis: |
|
return ((meta >> 10) & 3); |
|
default: |
|
return 0; |
|
} |
|
} |
|
|
|
void AnimationEffect::setMetaData(MetaType type, uint value, uint &meta) |
|
{ |
|
switch (type) { |
|
case SourceAnchor: |
|
meta &= ~(0x1f << 5); |
|
meta |= ((value & 0x1f) << 5); |
|
break; |
|
case TargetAnchor: |
|
meta &= ~(0x1f); |
|
meta |= (value & 0x1f); |
|
break; |
|
case RelativeSourceX: |
|
case RelativeSourceY: |
|
case RelativeTargetX: |
|
case RelativeTargetY: { |
|
const int shift = 10 + type - RelativeSourceX; |
|
if (value) { |
|
meta |= (1 << shift); |
|
} else { |
|
meta &= ~(1 << shift); |
|
} |
|
break; |
|
} |
|
case Axis: |
|
meta &= ~(3 << 10); |
|
meta |= ((value & 3) << 10); |
|
break; |
|
default: |
|
break; |
|
} |
|
} |
|
|
|
void AnimationEffect::triggerRepaint() |
|
{ |
|
Q_D(AnimationEffect); |
|
for (AniMap::const_iterator entry = d->m_animations.constBegin(), mapEnd = d->m_animations.constEnd(); entry != mapEnd; ++entry) { |
|
*const_cast<QRect *>(&(entry->second)) = QRect(); |
|
} |
|
updateLayerRepaints(); |
|
if (d->m_needSceneRepaint) { |
|
effects->addRepaintFull(); |
|
} else { |
|
AniMap::const_iterator it = d->m_animations.constBegin(), end = d->m_animations.constEnd(); |
|
for (; it != end; ++it) { |
|
it.key()->addLayerRepaint(it->second); |
|
} |
|
} |
|
} |
|
|
|
static float fixOvershoot(float f, const AniData &d, short int dir, float s = 1.1) |
|
{ |
|
switch (d.timeLine.easingCurve().type()) { |
|
case QEasingCurve::InOutElastic: |
|
case QEasingCurve::InOutBack: |
|
return f * s; |
|
case QEasingCurve::InElastic: |
|
case QEasingCurve::OutInElastic: |
|
case QEasingCurve::OutBack: |
|
return (dir & 2) ? f * s : f; |
|
case QEasingCurve::OutElastic: |
|
case QEasingCurve::InBack: |
|
return (dir & 1) ? f * s : f; |
|
default: |
|
return f; |
|
} |
|
} |
|
|
|
void AnimationEffect::updateLayerRepaints() |
|
{ |
|
Q_D(AnimationEffect); |
|
d->m_needSceneRepaint = false; |
|
for (AniMap::const_iterator entry = d->m_animations.constBegin(), mapEnd = d->m_animations.constEnd(); entry != mapEnd; ++entry) { |
|
if (!entry->second.isNull()) { |
|
continue; |
|
} |
|
float f[2] = {1.0, 1.0}; |
|
float t[2] = {0.0, 0.0}; |
|
bool createRegion = false; |
|
QList<QRect> rects; |
|
QRect *layerRect = const_cast<QRect *>(&(entry->second)); |
|
for (QList<AniData>::const_iterator anim = entry->first.constBegin(), animEnd = entry->first.constEnd(); anim != animEnd; ++anim) { |
|
if (anim->startTime > clock()) { |
|
continue; |
|
} |
|
switch (anim->attribute) { |
|
case Opacity: |
|
case Brightness: |
|
case Saturation: |
|
case CrossFadePrevious: |
|
case Shader: |
|
case ShaderUniform: |
|
createRegion = true; |
|
break; |
|
case Rotation: |
|
createRegion = false; |
|
*layerRect = QRect(QPoint(0, 0), effects->virtualScreenSize()); |
|
goto region_creation; // sic! no need to do anything else |
|
case Generic: |
|
d->m_needSceneRepaint = true; // we don't know whether this will change visual stacking order |
|
return; // sic! no need to do anything else |
|
case Translation: |
|
case Position: { |
|
createRegion = true; |
|
QRect r(entry.key()->frameGeometry().toRect()); |
|
int x[2] = {0, 0}; |
|
int y[2] = {0, 0}; |
|
if (anim->attribute == Translation) { |
|
x[0] = anim->from[0]; |
|
x[1] = anim->to[0]; |
|
y[0] = anim->from[1]; |
|
y[1] = anim->to[1]; |
|
} else { |
|
if (anim->from[0] >= 0.0 && anim->to[0] >= 0.0) { |
|
x[0] = anim->from[0] - xCoord(r, metaData(SourceAnchor, anim->meta)); |
|
x[1] = anim->to[0] - xCoord(r, metaData(TargetAnchor, anim->meta)); |
|
} |
|
if (anim->from[1] >= 0.0 && anim->to[1] >= 0.0) { |
|
y[0] = anim->from[1] - yCoord(r, metaData(SourceAnchor, anim->meta)); |
|
y[1] = anim->to[1] - yCoord(r, metaData(TargetAnchor, anim->meta)); |
|
} |
|
} |
|
r = entry.key()->expandedGeometry().toRect(); |
|
rects << r.translated(x[0], y[0]) << r.translated(x[1], y[1]); |
|
break; |
|
} |
|
case Clip: |
|
createRegion = true; |
|
break; |
|
case Size: |
|
case Scale: { |
|
createRegion = true; |
|
const QSize sz = entry.key()->frameGeometry().size().toSize(); |
|
float fx = std::max(fixOvershoot(anim->from[0], *anim, 1), fixOvershoot(anim->to[0], *anim, 2)); |
|
// float fx = std::max(interpolated(*anim,0), anim->to[0]); |
|
if (fx >= 0.0) { |
|
if (anim->attribute == Size) { |
|
fx /= sz.width(); |
|
} |
|
f[0] *= fx; |
|
t[0] += geometryCompensation(anim->meta & AnimationEffect::Horizontal, fx) * sz.width(); |
|
} |
|
// float fy = std::max(interpolated(*anim,1), anim->to[1]); |
|
float fy = std::max(fixOvershoot(anim->from[1], *anim, 1), fixOvershoot(anim->to[1], *anim, 2)); |
|
if (fy >= 0.0) { |
|
if (anim->attribute == Size) { |
|
fy /= sz.height(); |
|
} |
|
if (!anim->isOneDimensional()) { |
|
f[1] *= fy; |
|
t[1] += geometryCompensation(anim->meta & AnimationEffect::Vertical, fy) * sz.height(); |
|
} else if (((anim->meta & AnimationEffect::Vertical) >> 1) != (anim->meta & AnimationEffect::Horizontal)) { |
|
f[1] *= fx; |
|
t[1] += geometryCompensation(anim->meta & AnimationEffect::Vertical, fx) * sz.height(); |
|
} |
|
} |
|
break; |
|
} |
|
} |
|
} |
|
region_creation: |
|
if (createRegion) { |
|
const QRect geo = entry.key()->expandedGeometry().toRect(); |
|
if (rects.isEmpty()) { |
|
rects << geo; |
|
} |
|
QList<QRect>::const_iterator r, rEnd = rects.constEnd(); |
|
for (r = rects.constBegin(); r != rEnd; ++r) { // transform |
|
const_cast<QRect *>(&(*r))->setSize(QSize(qRound(r->width() * f[0]), qRound(r->height() * f[1]))); |
|
const_cast<QRect *>(&(*r))->translate(t[0], t[1]); // "const_cast" - don't do that at home, kids ;-) |
|
} |
|
QRect rect = rects.at(0); |
|
if (rects.count() > 1) { |
|
for (r = rects.constBegin() + 1; r != rEnd; ++r) { // unite |
|
rect |= *r; |
|
} |
|
const int dx = 110 * (rect.width() - geo.width()) / 100 + 1 - rect.width() + geo.width(); |
|
const int dy = 110 * (rect.height() - geo.height()) / 100 + 1 - rect.height() + geo.height(); |
|
rect.adjust(-dx, -dy, dx, dy); // fix pot. overshoot |
|
} |
|
*layerRect = rect; |
|
} |
|
} |
|
} |
|
|
|
void AnimationEffect::_windowExpandedGeometryChanged(KWin::EffectWindow *w) |
|
{ |
|
Q_D(AnimationEffect); |
|
AniMap::const_iterator entry = d->m_animations.constFind(w); |
|
if (entry != d->m_animations.constEnd()) { |
|
*const_cast<QRect *>(&(entry->second)) = QRect(); |
|
updateLayerRepaints(); |
|
if (!entry->second.isNull()) { // actually got updated, ie. is in use - ensure it get's a repaint |
|
w->addLayerRepaint(entry->second); |
|
} |
|
} |
|
} |
|
|
|
void AnimationEffect::_windowClosed(EffectWindow *w) |
|
{ |
|
Q_D(AnimationEffect); |
|
|
|
auto it = d->m_animations.find(w); |
|
if (it == d->m_animations.end()) { |
|
return; |
|
} |
|
|
|
QList<AniData> &animations = (*it).first; |
|
for (auto animationIt = animations.begin(); animationIt != animations.end(); ++animationIt) { |
|
if (animationIt->keepAlive) { |
|
animationIt->deletedRef = EffectWindowDeletedRef(w); |
|
} |
|
} |
|
} |
|
|
|
void AnimationEffect::_windowDeleted(EffectWindow *w) |
|
{ |
|
Q_D(AnimationEffect); |
|
d->m_animations.remove(w); |
|
} |
|
|
|
QString AnimationEffect::debug(const QString & /*parameter*/) const |
|
{ |
|
Q_D(const AnimationEffect); |
|
QString dbg; |
|
if (d->m_animations.isEmpty()) { |
|
dbg = QStringLiteral("No window is animated"); |
|
} else { |
|
AniMap::const_iterator entry = d->m_animations.constBegin(), mapEnd = d->m_animations.constEnd(); |
|
for (; entry != mapEnd; ++entry) { |
|
QString caption = entry.key()->isDeleted() ? QStringLiteral("[Deleted]") : entry.key()->caption(); |
|
if (caption.isEmpty()) { |
|
caption = QStringLiteral("[Untitled]"); |
|
} |
|
dbg += QLatin1String("Animating window: ") + caption + QLatin1Char('\n'); |
|
QList<AniData>::const_iterator anim = entry->first.constBegin(), animEnd = entry->first.constEnd(); |
|
for (; anim != animEnd; ++anim) { |
|
dbg += anim->debugInfo(); |
|
} |
|
} |
|
} |
|
return dbg; |
|
} |
|
|
|
AnimationEffect::AniMap AnimationEffect::state() const |
|
{ |
|
Q_D(const AnimationEffect); |
|
return d->m_animations; |
|
} |
|
|
|
} // namespace KWin |
|
|
|
#include "moc_animationeffect.cpp"
|
|
|