Animate ellipses along QPainterPath - qt

I have a bunch of ellipses that initially are lined on top of a path and should move along the QPainterPath. I have it working for the first ellipse but I can't figure out how to get the correct position for the other ellipses.
Is there a way to check if it passed the end of the path and move it back to the beginning?
class Animation : public QAbstractAnimation
{
public:
Animation(const QPainterPath& path, QObject *parent = Q_NULLPTR);
virtual void updateCurrentTime(int ms) override;
virtual int duration() const override;
QPainterPath mPath;
QVector<EllipseGraphicsItem*> mAnimationElements;
};
Animation::Animation (const QPainterPath& path, QObject *parent) : QAbstractAnimation(parent)
, mPath(path)
{
qreal pos = 0;
qreal length = mPath.length();
while (pos < length)
{
qreal percent = path.percentAtLength(pos);
QPointF pointAtPercent = path.pointAtPercent(percent);
pos += 40;
EllipseGraphicsItem * item = new EllipseGraphicsItem(parentItem());
mAnimationElements.append(item);
item->setPos(pointAtPercent);
}
}
void Animation::updateCurrentTime(int ms)
{
QPointF point = mPath.pointAtPercent(qreal(ms) / 6000);
if (mAnimationElements.size() > 0)
mAnimationElements[0]->setPos(point);
for (int i = 0; i < mAnimationElements.size(); i++) {
// how to update each circle's position?
}
}
Start the animation:
QPainterPath path;
path.moveTo(10, 10);
path.lineTo(QPointF(500, 10));
path.lineTo(QPointF(500, 700));
path.lineTo(QPointF(10, 700));
Animation *animation = new Animation(path, this);
animation->setLoopCount(-1);
animation->start();

Imho, it would be easier to use a QGraphicsObject with a QPropertyAnimation:
Use a property that varies between 0 and the length of the path and place your elements by calculating their positions from its value and their position in the list.
A quick example :
class AnimatedEllipses: public QGraphicsObject
{
Q_OBJECT
Q_PROPERTY(int progress READ progress WRITE setProgress)
private:
QGraphicsPathItem path;
QList<QGraphicsEllipseItem*> ellipses;
int propProgress;
public:
int progress() const { return propProgress;}
void setProgress(int value)
{
propProgress = value;
int index = 0;
for (QGraphicsEllipseItem* ellipse: ellipses)
{
// Keep value between 0 and length.
int lgt = (propProgress + index * 40) % int(path.path().length());
qreal percent = path.path().percentAtLength(lgt);
++index;
ellipse->setPos(path.path().pointAtPercent(percent));
}
}
AnimatedEllipses(QPainterPath const& path): QGraphicsObject(), path(path), propProgress(0)
{
qreal pos = 0;
qreal length = path.length();
while (pos < length)
{
qreal percent = path.percentAtLength(pos);
QPointF pointAtPercent = path.pointAtPercent(percent);
pos += 40;
QGraphicsEllipseItem * item = new QGraphicsEllipseItem(-10, -10, 20, 20, this);
item->setPos(pointAtPercent);
ellipses << item;
}
QPropertyAnimation* animation = new QPropertyAnimation(this, "progress");
animation->setStartValue(0);
animation->setEndValue(length);
animation->setDuration(10000);
animation->setLoopCount(-1);
animation->start();
}
// QGraphicsItem interface
public:
QRectF boundingRect() const { return path.boundingRect();}
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget){}
};
The modulo allows you to create an infinte loop for each ellipse.

Related

Change QPropertyAnimation duration on the fly

