I use Box2D as phisics engine and QtQuick as visualizer.
I setted up simple scene with falling small rectangles and platform to test collisions
// the platform block
Body {
id: block
bodyType: Body.Kinematic
width:200
height:20
transformOrigin: Body.Center
fixtures: Box {
anchors.fill: parent
friction: 1
density: 1
}
Rectangle {
anchors.fill: parent
color: "yellow"
border.color: "blue"
}
MouseArea {
anchors.fill: parent
onClicked: {
block.rotation += 20
}
}
}
In QML I can set center of rotation:
transformOrigin: Body.Center
By default transformOrigin is top-left corner of object and in this case everything paintet fine. But when i move origin to center in QML it is going wrong as decribed at attached image
This is part of code when it get coords from Box2D and paint QML object
//getting coordination and angle of Box2D object
const b2Vec2 position = mBody->GetPosition();
const float32 angle = mBody->GetAngle();
const qreal newX = position.x * scaleRatio;
const qreal newY = -position.y * scaleRatio;
const qreal newRotation = -(angle * 360.0) / (2 * b2_pi);
// paint QML object at received coords
setX(newX);
setY(newY);
setRotation(newRotation);
The problem is than Box2D rotates object with origin in top-left corner but QML object painted with origin in his center. So QML object is not syncronized with Box2D and falling rectangle painter in wrong place. I maked printscreen but actually box of Box2D is not visible, I added it to understand the problem.
And my question - how can I set origin point in Box2D?
To keep the code simple, and because qml-box2d is an unfinished library, transformOrigin set to TopLeft (the default for a Body) is currently the only supported value.
However, a Box2D body doesn't need to have a size itself. Only the size of its fixtures is relevant. If you set a 0-size on the body its transformOrigin no longer matters. If you then center the fixture on the body, the rotation of the body effectively applies to the center of the fixture.
I've adapted your simple scene using this approach as follows:
// the platform block
Body {
id: block
bodyType: Body.Kinematic
fixtures: Box {
id: boxFixture
anchors.centerIn: parent
width:200
height:20
friction: 1
density: 1
}
Rectangle {
anchors.fill: boxFixture
color: "yellow"
border.color: "blue"
}
MouseArea {
anchors.fill: boxFixture
onClicked: {
block.rotation += 20
}
}
}
Of course, ideally qml-box2d would eventually handle all possible values of transformOrigin regardless of the size of the body.
Related
I am currently working on a project that has a spinning object. The object's rotation speed is changed and its direction of rotation is changed (forwards/backward). After doing some research I came across RotationAnimator on the QT documentation, it seems to have everything I need and I can easily reverse direction. The only problem is that is in QML. My program is entirely in Qt's C++.
I tried an attempt at using QVariantAnimation (documentation here)but every way I have tried there is always some problem. Changing speed is relatively easy, just fooAnimation->setLoop(-1) (loop indefinitely) and change the duration of the animation. The problem I run into is looping forwards AND backward. Or in other terms, changing direction. I have attempted to change the direction of the animation using fooAnimation->setDirection(<Direction goes here>), but if I run the animation in reverse long enough it will stop the animation entirely. It will only loop forwards. That's the key part of the problem.
So I tried alternative ways to change the direction. One idea was changing the endValue from 360 degrees to -360 using fooAnimation->setEndValue(<value goes here>) when I want to reverse, but this has an adverse effect. If I were to swap the direction, the current rotation: let's say it's 110, will be inverted. So now it suddenly jumps to -110 and starts from there. This leads to very jittery rotation as every time a direction swap happens it teleports the rotation of the object.
This seems like a pretty simple thing to implement, rotating an object that can have its rotation speed and direction changed, but I just can't wrap my head around how I could implement it with animations.
A side note here is that all this is being done in Qt 3D so I can grab rotation and other properties from the rotating object (or in this case its QTransform)
There are multiple ways to achieve this effect. On this Qt/QML demo, I used an approach based on NumberAnimation to rotate the cube.
These are the key ideas behind it:
The cube has a Transform component that defines the 4x4 matrix that Qt3D uses to place the cube in the world using predefined rotation and scale values. The NumberAnimation simply changes the rotation angle used on this transform to make the cube rotate on the screen;
The rotation speed is defined by the duration property of that NumberAnimation: a smaller value makes the cube rotate faster. The cube takes 5 seconds to rotate from 0 to 360 degrees;
Changing the rotation speed means recalculating the duration of the animation based on how far away from 0 degrees the current rotation angle is. The animation needs to be restarted whenever we change ANY NumberAnimation properties;
When the direction of the rotation is changed, we store the current rotation angle in cubeTransform.startAngle and update the to and from properties of NumberAnimation along with duration so the cube can be rotated in a new direction starting from the current angle.
I added a few buttons to the UI to change the direction and speed in runtime:
main.qml:
import QtQuick 2.12 as QQ2
import QtQuick.Controls 2.12
import QtQuick.Scene3D 2.12
import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Input 2.0
import Qt3D.Extras 2.0
import Qt3D.Logic 2.0
QQ2.Item {
id: windowId
width: 800
height: 800
property real cubeScale: 100.0
property int rotationSpeed: 5000
property real speedFactor: 1.0
property bool clockwiseRotation: true
Scene3D {
id: scene3D
anchors.fill: parent
aspects: ["input", "logic"]
focus: true
cameraAspectRatioMode: Scene3D.AutomaticAspectRatio
Entity {
id: rootEntity
Camera {
id: camera
projectionType: CameraLens.PerspectiveProjection
fieldOfView: 45
nearPlane : 0.1
farPlane : 100000.0
position: Qt.vector3d(0.0, 0.0, 500.0)
viewCenter: Qt.vector3d(0.0, 0.0, 0.0)
upVector: Qt.vector3d( 0.0, 1.0, 0.0 )
}
components: [
RenderSettings {
activeFrameGraph: ForwardRenderer {
camera: camera
clearColor: "silver"
}
},
InputSettings { }
]
Entity {
id: lightEntity
enabled: true
DirectionalLight {
id: infiniteLight
color: "white"
intensity: 1.0
worldDirection: Qt.vector3d(0, 0, 0)
}
Transform {
id: infiniteLightTransform
translation: Qt.vector3d(0.0, 0.0, 500.0)
}
components: [ infiniteLight, infiniteLightTransform ]
}
Entity {
id: cubeEntity
CuboidMesh {
id: cubeMesh
}
PhongMaterial {
id: cubeMaterial
ambient: "red"
diffuse: Qt.rgba(1.0, 1.0, 1.0, 1.0)
}
Transform {
id: cubeTransform
property real angle: 0.0
property real startAngle: 0.0
matrix: {
var m = Qt.matrix4x4();
m.rotate(cubeTransform.angle, Qt.vector3d(0, 1, 0));
m.translate(Qt.vector3d(0, 0, 0));
m.scale(windowId.cubeScale, windowId.cubeScale, windowId.cubeScale);
return m;
}
}
components: [ cubeMesh, cubeMaterial, cubeTransform ]
}
QQ2.NumberAnimation {
id: cubeAnimation
property int startPos: {
if (cubeTransform.startAngle === 0)
{
if (windowId.clockwiseRotation === true)
return 0;
return 360;
}
return cubeTransform.startAngle;
}
property int endPos: (windowId.clockwiseRotation === true) ? 360 : 0
target: cubeTransform
property: "angle"
duration: windowId.rotationSpeed / windowId.speedFactor
from: cubeAnimation.startPos
to: cubeAnimation.endPos
loops: 1
running: true
onStarted: {
console.log("onStarted");
}
onFinished: {
console.log("onFinished");
// reset the starting angle
cubeTransform.startAngle = 0;
// reset the duration of the animation
cubeAnimation.duration = windowId.rotationSpeed / windowId.speedFactor;
// the animation is currently stopped, run it again
cubeAnimation.running = true;
}
}
}
}
QQ2.Row {
anchors.fill: parent
Button {
text: "Change Direction"
onClicked: {
// pause the animation
cubeAnimation.pause();
// invert rotation
windowId.clockwiseRotation = !windowId.clockwiseRotation;
// store current angle as the starting angle for the rotation
cubeTransform.startAngle = cubeTransform.angle;
// update the remaining time to avoid changing the rotation speed
let progressPercentage = cubeTransform.startAngle / 360.0;
if (windowId.clockwiseRotation)
progressPercentage = 1.0 - progressPercentage;
cubeAnimation.duration = (windowId.rotationSpeed / windowId.speedFactor) * progressPercentage;
// restart the animation from this angle using a new direction
cubeAnimation.restart();
}
}
Button {
text: "1x"
highlighted: (windowId.speedFactor === 1.0)
onClicked: changeSpeed(1.0)
}
Button {
text: "2x"
highlighted: (windowId.speedFactor === 2.0)
onClicked: changeSpeed(2.0)
}
}
function changeSpeed(newSpeed) {
windowId.speedFactor = newSpeed;
// pause the animation
cubeAnimation.pause();
// store current angle as the starting angle for the rotation
cubeTransform.startAngle = cubeTransform.angle;
// update the remaining time to avoid changing the rotation speed
let progressPercentage = cubeTransform.startAngle / 360.0;
if (windowId.clockwiseRotation)
progressPercentage = 1.0 - progressPercentage;
cubeAnimation.duration = (windowId.rotationSpeed / windowId.speedFactor) * progressPercentage;
// restart the animation from this angle using a new direction
cubeAnimation.restart();
}
}
I have Qt Quick Controls 2 Application. In main.qml I have besides other things canvas in scroll view:
Rectangle {
id: graph
width: mainArea.width / 3 - 14;
height: mainArea.height - 20;
ScrollView{
anchors.fill: parent;
Canvas {
id:canvasGraph;
width: graph.width;
height: graph.height;
property bool paintB: false;
property string colorRect: "#FFFF40";
property string name: "ELF header";
property int paintX: 0;
property int paintY: 0;
property int widthP: 160;
property int heightP: 30;
property int textX: (paintX + (widthP / 2)) - 15/*func return int length of text*/;
property int textY: (paintY + (heightP / 2)) + 3;
onPaint:{
if (paintB){
var ctx = canvasGraph.getContext('2d');
ctx.beginPath();
ctx.font = "normal 12px serif";
ctx.fillStyle = colorRect;
ctx.strokeRect(paintX, paintY, widthP, heightP);
ctx.fillRect(paintX, paintY, widthP, heightP);
ctx.strokeText("ELF header", textX, textY);
ctx.closePath();
ctx.save();
}
}
MouseArea{
id: canvasArea;
anchors.fill: parent;
onPressed: {
paint(mouseX,mouseY,"aaa",1);
}
}
}
}
}
At first I tried draw into canvas by js function, here:
function paint(x, y, name, type) {
canvasGraph.paintB = true;
canvasGraph.paintX = x;
canvasGraph.paintY = y;
canvasGraph.requestPaint();
}
This function was called by pressing mouse on canvas. It works good, it draw rectangles, one by one. But only one problem was, that after resizing app window, all rectangles except last one get lost. But it's not primary problem, because it works and this promblem I could resolve later.
For drawing chart I need C++ library (ELFIO, for reading ELF files). So in main.cpp I have two object. First allows me call from main.qml functions of some C++ class. Second allows me calling js functions from C++. Here is main.cpp:
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QScopedPointer<elfFile> elfFileObj(new elfFile);
QQmlApplicationEngine engine;
engine.load(QUrl(QLatin1String("qrc:/main.qml")));
engine.rootContext()->setContextProperty("elfFileObj", elfFileObj.data()); //this is for calling c++ from qml
QObject *rof = engine.rootObjects().first();
elfFileObj.data()->rofS = rof; //this one is for calling js func from c++
return app.exec();
How you can see, reading ELF files is manage from object elfFileObj, where is public variable which holds loaded ELF file and variable rofS which hold object for access to main.qml to js functions.
In elfFileObj is Q_INVOKABLE int loadELF(QString fileName); where Q_INVOKABLE is macro, which ensure, that this function is possible call from qml file. Function:
int elfFile::loadELF(QString fileName)
{
string fileNameReal = (fileName).toStdString().substr(7);
if (!reader.load(fileNameReal.c_str())){
return -1;
}
QVariant x(30);
QVariant y(10);
QVariant name("ELF header");
QVariant type(1);
QMetaObject::invokeMethod(rofS, "paint", Q_ARG(QVariant,x), Q_ARG(QVariant,y), Q_ARG(QVariant,name), Q_ARG(QVariant,type));
y = QVariant(40);
QMetaObject::invokeMethod(rofS, "paint", Q_ARG(QVariant,x), Q_ARG(QVariant,y), Q_ARG(QVariant,name), Q_ARG(QVariant,type));
}
I try draw two rectangles, one by one. QMetaObject::invokeMethod should call js functions, which draw rectangle on (x,y). Other args are in this moment unusable.
Main problem: It draw rectangles on canvas, but after every call by invokeMethod is canvas cleared. So on the canvas always stay only last one rectangle.
Have somebody any idea, how to save actual state of canvas? Thanks for any help.
It isn't pretty code, but it's my first experience with qml.
The canvas, being an imperative drawing API, just has a dumb buffer of pixel data. It has no concept of objects like rectangles or anything else once the draw call has finished. As such, you are responsible for everything that it displays via your onPaint handler. It does not clear the canvas content from one frame to another (as an optimization), but it will (by necessity) clear it when you resize the window, as it has to allocate a differently sized buffer.
You can see this behaviour here:
import QtQuick 2.6
Canvas {
id: canvasGraph;
width: 500
height: 500
property int paintY: 10;
onPaint:{
var ctx = canvasGraph.getContext('2d');
ctx.beginPath();
ctx.font = "normal 12px serif";
ctx.fillStyle = "#ff0000";
ctx.strokeRect(10, paintY, 160, 30);
ctx.fillRect(10, paintY, 160, 30);
ctx.closePath();
ctx.save();
}
Timer {
interval: 16
running: true
repeat: true
onTriggered: {
canvasGraph.requestPaint();
canvasGraph.paintY += 10
if (canvasGraph.paintY > canvasGraph.height)
canvasGraph.paintY = 10
}
}
}
Try run this example with qmlscene and resize the window. You'll notice that all content is cleared on resize, except that one single rectangle that it draws.
So, if you want all your rectangles to be retained, then you need to paint them in the onPaint handler each time (and make use of clearRect or some other method to fill the background to get rid of stuff that doesn't belong there anymore, if you are moving stuff around or making them invisible).
On the other hand, I can't directly explain why invokeMethod would be causing it to clear, as you haven't really presented enough code. It may be that it's resizing the canvas (causing the buffer to reallocate, and be cleared). Either way, given the above, I'd say that it isn't all that relevant.
After all this, while I don't have full background over what you are making, I'd suggest that perhaps Canvas might not be the best tool to do what you want. You might want to look into QQuickPaintedItem instead, or (better still) composing your scene using a custom QQuickItem which positions other QQuickItems (or QSGNodes). Canvas (and QQuickPaintedItem) while easy to use, are not especially performant.
I didn't solved this problem. I just stop using QML and return to use just Qt. It helps me.
How do I move a rectangle component in Qml using c++ program, it has to progress from minimum to maximum value like a progress bar with color gradient. i have tried to use number animation and it is working fine,but how do i change the color as it progresses.
It's hard to give a specific answer as you've not provided much detail or a code sample. However it seems like you want to set the color property of your progress bar rectangle to be dependent on it's width or position, so that it changes depending on how much 'progress' has been made.
In addition, you may be able to use a ColorAnimation class to animate this, along with a Behaviour, for example:
Rectangle {
id: progressBar
width: 0
height: 20
color: (width < 30) ? "red" : (width < 60) ? "yellow" : "green"
Behavior {
ColorAnimation { target: progressBar; duration: 500 }
}
}
I wonder if there is a way to send to some function like utilety.move some variables like (newX, newY) that would represent where to the mouse have moved alike something like such pseudocode
each time event calls function
var coord = - initioal mouse coords + current mouse coords
utilety.move(coord )
initioal mouse coords = current mouse coords
So to controll movment of QML element not using Drag target but own method (probable C++ one)
I don't understand your question clearly, but looks like you want to have mouse cordinate in QML.
if that is case then, you can use MouseArea element, it has mouseX and mouseY property which you can use to locate mouse cursor.
and onPositionChanged handler is called when mouse position is changed.
MouseArea {
anchors.fill: parent
onPositionChanged: {
//var curMouseX = mouseX
//var curMouseY = mouseY
}
}
If you developing for mobile platform you may consider for Qt Quick Gestures plugin:
import Qt.labs.gestures 2.0
GestureArea {
Tap: {
when: gesture.hotspot.x > gesture.hotspot.y
onStarted: console.log("tap in upper right started")
onFinished: console.log("tap in upper right completed")
}
}
Look here for more info: http://labs.qt.nokia.com/2010/10/05/getting-in-touch-with-qt-quick-gestures-and-qml/
I am going to do pan/scale stuff on QGraphicsView.
So I read the documentation of QGraphicsView and see some utility functions like ensureVisible() and centerOn().
I think I understand what the documentation says but I can' t manage to write a working example.
Could you please write/suggest me an example code to understand the issue.
Ton pan the view by a certain amount (for example in your view's mouseMoveEvent()), assuming MyView is a subclass of QGraphicsView (all the following code was ported from Python, I didn't test it):
void MyView::moveBy(QPoint &delta)
{
QScrollBar *horiz_scroll = horizontalScrollBar();
QScrollBar *vert_scroll = verticalScrollBar();
horiz_scroll->setValue(horiz_scroll.value() - delta.x());
vert_scroll->setValue(vert_scroll.value() - delta.y());
}
To fit a rectangle specified in scene coordinates by zooming and panning:
void MyView::fit(QRectF &rect)
{
setSceneRect(rect);
fitInView(rect, Qt::KeepAspectRatio);
}
Note that if your scene contains non transformable items (with the QGraphicsItem::ItemIgnoresTransformations flag set), you'll have to take extra steps to compute their correct bounding box:
/**
* Compute the bounding box of an item in scene space, handling non
* transformable items.
*/
QRectF sceneBbox(QGraphicsItem *item, QGraphicsItemView *view=NULL)
{
QRectF bbox = item->boundingRect();
QTransform vp_trans, item_to_vp_trans;
if (!(item->flags() & QGraphicsItem::ItemIgnoresTransformations)) {
// Normal item, simply map its bounding box to scene space
bbox = item->mapRectToScene(bbox);
} else {
// Item with the ItemIgnoresTransformations flag, need to compute its
// bounding box with deviceTransform()
if (view) {
vp_trans = view->viewportTransform();
} else {
vp_trans = QTransform();
}
item_to_vp_trans = item->deviceTransform(vp_trans);
// Map bbox to viewport space
bbox = item_to_vp_trans.mapRect(bbox);
// Map bbox back to scene space
bbox = vp_trans.inverted().mapRect(bbox);
}
return bbox;
}
In that case the bounding rect of your objects becomes dependent on the view's zoom level, meaning that sometimes MyView::fit() won't fit exactly your objects (for example when fitting a selection of objects from a largely zoomed out view). A quick and dirty solution is to call MyView::fit() repeatedly until the bounding rect naturally "stabilizes" itself.