profile: redo animation of labels and grid lines

The old animation was weird: it would reuse the labels
based on the index, not on the value. Thus, with the
new scaling code, sometimes there was no animation at all,
if the value, but not the position changed.

Consider the values instead and let labels appear/disappear.
This makes things slightly more complex.

While changing this code, create our own animation-class.
Thus, we can avoid having the dive axes being QObjects.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
This commit is contained in:
Berthold Stoeger 2021-12-02 23:53:17 +01:00 committed by Dirk Hohndel
parent afb0978460
commit 5c5c0c4880
4 changed files with 216 additions and 106 deletions

View file

@ -70,15 +70,6 @@ void DiveCartesianAxis::setTransform(double a, double b)
transform.b = b;
}
template <typename T>
void emptyList(QList<T *> &list, int steps, int speed)
{
while (list.size() > steps) {
T *removedItem = list.takeLast();
Animations::animDelete(removedItem, speed);
}
}
double DiveCartesianAxis::width() const
{
return labelWidth + labelSpaceHorizontal * dpr;
@ -151,6 +142,10 @@ void DiveCartesianAxis::updateTicks(int animSpeed)
if (!textVisibility && !lineVisibility)
return; // Nothing to display...
// Remember the old range for animations.
double dataMaxOld = dataMax;
double dataMinOld = dataMin;
// Guess the number of tick marks.
QLineF m = line();
double spaceNeeded = position == Position::Bottom ? labelWidth * 3.0 / 2.0
@ -188,8 +183,6 @@ void DiveCartesianAxis::updateTicks(int animSpeed)
numTicks = lrint((lastDisplay - firstDisplay) / intervalDisplay) + 1;
numTicks = std::max(numTicks, 0);
emptyList(labels, numTicks, animSpeed);
emptyList(lines, numTicks, animSpeed);
if (numTicks == 0)
return;
@ -209,100 +202,151 @@ void DiveCartesianAxis::updateTicks(int animSpeed)
(inverted ? m.x2() - offsetScreen : m.x1() + offsetScreen) :
(inverted ? m.y1() + offsetScreen : m.y2() - offsetScreen);
if (textVisibility)
updateLabels(numTicks, firstPosScreen, firstValue, stepScreen, stepValue, animSpeed);
if (lineVisibility)
updateLines(numTicks, firstPosScreen, stepScreen, animSpeed);
updateLabels(numTicks, firstPosScreen, firstValue, stepScreen, stepValue, animSpeed, dataMinOld, dataMaxOld);
}
void DiveCartesianAxis::updateLabels(int numTicks, double firstPosScreen, double firstValue, double stepScreen, double stepValue, int animSpeed)
{
for (int i = 0, count = labels.size(); i < count; i++, firstValue += stepValue) {
double childPos = ((position == Position::Bottom) != inverted) ?
firstPosScreen + i * stepScreen :
firstPosScreen - i * stepScreen;
labels[i]->set(textForValue(firstValue), textColor);
switch (position) {
default:
case Position::Bottom:
Animations::moveTo(labels[i], animSpeed, childPos, rect.bottom() + labelSpaceVertical * dpr);
break;
case Position::Left:
Animations::moveTo(labels[i], animSpeed, rect.left() - labelSpaceHorizontal * dpr, childPos);
break;
case Position::Right:
Animations::moveTo(labels[i], animSpeed, rect.right() + labelSpaceHorizontal * dpr, childPos);
break;
}
}
// Add the rest of the needed labels.
for (int i = labels.size(); i < numTicks; i++, firstValue += stepValue) {
double childPos = ((position == Position::Bottom) != inverted) ?
firstPosScreen + i * stepScreen :
firstPosScreen - i * stepScreen;
QPointF DiveCartesianAxis::labelPos(double pos) const
{
return position == Position::Bottom ? QPointF(pos, rect.bottom() + labelSpaceVertical * dpr) :
position == Position::Left ? QPointF(rect.left() - labelSpaceHorizontal * dpr, pos) :
QPointF(rect.right() + labelSpaceHorizontal * dpr, pos);
}
QLineF DiveCartesianAxis::linePos(double pos) const
{
return position == Position::Bottom ? QLineF(pos, rect.top(), pos, rect.bottom()) :
QLineF(rect.left(), pos, rect.right(), pos);
}
void DiveCartesianAxis::updateLabel(Label &label, double opacityEnd, double pos) const
{
label.opacityStart = textVisibility ? label.label->opacity()
: label.line->opacity();
label.opacityEnd = opacityEnd;
if (textVisibility) {
label.labelPosStart = label.label->pos();
label.labelPosEnd = labelPos(pos);
}
if (lineVisibility) {
label.lineStart = label.line->line();
label.lineEnd = linePos(pos);
}
}
DiveCartesianAxis::Label DiveCartesianAxis::createLabel(double value, double pos, double dataMinOld, double dataMaxOld, int animSpeed)
{
Label label { value, 0.0, 1.0 };
double posStart = posAtValue(value, dataMaxOld, dataMinOld);
if (textVisibility) {
label.labelPosStart = labelPos(posStart);
label.labelPosEnd = labelPos(pos);
int alignFlags = position == Position::Bottom ? Qt::AlignTop | Qt::AlignHCenter :
position == Position::Left ? Qt::AlignVCenter | Qt::AlignLeft:
Qt::AlignVCenter | Qt::AlignRight;
DiveTextItem *label = new DiveTextItem(dpr, labelScale, alignFlags, this);
label->set(textForValue(firstValue), textColor);
label->setZValue(1);
labels.push_back(label);
switch (position) {
default:
case Position::Bottom:
label->setPos(scene.sceneRect().width() + 10, rect.bottom() + labelSpaceVertical * dpr); // position it outside of the scene;
Animations::moveTo(labels[i], animSpeed, childPos, rect.bottom() + labelSpaceVertical * dpr);
break;
case Position::Left:
label->setPos(rect.left() - labelSpaceHorizontal * dpr, scene.sceneRect().height() + 10);
Animations::moveTo(labels[i], animSpeed, rect.left() - labelSpaceHorizontal * dpr, childPos);
break;
case Position::Right:
label->setPos(rect.right() + labelSpaceHorizontal * dpr, scene.sceneRect().height() + 10);
Animations::moveTo(labels[i], animSpeed, rect.right() + labelSpaceHorizontal * dpr, childPos);
break;
}
label.label = std::make_unique<DiveTextItem>(dpr, labelScale, alignFlags, this);
label.label->set(textForValue(value), textColor);
label.label->setZValue(1);
label.label->setPos(animSpeed <= 0 ? label.labelPosEnd : label.labelPosStart);
label.label->setOpacity(animSpeed <= 0 ? 1.0 : 0.0);
}
if (lineVisibility) {
label.lineStart = linePos(posStart);
label.lineEnd = linePos(pos);
label.line = std::make_unique<DiveLineItem>(this);
label.line->setPen(gridPen);
label.line->setZValue(0);
label.line->setLine(animSpeed <= 0 ? label.lineEnd : label.lineStart);
label.line->setOpacity(animSpeed <= 0 ? 1.0 : 0.0);
}
return label;
}
void DiveCartesianAxis::updateLines(int numTicks, double firstPosScreen, double stepScreen, int animSpeed)
void DiveCartesianAxis::updateLabels(int numTicks, double firstPosScreen, double firstValue, double stepScreen, double stepValue,
int animSpeed, double dataMinOld, double dataMaxOld)
{
for (int i = 0, count = lines.size(); i < count; i++) {
double childPos = ((position == Position::Bottom) != inverted) ?
firstPosScreen + i * stepScreen :
firstPosScreen - i * stepScreen;
if (animSpeed <= 0)
labels.clear(); // No animation? Simply redo the labels.
if (position == Position::Bottom) {
// Fix size in case the scene changed
QLineF old = lines[i]->line();
lines[i]->setLine(old.x1(), old.y1(), old.x1(), old.y1() + rect.height());
Animations::moveTo(lines[i], animSpeed, childPos, rect.top());
std::vector<Label> newLabels;
newLabels.reserve(numTicks);
auto actOld = labels.begin();
double value = firstValue;
for (int i = 0; i < numTicks; i++, value += stepValue) {
// Check if we already got that label. If we find unused labels, mark them for deletion.
// Labels to be deleted are recognized by an end-opacity of 0.0.
// Note: floating point comparisons should be fine owing to our rounding to integers above.
for ( ; actOld != labels.end() && actOld->value < value; ++actOld) {
double pos = posAtValue(actOld->value);
updateLabel(*actOld, 0.0, pos);
newLabels.push_back(std::move(*actOld));
}
double pos = ((position == Position::Bottom) != inverted) ?
firstPosScreen + i * stepScreen :
firstPosScreen - i * stepScreen;
if (actOld != labels.end() && actOld->value == value) {
// Update label, but don't delete it
updateLabel(*actOld, 1.0, pos);
newLabels.push_back(std::move(*actOld));
++actOld;
} else {
// Fix size in case the scene changed
QLineF old = lines[i]->line();
lines[i]->setLine(old.x1(), old.y1(), old.x1() + rect.width(), old.y1());
Animations::moveTo(lines[i], animSpeed, rect.left(), childPos);
// Create new label
newLabels.push_back(createLabel(value, pos, dataMinOld, dataMaxOld, animSpeed));
}
}
// Add the rest of the needed grid lines.
for (int i = lines.size(); i < numTicks; i++) {
double childPos = ((position == Position::Bottom) != inverted) ?
firstPosScreen + i * stepScreen :
firstPosScreen - i * stepScreen;
DiveLineItem *line = new DiveLineItem(this);
line->setPen(gridPen);
line->setZValue(0);
lines.push_back(line);
if (position == Position::Bottom) {
line->setLine(0.0, 0.0, 0.0, rect.height());
line->setPos(scene.sceneRect().width() + 10, rect.top()); // position it outside of the scene);
Animations::moveTo(line, animSpeed, childPos, rect.top());
} else {
line->setLine(0.0, 0.0, rect.width(), 0.0);
line->setPos(rect.left(), scene.sceneRect().height() + 10);
Animations::moveTo(line, animSpeed, rect.left(), childPos);
// If there are any labels left, mark them for deletion.
for ( ; actOld != labels.end(); ++actOld) {
double pos = posAtValue(actOld->value);
updateLabel(*actOld, 0.0, pos);
newLabels.push_back(std::move(*actOld));
}
labels = std::move(newLabels);
}
// Arithmetics with lines. Needed for animations. Operates pointwise.
static QLineF operator-(const QLineF &l1, const QLineF &l2)
{
return QLineF(l1.p1() - l2.p1(), l1.p2() - l2.p2());
}
static QLineF operator+(const QLineF &l1, const QLineF &l2)
{
return QLineF(l1.p1() + l2.p1(), l1.p2() + l2.p2());
}
static QLineF operator*(double f, const QLineF &l)
{
return QLineF(f*l.p1(), f*l.p2());
}
// Helper template: get point in interval (0.0: start, 1.0: end)
template <typename T>
T mid(const T &start, const T &end, double fraction)
{
return start + fraction * (end - start);
}
void DiveCartesianAxis::anim(double fraction)
{
if (fraction == 1.0) {
// The animation has finished.
// Remove labels that have been marked for deletion by setting the opacity to 0.0.
// Use the erase-remove idiom (yes, it is a weird idiom).
labels.erase(std::remove_if(labels.begin(), labels.end(),
[](const Label &l) { return l.opacityEnd == 0.0; }),
labels.end());
}
for (Label &label: labels) {
double opacity = mid(label.opacityStart, label.opacityEnd, fraction);
if (label.label) {
label.label->setOpacity(opacity);
label.label->setPos(mid(label.labelPosStart, label.labelPosEnd, fraction));
}
if (label.line) {
label.line->setOpacity(opacity);
label.line->setLine(mid(label.lineStart, label.lineEnd, fraction));
}
}
}
@ -347,7 +391,7 @@ QString DiveCartesianAxis::textForValue(double value) const
}
}
qreal DiveCartesianAxis::valueAt(const QPointF &p) const
double DiveCartesianAxis::valueAt(const QPointF &p) const
{
QLineF m = line();
QPointF relativePosition = p;
@ -362,7 +406,7 @@ qreal DiveCartesianAxis::valueAt(const QPointF &p) const
return fraction * (max - min) + min;
}
qreal DiveCartesianAxis::posAtValue(qreal value) const
double DiveCartesianAxis::posAtValue(double value, double max, double min) const
{
QLineF m = line();
@ -376,6 +420,11 @@ qreal DiveCartesianAxis::posAtValue(qreal value) const
(screenTo - screenFrom) + screenFrom;
}
double DiveCartesianAxis::posAtValue(double value) const
{
return posAtValue(value, max, min);
}
static std::pair<double, double> getLineFromTo(const QLineF &l, bool horizontal)
{
if (horizontal)

View file

@ -2,24 +2,18 @@
#ifndef DIVECARTESIANAXIS_H
#define DIVECARTESIANAXIS_H
#include <QObject>
#include <memory>
#include <QGraphicsLineItem>
#include <QPen>
#include "core/color.h"
#include "core/units.h"
class ProfileScene;
class QPropertyAnimation;
class DiveTextItem;
class DiveLineItem;
class DivePlotDataModel;
class DiveCartesianAxis : public QObject, public QGraphicsLineItem {
Q_OBJECT
Q_PROPERTY(QLineF line WRITE setLine READ line)
Q_PROPERTY(QPointF pos WRITE setPos READ pos)
Q_PROPERTY(qreal x WRITE setX READ x)
Q_PROPERTY(qreal y WRITE setY READ y)
class DiveCartesianAxis : public QGraphicsLineItem {
private:
bool printMode;
public:
@ -36,8 +30,8 @@ public:
double minimum() const;
double maximum() const;
std::pair<double, double> screenMinMax() const;
qreal valueAt(const QPointF &p) const;
qreal posAtValue(qreal value) const;
double valueAt(const QPointF &p) const;
double posAtValue(double value) const;
void setPosition(const QRectF &rect);
double screenPosition(double pos) const; // 0.0 = begin, 1.0 = end of axis, independent of represented values
double pointInRange(double pos) const; // Point on screen is in range of axis
@ -47,11 +41,23 @@ public:
double width() const; // only for vertical axes
double height() const; // only for horizontal axes
double horizontalOverhang() const; // space needed for labels of horizontal axes
void anim(double fraction);
// The minimum space between two labels on the plot in seconds
int getMinLabelDistance(const DiveCartesianAxis &timeAxis) const;
private:
struct Label {
double value;
double opacityStart;
double opacityEnd; // If 0.0, label will be removed at end of animation
QPointF labelPosStart;
QPointF labelPosEnd;
QLineF lineStart;
QLineF lineEnd;
std::unique_ptr<DiveTextItem> label;
std::unique_ptr<DiveLineItem> line;
};
Position position;
bool inverted; // Top-to-bottom or right-to-left axis.
int fractionalDigits;
@ -59,9 +65,13 @@ private:
QPen gridPen;
QColor textColor;
ProfileScene &scene;
double posAtValue(double value, double max, double min) const;
QPointF labelPos(double pos) const;
QLineF linePos(double pos) const;
void updateLabel(Label &label, double opacityEnd, double pos) const;
Label createLabel(double value, double pos, double dataMinOld, double dataMaxOld, int animSpeed);
QString textForValue(double value) const;
QList<DiveTextItem *> labels;
QList<DiveLineItem *> lines;
std::vector<Label> labels;
double dataMin, dataMax;
double min, max;
bool textVisibility;
@ -80,8 +90,8 @@ private:
double from(double y) const;
} transform;
void updateLabels(int numTicks, double firstPosScreen, double firstValue, double stepScreen, double stepValue, int animSpeed);
void updateLines(int numTicks, double firstPosScreen, double stepScreen, int animSpeed);
void updateLabels(int numTicks, double firstPosScreen, double firstValue, double stepScreen, double stepValue,
int animSpeed, double dataMinOld, double dataMaxOld);
};
#endif // DIVECARTESIANAXIS_H

View file

@ -16,9 +16,37 @@
#include "core/settings/qPrefDisplay.h"
#include "qt-models/diveplotdatamodel.h"
#include "qt-models/diveplannermodel.h"
#include <QAbstractAnimation>
static const double diveComputerTextBorder = 1.0;
// Class for animations (if any). Might want to do our own.
class ProfileAnimation : public QAbstractAnimation {
ProfileScene &scene;
// For historical reasons, speed is actually the duration
// (i.e. the reciprocal of speed). Ouch, that hurts.
int speed;
int duration() const override
{
return speed;
}
void updateCurrentTime(int time) override
{
// Note: we explicitly pass 1.0 at the end, so that
// the callee can do a simple float comparison for "end".
scene.anim(time == speed ? 1.0
: static_cast<double>(time) / speed);
}
public:
ProfileAnimation(ProfileScene &scene, int animSpeed) :
scene(scene),
speed(animSpeed)
{
start();
}
};
template<typename T, class... Args>
T *ProfileScene::createItem(const DiveCartesianAxis &vAxis, int vColumn, int z, Args&&... args)
{
@ -326,6 +354,7 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM
{
d = dIn;
dc = dcIn;
animatedAxes.clear();
if (!d) {
clear();
return;
@ -397,6 +426,7 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM
// each item, I'll mostly like to fix this in the future, but I'll keep at this for now.
profileYAxis->setBounds(0.0, maxdepth);
profileYAxis->updateTicks(animSpeed);
animatedAxes.push_back(profileYAxis);
temperatureAxis->setBounds(plotInfo.mintemp,
plotInfo.maxtemp - plotInfo.mintemp > 2000 ? plotInfo.maxtemp : plotInfo.mintemp + 2000);
@ -404,11 +434,13 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM
if (hasHeartBeat) {
heartBeatAxis->setBounds(plotInfo.minhr, plotInfo.maxhr);
heartBeatAxis->updateTicks(animSpeed);
animatedAxes.push_back(heartBeatAxis);
}
percentageAxis->setBounds(0, 100);
percentageAxis->setVisible(false);
percentageAxis->updateTicks(animSpeed);
animatedAxes.push_back(percentageAxis);
if (calcMax) {
double relStart = (1.0 - 1.0/zoom) * zoomedPosition;
@ -433,6 +465,7 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM
int to = it2 - plotInfo.entry;
timeAxis->updateTicks(animSpeed);
animatedAxes.push_back(timeAxis);
cylinderPressureAxis->setBounds(plotInfo.minpressure, plotInfo.maxpressure);
tankItem->setData(d, firstSecond, lastSecond);
@ -445,6 +478,7 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM
gasYAxis->setBounds(0.0, max);
gasYAxis->updateTicks(animSpeed);
animatedAxes.push_back(gasYAxis);
}
// Replot dive items
@ -499,6 +533,18 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM
dcText += tr(" (#%1 of %2)").arg(dc + 1).arg(nr);
#endif
diveComputerText->set(dcText, getColor(TIME_TEXT, isGrayscale));
// Reset animation.
if (animSpeed <= 0)
animation.reset();
else
animation = std::make_unique<ProfileAnimation>(*this, animSpeed);
}
void ProfileScene::anim(double fraction)
{
for (DiveCartesianAxis *axis: animatedAxes)
axis->anim(fraction);
}
void ProfileScene::draw(QPainter *painter, const QRect &pos,

View file

@ -29,6 +29,7 @@ class DiveReportedCeiling;
class DiveTemperatureItem;
class DiveTextItem;
class PartialPressureGasItem;
class ProfileAnimation;
class TankItem;
class ProfileScene : public QGraphicsScene {
@ -39,6 +40,8 @@ public:
void resize(QSizeF size);
void clear();
bool pointOnProfile(const QPointF &point) const;
void anim(double fraction); // Called by the animation with 0.0-1.0 (start to stop).
// Can be compared with literal 1.0 to determine "end" state.
// If a plannerModel is passed, the deco-information is taken from there.
void plotDive(const struct dive *d, int dc, DivePlannerPointsModel *plannerModel = nullptr, bool inPlanner = false,
@ -96,6 +99,8 @@ private:
DivePercentageItem *percentageItem;
TankItem *tankItem;
std::shared_ptr<const DivePixmaps> pixmaps;
std::unique_ptr<ProfileAnimation> animation;
std::vector<DiveCartesianAxis *> animatedAxes;
};
#endif