I'm using this solution to animate ellipses along a QPainterPath.
But I need to slowly increase or decrease the speed of the animation.
I create a timer and set a new duration for the animation but the result is a choppy animation because the ellipses start from the beginning.
Is there a better way to accomplish this?
class AnimatedEllipses: public QGraphicsObject
{
Q_OBJECT
Q_PROPERTY(int progress READ progress WRITE setProgress)
private:
QGraphicsPathItem path;
QList<QGraphicsEllipseItem*> ellipses;
int propProgress;
QPropertyAnimation* animation;
public:
int progress() const { return propProgress;}
void setProgress(int value)
{
propProgress = value;
int index = 0;
for (QGraphicsEllipseItem* ellipse: ellipses)
{
// Keep value between 0 and length.
int lgt = (propProgress + index * 40) % int(path.path().length());
qreal percent = path.path().percentAtLength(lgt);
++index;
ellipse->setPos(path.path().pointAtPercent(percent));
}
}
AnimatedEllipses(QPainterPath const& path): QGraphicsObject(), path(path), propProgress(0)
{
qreal pos = 0;
qreal length = path.length();
while (pos < length)
{
qreal percent = path.percentAtLength(pos);
QPointF pointAtPercent = path.pointAtPercent(percent);
pos += 40;
QGraphicsEllipseItem * item = new QGraphicsEllipseItem(-10, -10, 20, 20, this);
item->setPos(pointAtPercent);
ellipses << item;
}
animation = new QPropertyAnimation(this, "progress");
animation->setStartValue(0);
animation->setEndValue(length);
animation->setDuration(10000);
animation->setLoopCount(-1);
animation->start();
QTimer *timer = new QTimer();
connect(timer, SIGNAL(timeout()), this, SLOT(SlotTimeOut()));
timer->start(1000);
}
void SlotTimeOut() {
int newDuration = GetRandomDuration();
animation->setDuration(newDuration);
}
// QGraphicsItem interface
public:
QRectF boundingRect() const { return path.boundingRect();}
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget){}
};
You can do this with custom QEasingCurve. Here is a small example for progressbar value.
MainWindow.h
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void OnChangeDurationTimer();
private:
Ui::MainWindow *ui;
QPropertyAnimation m_animProgress;
};
Initialization
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
m_animProgress.setTargetObject(ui->progressBar);
m_animProgress.setPropertyName("value");
m_animProgress.setStartValue(0);
m_animProgress.setEndValue(100);
m_animProgress.setDuration(10000);
m_animProgress.setLoopCount(-1);
//setting custom QEasingCurve
QEasingCurve eCurve;
eCurve.setCustomType(myEasingFunction);
m_animProgress.setEasingCurve(eCurve);
m_animProgress.start();
QTimer::singleShot(3000, this, &MainWindow::OnChangeDurationTimer); //timer to change duration
}
Most interesting is function myEasingFunction for custom EasingCurve
qreal g_offset = 0; //value last animation stopped with
qreal g_offsetLast = 0; //keep current value of animation
qreal myEasingFunction(qreal progress)
{
qreal val = g_offset + progress;
while (val > 1) {
val -= 1; //normalize
}
g_offsetLast = val;
return val;
}
And changing duration on timer
void MainWindow::OnChangeDurationTimer()
{
g_offset = g_offsetLast; //remember stopped value
m_animProgress.stop();
m_animProgress.setDuration((rand() % 10 + 1) * 1000);
m_animProgress.start();
QTimer::singleShot(3000, this, &MainWindow::OnChangeDurationTimer); //next changing
}

QT Scene Graph cannot draw a grid

I want to use Qt scene graph to draw a grid. I haven't succeeded in research for a few days. Please help me, thank you!
The issue is:
Why can't I show the results?
Where do I call glViewport? or some other way?
I have followed the code and found that Qt called renderer->setViewportRect(rect) in QQuickWindowPrivate::renderSceneGraph();
But the scene graph uses the entire window as the drawing area instead of the custom QQuickItem object.
I recalculated the shader matrix, but it didn't work. I think it is ugly
source code
// grid_item.h
class GridItem : public QQuickItem
{
Q_OBJECT
public:
explicit GridItem(QQuickItem *parent = nullptr);
protected:
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) Q_DECL_OVERRIDE;
};
// grid_item.cpp
GridItem::GridItem(QQuickItem *parent) : QQuickItem (parent)
{
setFlag(ItemHasContents, true);
}
QSGNode *GridItem::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
QRectF rect = boundingRect();
if (rect.isEmpty()) {
delete oldNode;
return nullptr;
}
QSGGeometryNode *node = nullptr;
QSGGeometry *geometry = nullptr;
GridItemMaterial *material = nullptr;
if(!oldNode)
{
node = new QSGGeometryNode;
node->setFlags(QSGNode::OwnsGeometry | QSGNode::OwnsMaterial, true);
geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0);
geometry->setDrawingMode(QSGGeometry::DrawLines);
node->setGeometry(geometry);
material = new GridItemMaterial;
material->setFlag(QSGMaterial::RequiresDeterminant, true);
node->setMaterial(material);
}
else
{
node = static_cast<QSGGeometryNode *>(oldNode);
geometry = node->geometry();
material = static_cast<GridItemMaterial *>(node->material());
}
int m_xAxisSegment {10};
int m_yAxisSegment {10};
const int totalVertices = (m_xAxisSegment+1)*2 + (m_yAxisSegment+1)*2;
if(geometry->vertexCount() != totalVertices)
{
geometry->allocate(totalVertices);
QSGGeometry::Point2D *vertices = geometry->vertexDataAsPoint2D();
for(int x=0; x<=m_xAxisSegment; x++)
{
float xPos = 1.0f*x/m_xAxisSegment;
(*vertices++).set(xPos, 0.0f);
(*vertices++).set(xPos, 1.0f);
}
for(int y=0; y<=m_yAxisSegment; y++)
{
float yPos = 1.0f*y/m_yAxisSegment;
(*vertices++).set(0.0f, yPos);
(*vertices++).set(1.0f, yPos);
}
node->markDirty(QSGNode::DirtyGeometry);
}
// calculate matrix for shader
ConvertParameter param;
param.windowWidth = 640;
param.windowHeight = 480;
param.contentX = 100;
param.contentY = 100;
param.contentWidth = 200;
param.contentHeight = 200;
param.glX = 0;
param.glY = 0;
param.glWidth = 1.0f;
param.glHeight = 1.0f;
material->m_convertParameter = param;
return node;
}
// grid_item_material.h
class GridItemMaterial : public QSGMaterial
{
public:
QSGMaterialType *type() const Q_DECL_OVERRIDE;
QSGMaterialShader *createShader() const Q_DECL_OVERRIDE;
ConvertParameter m_convertParameter;
};
// grid_item_material.cpp
QSGMaterialType *GridItemMaterial::type() const
{
static QSGMaterialType type;
return &type;
}
QSGMaterialShader *GridItemMaterial::createShader() const
{
return new GridItemMaterialShader;
}
// grid_item_material_shader.h
class GridItemMaterialShader : public QSGMaterialShader
{
public:
GridItemMaterialShader();
const char *const *attributeNames() const Q_DECL_OVERRIDE;
void updateState(const RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) Q_DECL_OVERRIDE;
protected:
void initialize() Q_DECL_OVERRIDE;
QMatrix4x4 getConvertMatrix(const ConvertParameter &param);
private:
int m_id_mvpMatrix {-1};
int m_id_gridlineColor {-1};
};
// grid_item_material_shader.cpp
GridItemMaterialShader::GridItemMaterialShader()
{
setShaderSourceFile(QOpenGLShader::Vertex, ":/shaders/gridlines.vert");
setShaderSourceFile(QOpenGLShader::Fragment, ":/shaders/gridlines.frag");
}
const char * const *GridItemMaterialShader::attributeNames() const
{
static char const *const names[] = { "Vertex", 0 };
return names;
}
void GridItemMaterialShader::updateState(const RenderState &state, QSGMaterial *newMaterial, QSGMaterial *)
{
GridItemMaterial *material = static_cast<GridItemMaterial *>(newMaterial);
QMatrix4x4 matrix = getConvertMatrix(material->m_convertParameter);
program()->setUniformValue(m_id_mvpMatrix, matrix);
program()->setUniformValue(m_id_gridlineColor, QColor::fromRgbF(1, 0, 0, 1));
}
void GridItemMaterialShader::initialize()
{
m_id_mvpMatrix = program()->uniformLocation("mvpMatrix");
m_id_gridlineColor = program()->uniformLocation("gridlineColor");
}
QMatrix4x4 GridItemMaterialShader::getConvertMatrix(const ConvertParameter &param)
{
QMatrix4x4 model1;
// convert window to (-1, -1)..(+1, +1)
model1.setToIdentity();
model1.translate(-1, -1, 0);
model1.scale(2.0f/param.windowWidth, 2.0f/param.windowHeight, 1.0f);
// left-bottom
QVector4D v3(param.contentX, param.windowHeight-param.contentY-param.contentHeight, 0, 1);
v3 = model1 * v3;
// right-top
QVector4D v4(param.contentX+param.contentWidth, param.windowHeight-param.contentY, 0, 1);
v4 = model1 * v4;
// content area should in (-1, -1)..(+1, +1)
float width = v4.x() - v3.x();
float height = v4.y() - v3.y();
QMatrix4x4 model2;
model2.setToIdentity();
model2.translate(v3.x(), v3.y(), 0);
model2.scale(width/param.glWidth, height/param.glHeight, 1);
model2.translate(-param.glX, -param.glY, 0);
return model2;
}
// grid_convert_parameter.h
struct ConvertParameter
{
int windowWidth = 640;
int windowHeight = 480;
int contentX = 100;
int contentY = 100;
int contentWidth = 200;
int contentHeight = 200;
float glX = 3;
float glY = 3;
float glWidth = 4.0f;
float glHeight = 4.0f;
};
// main.cpp
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
qmlRegisterType<GridItem>("io.draw", 1, 0, "GridItem");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
QQuickWindow *window = static_cast<QQuickWindow *>(engine.rootObjects().first());
QSurfaceFormat format = window->requestedFormat();
format.setProfile(QSurfaceFormat::CoreProfile);
format.setVersion(3, 3);
window->setFormat(format);
window->show();
return app.exec();
}
// main.qml
import QtQuick 2.9
import QtQuick.Controls 2.4
import io.draw 1.0
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
GridItem {
x: 100
y: 100
width: 200
height: 200
}
}
// gridlines.vert
#version 330 core
uniform mat4 mvpMatrix;
layout(location = 0) in vec2 Vertex;
void main(void)
{
gl_Position = mvpMatrix * vec4(Vertex, 0.0, 1.0);
}
// gridlines.frag
#version 330 core
uniform vec4 gridlineColor;
layout(location = 0) out vec4 fragColor;
void main(void)
{
fragColor = gridlineColor;
}
I have also made a simple change based on the Qt OpenGL demo.
class OpenGLWindow : public QWindow, protected QOpenGLFunctions_3_3_Core
Almost done the same thing, except that the results are output directly to the entire window (but this is not what I want)
Another difference is the transformation matrix changed:
QMatrix4x4 model, view, projection;
projection.ortho(0, 1, 0, 1, -10, 10);
m_program->setUniformValue(m_matrixUniform, projection*view*model);
It works properly...
Because it involves OpenGL and Qt Scene Graph, I don't know what went wrong.

QPropertyAnimation for QGraphics does not work

So I have looked at other similar questions on stackoverflow but none seem to help. The problem is that the animation doesn't occur, but once I click somewhere in the QGraphicsView, it updates to the end position.
I'm animating QGraphicsRectItem with QPropertyAnimation, so I made a new class and extended QObject and QGraphicsRectItem:
class MyGraphicsRectItem : public QObject, public QGraphicsRectItem {
Q_OBJECT
Q_PROPERTY(QPointF position READ position WRITE setPosition)
Q_PROPERTY(QRectF geometry READ geometry WRITE setGeometry)
public:
...
QPointF position();
void setPosition(QPointF animBox);
QRectF geometry();
void setGeometry(QRectF geo);
...
}
One common problem with regarding this problem is that QPropertyAnimation goes out of scope when the function finishes, but I think I circumvented this problem using QAbstractAnimation::DeleteWhenStopped. In my MainWindow.cpp, I have:
group = new QParallelAnimationGroup;
for (int i = 0; i < 10 ; i += 1) {
MyGraphicsRectItem *temp = dynamic_cast<MyGraphicsRectItem*>(histZToItem[i]);
QPropertyAnimation *anim = new QPropertyAnimation(temp, "position");
anim->setDuration(500);
anim->setStartValue(temp->pos());
QPropertyAnimation *geo = new QPropertyAnimation(temp, "geometry");
geo->setDuration(500);
geo->setStartValue(temp->rect());
geo->setEndValue(QRectF(0, 0, colWidth, -80));
if (i > anchorID) {
anim->setEndValue(QPointF(40 + spaceWidth * (i) + colWidth * (i - 1), graphScene->height() - 40));
} else {
anim->setEndValue(QPointF(40 + spaceWidth * (i + 1) + colWidth * (i), graphScene->height() - 40));
}
group->addAnimation(geo);
group->addAnimation(anim);
}
group->start(QAbstractAnimation::DeleteWhenStopped);
Any ideas?
Edit
Here are my implementations for position, setPosition, geometry and setGeometry:
QPointF MyGraphicsRectItem::position()
{
return QGraphicsRectItem::pos();
}
void MyGraphicsRectItem::setPosition(QPointF animPos)
{
QGraphicsRectItem::setPos(animPos);
}
QRectF MyGraphicsRectItem::geometry()
{
return rect();
}
void MyGraphicsRectItem::setGeometry(QRectF geo)
{
setRect(geo);
}
EDIT 2
Here's the constructor:
MyGraphicsRectItem::MyGraphicsRectItem(qreal x, qreal y, qreal width, qreal height, QGraphicsItem *parent) :QGraphicsRectItem(x, y, width, height, parent) {
setAcceptHoverEvents(true);
}

How to perform swap of QGraphicsItem(s) in QGraphicsView?

My idea in this project is to perform swap animation on items.
Problem is however that when I perform swap on items for the first time they keep their position still, but when the other animation starts that involves already swapped items, those items fall back to their initial positions. Please tell me what am I doing wrong. Animation as follows:
#include <QtCore>
#include <QtWidgets>
/**
* Element to be displayed in QGraphicsView
*/
class QGraphicsRectWidget : public QGraphicsWidget
{
Q_OBJECT
int m_number;
public:
void changePosition(QGraphicsRectWidget *other)
{
setPos(mapToParent(other->x() < x() ? -abs(x() - other->x())
: abs(x() - other->x()) ,0));
}
static int NUMBER;
QGraphicsRectWidget(QGraphicsItem *parent = 0) : QGraphicsWidget(parent), m_number(NUMBER)
{ NUMBER++;}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *,
QWidget *) Q_DECL_OVERRIDE
{
painter->fillRect(rect(), QColor(127, 63, 63));
painter->drawText(rect(), QString("%1").arg(m_number), QTextOption(Qt::AlignCenter));
}
};
int QGraphicsRectWidget::NUMBER = 1;
class MyAnim : public QPropertyAnimation
{
Q_OBJECT
QGraphicsView &pview; // View in which elements must be swapped
int i1, i2; // Indices for elements to be swapped
public:
MyAnim(QGraphicsView &view, int index1 = 0, int index2 = 1, QObject *par = 0)
: QPropertyAnimation(par), pview(view), i1(index1), i2(index2)
{
QObject::connect(this, SIGNAL(finished()), SLOT(slotOnFinish()));
}
public slots:
/* !!!!!!!!!!!!!!!!!!!!!!! HERE IS THE PROBLEM (brobably) !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!*/
// Triggered when animation is over and sets position of target element to position of its end value
void slotOnFinish()
{
auto list = pview.items();
static_cast<QGraphicsRectWidget*>(list.at(i1))
->changePosition(static_cast<QGraphicsRectWidget*>(list.at(i2)));
}
};
class GraphicsView : public QGraphicsView
{
Q_OBJECT
public:
GraphicsView(QGraphicsScene *scene, QWidget *parent = NULL) : QGraphicsView(scene, parent)
{
}
protected:
virtual void resizeEvent(QResizeEvent *event) Q_DECL_OVERRIDE
{
fitInView(scene()->sceneRect());
QGraphicsView::resizeEvent(event);
}
};
#define SWAP_HEIGHT 75
/**
* Creates swap animation for items in QGraphicsView
*/
QParallelAnimationGroup* getSwapAnimation(QGraphicsView &view, int noItem1, int noItem2)
{
auto list = view.items();
QGraphicsRectWidget *wgt1 = static_cast<QGraphicsRectWidget*>(list.at(noItem1));
QGraphicsRectWidget *wgt2 = static_cast<QGraphicsRectWidget*>(list.at(noItem2));
MyAnim *pupperAnim, *plowerAnim;
QParallelAnimationGroup *par = new QParallelAnimationGroup;
plowerAnim = new MyAnim(view, noItem1, noItem2);
plowerAnim->setTargetObject(wgt2);
plowerAnim->setPropertyName("pos");
plowerAnim->setDuration(5000);
plowerAnim->setKeyValueAt(1.0/3.0, QPoint(wgt2->x(), wgt1->y() - SWAP_HEIGHT));
plowerAnim->setKeyValueAt(2.0/3.0, QPoint(wgt1->x(), wgt1->y() - SWAP_HEIGHT));
plowerAnim->setEndValue(wgt1->pos());
pupperAnim = new MyAnim(view, noItem2, noItem1);
pupperAnim->setTargetObject(wgt1);
pupperAnim->setPropertyName("pos");
pupperAnim->setDuration(5000);
pupperAnim->setKeyValueAt(1.0/3.0, QPoint(wgt1->x(), wgt2->y() + SWAP_HEIGHT));
pupperAnim->setKeyValueAt(2.0/3.0, QPoint(wgt2->x(), wgt2->y() + SWAP_HEIGHT));
pupperAnim->setEndValue(wgt2->pos());
par->addAnimation(pupperAnim);
par->addAnimation(plowerAnim);
return par;
}
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QGraphicsRectWidget *button1 = new QGraphicsRectWidget;
QGraphicsRectWidget *button2 = new QGraphicsRectWidget;
QGraphicsRectWidget *button3 = new QGraphicsRectWidget;
QGraphicsRectWidget *button4 = new QGraphicsRectWidget;
button2->setZValue(1);
button3->setZValue(2);
button4->setZValue(3);
QGraphicsScene scene(0, 0, 300, 300);
scene.setBackgroundBrush(QColor(23, 0, 0));
scene.addItem(button1);
scene.addItem(button2);
scene.addItem(button3);
scene.addItem(button4);
GraphicsView window(&scene);
window.setFrameStyle(0);
window.setAlignment(Qt::AlignLeft | Qt::AlignTop);
window.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
window.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
QList<QGraphicsItem*> items = window.items();
QPoint start(20, 125);
for (auto item : items) // Set items in initial position
{
QGraphicsWidget *wgt = static_cast<QGraphicsWidget*>(item);
wgt->resize(50,50);
wgt->moveBy(start.x(), start.y());
start.setX(start.x() + 70);
}
QSequentialAnimationGroup gr;
gr.addAnimation(getSwapAnimation(window, 0, 1));
gr.addAnimation(getSwapAnimation(window, 1, 2));
gr.addAnimation(getSwapAnimation(window, 2, 3));
gr.addAnimation(getSwapAnimation(window, 3, 1));
gr.start();
window.resize(300, 300);
window.show();
return app.exec();
}
#include "main.moc"
UPD: Don't use animation with that purpose
UPD*: Forget previous UPD
Your animation retains the position of the items involved at the time the animation is created. By the time the second animation runs, this information is invalid.
You need to redesign the animation to update its keypoint values at the time it starts. You might also wish to ensure that the animated items run at a constant speed - or at least at a speed you have full control over.
For example:
// https://github.com/KubaO/stackoverflown/tree/master/questions/scene-anim-swap-40787655
#include <QtWidgets>
#include <cmath>
class QGraphicsRectWidget : public QGraphicsWidget
{
public:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
{
painter->fillRect(rect(), Qt::blue);
painter->setPen(Qt::yellow);
painter->drawText(rect(), QString::number(zValue()), QTextOption(Qt::AlignCenter));
}
};
class SwapAnimation : public QPropertyAnimation
{
QPointer<QObject> other;
qreal offset;
QPoint propertyOf(QObject *obj) {
return obj->property(propertyName().constData()).toPoint();
}
void updateState(State newState, State oldState) override {
if (newState == Running && oldState == Stopped) {
auto start = propertyOf(targetObject());
auto end = propertyOf(other);
auto step1 = fabs(offset);
auto step2 = QLineF(start,end).length();
auto steps = 2.0*step1 + step2;
setStartValue(start);
setKeyValueAt(step1/steps, QPoint(start.x(), start.y() + offset));
setKeyValueAt((step1+step2)/steps, QPoint(end.x(), end.y() + offset));
setEndValue(end);
setDuration(10.0 * steps);
}
QPropertyAnimation::updateState(newState, oldState);
}
public:
SwapAnimation(QObject *first, QObject *second, qreal offset) : other(second), offset(offset) {
setTargetObject(first);
setPropertyName("pos");
}
};
QParallelAnimationGroup* getSwapAnimation(QObject *obj1, QObject *obj2)
{
auto const swapHeight = 75.0;
auto par = new QParallelAnimationGroup;
par->addAnimation(new SwapAnimation(obj2, obj1, -swapHeight));
par->addAnimation(new SwapAnimation(obj1, obj2, swapHeight));
return par;
}
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QGraphicsScene scene(0, 0, 300, 300);
QGraphicsRectWidget buttons[4];
int i = 0;
QPointF start(20, 125);
for (auto & button : buttons) {
button.setZValue(i++);
button.resize(50,50);
button.setPos(start);
start.setX(start.x() + 70);
scene.addItem(&button);
}
QSequentialAnimationGroup gr;
gr.addAnimation(getSwapAnimation(&buttons[0], &buttons[1]));
gr.addAnimation(getSwapAnimation(&buttons[1], &buttons[2]));
gr.addAnimation(getSwapAnimation(&buttons[2], &buttons[3]));
gr.addAnimation(getSwapAnimation(&buttons[3], &buttons[1]));
gr.start();
QGraphicsView view(&scene);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.resize(300, 300);
view.show();
return app.exec();
}

Finding the point of intersection between a line and a QPainterPath

I'm trying to determine the point where a hitscan projectile's path (basically a line, but I've represented it as a QPainterPath in my example) intersects with an item in my scene. I am not sure if there is a way to find this point using the functions provided by QPainterPath, QLineF, etc. The code below illustrates what I'm trying to do:
#include <QtWidgets>
bool hit(const QPainterPath &projectilePath, QGraphicsScene *scene, QPointF &hitPos)
{
const QList<QGraphicsItem *> itemsInPath = scene->items(projectilePath, Qt::IntersectsItemBoundingRect);
if (!itemsInPath.isEmpty()) {
const QPointF projectileStartPos = projectilePath.elementAt(0);
float shortestDistance = std::numeric_limits<float>::max();
QGraphicsItem *closest = 0;
foreach (QGraphicsItem *item, itemsInPath) {
QPointF distanceAsPoint = item->pos() - projectileStartPos;
float distance = abs(distanceAsPoint.x() + distanceAsPoint.y());
if (distance < shortestDistance) {
shortestDistance = distance;
closest = item;
}
}
QPainterPath targetShape = closest->mapToScene(closest->shape());
// hitPos = /* the point at which projectilePath hits targetShape */
hitPos = closest->pos(); // incorrect; always gives top left
qDebug() << projectilePath.intersects(targetShape); // true
qDebug() << projectilePath.intersected(targetShape); // QPainterPath: Element count=0
// To show that they do actually intersect..
QPen p1(Qt::green);
p1.setWidth(2);
QPen p2(Qt::blue);
p2.setWidth(2);
scene->addPath(projectilePath, p1);
scene->addPath(targetShape, p2);
return true;
}
return false;
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsView view;
view.setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
QGraphicsScene *scene = new QGraphicsScene;
view.setScene(scene);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
QGraphicsItem *target = scene->addRect(0, 0, 25, 25);
target->setTransformOriginPoint(QPointF(12.5, 12.5));
target->setRotation(35);
target->setPos(100, 100);
QPainterPath projectilePath;
projectilePath.moveTo(200, 200);
projectilePath.lineTo(0, 0);
projectilePath.lineTo(200, 200);
QPointF hitPos;
if (hit(projectilePath, scene, hitPos)) {
scene->addEllipse(hitPos.x() - 2, hitPos.y() - 2, 4, 4, QPen(Qt::red));
}
scene->addPath(projectilePath, QPen(Qt::DashLine));
scene->addText("start")->setPos(180, 150);
scene->addText("end")->setPos(20, 0);
view.show();
return app.exec();
}
projectilePath.intersects(targetShape) returns true, but projectilePath.intersected(targetShape) returns an empty path.
Is there a way to achieve this?
As the answer to Intersection point of QPainterPath and line (find QPainterPath y by x) points out, QPainterPath::intersected() only accounts for paths which have fill areas. The rectangular path trick which is also mentioned there can be implemented like this:
#include <QtWidgets>
/*!
Returns the closest element (position) in \a sourcePath to \a target,
using \l{QPoint::manhattanLength()} to determine the distances.
*/
QPointF closestPointTo(const QPointF &target, const QPainterPath &sourcePath)
{
Q_ASSERT(!sourcePath.isEmpty());
QPointF shortestDistance = sourcePath.elementAt(0) - target;
qreal shortestLength = shortestDistance.manhattanLength();
for (int i = 1; i < sourcePath.elementCount(); ++i) {
const QPointF distance(sourcePath.elementAt(i) - target);
const qreal length = distance.manhattanLength();
if (length < shortestLength) {
shortestDistance = sourcePath.elementAt(i);
shortestLength = length;
}
}
return shortestDistance;
}
/*!
Returns \c true if \a projectilePath intersects with any items in \a scene,
setting \a hitPos to the position of the intersection.
*/
bool hit(const QPainterPath &projectilePath, QGraphicsScene *scene, QPointF &hitPos)
{
const QList<QGraphicsItem *> itemsInPath = scene->items(projectilePath, Qt::IntersectsItemBoundingRect);
if (!itemsInPath.isEmpty()) {
const QPointF projectileStartPos = projectilePath.elementAt(0);
float shortestDistance = std::numeric_limits<float>::max();
QGraphicsItem *closest = 0;
foreach (QGraphicsItem *item, itemsInPath) {
QPointF distanceAsPoint = item->pos() - projectileStartPos;
float distance = abs(distanceAsPoint.x() + distanceAsPoint.y());
if (distance < shortestDistance) {
shortestDistance = distance;
closest = item;
}
}
QPainterPath targetShape = closest->mapToScene(closest->shape());
// QLineF has normalVector(), which is useful for extending our path to a rectangle.
// The path needs to be a rectangle, as QPainterPath::intersected() only accounts
// for intersections between fill areas, which projectilePath doesn't have.
QLineF pathAsLine(projectileStartPos, projectilePath.elementAt(1));
// Extend the first point in the path out by 1 pixel.
QLineF startEdge = pathAsLine.normalVector();
startEdge.setLength(1);
// Swap the points in the line so the normal vector is at the other end of the line.
pathAsLine.setPoints(pathAsLine.p2(), pathAsLine.p1());
QLineF endEdge = pathAsLine.normalVector();
// The end point is currently pointing the wrong way; move it to face the same
// direction as startEdge.
endEdge.setLength(-1);
// Now we can create a rectangle from our edges.
QPainterPath rectPath(startEdge.p1());
rectPath.lineTo(startEdge.p2());
rectPath.lineTo(endEdge.p2());
rectPath.lineTo(endEdge.p1());
rectPath.lineTo(startEdge.p1());
// Visualize the rectangle that we created.
scene->addPath(rectPath, QPen(QBrush(Qt::blue), 2));
// Visualize the intersection of the rectangle with the item.
scene->addPath(targetShape.intersected(rectPath), QPen(QBrush(Qt::cyan), 2));
// The hit position will be the element (point) of the rectangle that is the
// closest to where the projectile was fired from.
hitPos = closestPointTo(projectileStartPos, targetShape.intersected(rectPath));
return true;
}
return false;
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsView view;
QGraphicsScene *scene = new QGraphicsScene;
view.setScene(scene);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
QGraphicsItem *target = scene->addRect(0, 0, 25, 25);
target->setTransformOriginPoint(QPointF(12.5, 12.5));
target->setRotation(35);
target->setPos(100, 100);
QPainterPath projectilePath;
projectilePath.moveTo(200, 200);
projectilePath.lineTo(0, 0);
projectilePath.lineTo(200, 200);
QPointF hitPos;
if (hit(projectilePath, scene, hitPos)) {
scene->addEllipse(hitPos.x() - 2, hitPos.y() - 2, 4, 4, QPen(Qt::red));
}
scene->addPath(projectilePath, QPen(Qt::DashLine));
scene->addText("start")->setPos(180, 150);
scene->addText("end")->setPos(20, 0);
view.show();
return app.exec();
}
This has pretty good precision (± 1 pixel, since QLineF::length() is an integer), but there might be a neater way to achieve the same thing.
Just for the record (and if someone else steps here). The above answer is excellent. There's just a little bug in the closestPoint function that may happens if the first point is already the closest one. It should return elementAt(0) instead of elementAt(0) - target.
Here is the fixed function:
QPointF closestPointTo(const QPointF &target, const QPainterPath &sourcePath)
{
Q_ASSERT(!sourcePath.isEmpty());
QPointF shortestDistance;
qreal shortestLength = std::numeric_limits<int>::max();
for (int i = 0; i < sourcePath.elementCount(); ++i) {
const QPointF distance(sourcePath.elementAt(i) - target);
const qreal length = distance.manhattanLength();
if (length < shortestLength) {
shortestDistance = sourcePath.elementAt(i);
shortestLength = length;
}
}
return shortestDistance;
}

Resources