It took quite some time for me to figure out a problem I had about rotating a QML object, so I decided to share this.
The question:
How can I dynamically update the rotation of a QML element (like image). I know I can set, e.g. the rotation using a transformation (in the example of a MapQuickItem):
MapQuickItem
{
sourceItem: Image
{
id: image
anchors.centerIn: parent
source: "image.png"
sourceSize.width: Screen.devicePixelRatio * 256
sourceSize.height: Screen.devicePixelRatio * 256
transform: Rotation {
origin { x: image.sourceSize.width/2;
y: image.sourceSize.height/2;
z: 0}
angle: 0
}
}
}
However, how can I update the angle (or other parts of the transform) dynamically?
A cleaner way to do it would be using a property or an alias:
MapQuickItem
{
sourceItem: Image
{
id: image
property alias rotationAngle: rotation.angle
anchors.centerIn: parent
source: "image.png"
sourceSize.width: Screen.devicePixelRatio * 256
sourceSize.height: Screen.devicePixelRatio * 256
transform: Rotation {
id: rotation
origin { x: image.sourceSize.width/2;
y: image.sourceSize.height/2;
z: 0}
angle: 0
}
}
}
with:
function updatePositionAndRotation(angleDeg)
{
image.rotationAngle = angleDeg
}
The key information was that transform is actually a list of transformations.
So one possible solution to the above question is a function like this (e.g. changing the rotation):
function updateRotation(angleDeg)
{
image.transform[0].angle = angleDeg;
}
Related
I have a QML OSM map and a MapQuickItem with Text source item:
MapQuickItem {
property alias rulerRotationAngle: rulerRotation.angle
id: rulerTextMapItem
visible: false
width: 2
height: 2
transform: Rotation {
id: rulerRotation
origin.x: rulerText.width/2;
origin.y: rulerText.height/2;
angle: 0
}
anchorPoint.x: rulerText.width/2
anchorPoint.y: rulerText.height/2
z:5
sourceItem: Text {
id: rulerText; horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: Material.color(Material.Amber, Material.Shade100)
text: "0.0 km";
}
}
I also have two points (QtPositioning.coordinate) and I want the text to rotate depending on the angle of the straight line (MapPolyLine) drawn between those points:
function drawRuler()
{
rulerLine.path = [];
rulerLine.addCoordinate(r_firstpoint);
rulerLine.addCoordinate(r_secondpoint);
rulerTextMapItem.visible = true;
rulerTextMapItem.coordinate = QtPositioning.coordinate((r_firstpoint.latitude+r_secondpoint.latitude)/2, (r_firstpoint.longitude+r_secondpoint.longitude)/2);
var atan = Math.atan2(r_secondpoint.longitude-r_firstpoint.longitude, r_secondpoint.latitude-r_firstpoint.latitude);
var angle = ((atan*180)/Math.PI); //used by another MapItem
var textAngle = angle+270;
if(textAngle>90 & textAngle<270) { textAngle+=180 }
if(angle>90 & angle<270) { angle +=180 }
rulerTextMapItem.rulerRotationAngle = textAngle;
}
However, text rotates correctly only at angles that are multiples of 90 degrees. At an angle of 45 degrees, the text deviates from the mappolyline by about 10-20 degrees.
I have no clue why it happens and appreciate any help.
Tried to move transform.origin of MapQuickItem - angle difference only gets bigger.
Tried to use Math.Atan instead of Math.Atan2 - no difference.
The main issue is this line and the order of inputs:
var atan = Math.atan2(
r_secondpoint.longitude-r_firstpoint.longitude,
r_secondpoint.latitude-r_firstpoint.latitude);
latitude should come before longitude, i.e.
var atan = Math.atan2(
r_secondpoint.latitude-r_firstpoint.latitude,
r_secondpoint.longitude-r_firstpoint.longitude);
Generally speaking, to use Math.atan2() to convert to an angle, you need to use one of the following patterns:
let radians = Math.atan2(vectorY, vectorX)
let degrees = Math.atan2(vectorY, vectorX) * 180 / Math.PI
Also over large angles, you definitely should use project your angular coordinates to a flat projection, e.g. QtPositioning.coordToMercator. (This point was raised in one of the earlier comments).
For very small angles you can get away with it because the earth can be approximated to a flat earth directly from angular coordinates, but, as the area goes, this fact quickly disappears.
The following code demonstrates Math.atan2() and how it must work with (vectorY, vectorX) inputs. It has two draggable squares and you watch that the text will always follow the direction of the blue line no matter where the squares are:
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Shapes
Page {
id: page
width: 200; height: 200
property int startX: rect1.x + rect1.width / 2
property int startY: rect1.y + rect1.height / 2
property int finishX: rect2.x + rect2.width / 2
property int finishY: rect2.y + rect2.height / 2
Rectangle {
id: rect1
x: 40; y: 40
width: 40; height: 40
color: "red"
Drag.active: dragArea.drag.active
Drag.hotSpot.x: 20
Drag.hotSpot.y: 20
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
}
}
Rectangle {
id: rect2
x: 400; y: 250
width: 40; height: 40
color: "red"
Drag.active: dragArea2.drag.active
Drag.hotSpot.x: 20
Drag.hotSpot.y: 20
MouseArea {
id: dragArea2
anchors.fill: parent
drag.target: parent
}
}
Shape {
id: shape
ShapePath {
strokeWidth: 4
strokeColor: "blue"
startX: page.startX
startY: page.startY
PathLine {
x: page.finishX
y: page.finishY
}
}
}
Item {
x: (startX + finishX) / 2
y: (startY + finishY) / 2
rotation: Math.atan2(finishY - startY, finishX - startX) * 180 / Math.PI
Frame {
anchors.centerIn: parent
background: Rectangle {
border.color: "black"
}
Text {
text: "Hello World"
}
}
}
}
You can Try it Online!
I'm very new to QML and I want to make a basic application that consists of a segmented circle with 20-30 segments (pizza slices) and a counter. The number on the counter is the number of segment being highlighted. I found a few ways to make segmented circles in other questions but unfortunately none of them seem to work for my assignment.
The only way I see making it right now is by redrwing all the segments every time the counter is changed, and changing the color of the needed segment. So is there an optimal way to implement this?
To reduce the complexity, let's work through a simplified version of the problem:
Assume there are 6 pieces
Assume we want to draw piece 2
Assume we want to fit it in a 300x300 rectangle
Here's the math:
Each piece will occupy 60 degrees (i.e. 360 / 6)
Piece 2 will occupy angles from 120 to 180
To render the piece the drawing will be:
From the center point (150, 150)
Then (150 + 150 * cos(120), 150 + 150 * sin(120))
Then (150 + 150 * cos(180), 150 + 150 * sin(180))
Then back to the center point (150, 150)
Instead of a straight line, we want to draw a curve line between points 2 and points 3.
To render this, we can use Shape, ShapePath, PathLine, and PathArc.
To generalize, we can replace 6 with 20 and generalize all formulas accordingly. To draw 20 piece slices, we can make use of a Repeater, e.g.
Repeater {
model: 20
PizzaPiece {
piece: index
}
}
To polish it off, I added a Slider so you can interactively change the number of pieces you want from 0-20 and set the color to "orange", otherwise it will be a light yellow "#ffe".
Repeater {
model: 20
PizzaPiece {
piece: index
fillColor: index < slider.value ? "orange" : "#ffe"
}
}
Slider {
id: slider
from: 0
to: 20
stepSize: 1
}
As an extra bonus, I added a TapHandler so that each piece is clickable. If you leave the mouse pressed down, the piece will appear "red" until you release the mouse.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Page {
id: page
property int pieces: 20
Rectangle {
anchors.centerIn: parent
width: 300
height: 300
border.color: "grey"
Repeater {
model: pieces
PizzaPiece {
anchors.fill: parent
anchors.margins: 10
pieces: page.pieces
piece: index
fillColor: pressed ? "red" : index < slider.value ? "orange" : "#ffe"
onClicked: {
slider.value = index + 1;
}
}
}
}
footer: Frame {
RowLayout {
width: parent.width
Label {
text: slider.value
}
Slider {
id: slider
Layout.fillWidth: true
from: 0
to: pieces
value: 3
stepSize: 1
}
}
}
}
//PizzaPiece.qml
import QtQuick
import QtQuick.Shapes
Shape {
id: pizzaPiece
property int pieces: 20
property int piece: 0
property real from: piece * (360 / pieces)
property real to: (piece + 1) * (360 / pieces)
property real centerX: width / 2
property real centerY: height / 2
property alias fillColor: shapePath.fillColor
property alias strokeColor: shapePath.strokeColor
property alias pressed: tapHandler.pressed
property real fromX: centerX + centerX * Math.cos(from * Math.PI / 180)
property real fromY: centerY + centerY * Math.sin(from * Math.PI / 180)
property real toX: centerX + centerX * Math.cos(to * Math.PI / 180)
property real toY: centerY + centerY * Math.sin(to * Math.PI / 180)
signal clicked()
containsMode: Shape.FillContains
ShapePath {
id: shapePath
fillColor: "#ffe"
strokeColor: "grey"
startX: centerX; startY: centerY
PathLine { x: fromX; y: fromY }
PathArc {
radiusX: centerX; radiusY: centerY
x: toX; y: toY
}
PathLine { x: centerX; y: centerY }
}
TapHandler {
id: tapHandler
onTapped: pizzaPiece.clicked()
}
}
You can Try it Online!
Use MapQuickItem to display a component on a map, the quickitem's coordination is not changed, but when the map is modified size( width or height), the quickitem will dispear on a wrong coordination, how to reset quickitem's coordination(latitude, longitude)
Map {
id: map
height: 100 // for example, i change the height, marker's position will not update
width: 100 // but,,, if change, width , will auto update.
MapItemView {
model: xxxx
delegate: MapQuickItem {
id: marker
anchorPoint.x: image.width/4
anchorPoint.y: image.height
coordinate: object.coordinate
sourceItem: Image {
id: image
source: "xxxx.png"
}
}
}
}
i.e the marker does not adjust the position (not coordinate) as map's size changed.
A little late but here's how I did it, because I just had the same problem on macOS and Windows and it may help anyone.
As I am on Qt 5.11, I don't have access to onVisibleRegionChanged signal, so I used onHeightChanged and onWidthChanged signals to trigger a timer when resizing is done. To simulate map movement and trigger refresh, I used pan() function.
Map {
id: map
height: 500
width: 500
onHeightChanged: {
resizeBugTimer.restart();
}
onWidthChanged: {
resizeBugTimer.restart();
}
Timer {
id: resizeBugTimer
interval: 50
repeat: false
running: false
onTriggered: {
map.fixPositionOnResizeBug();
}
}
function fixPositionOnResizeBug() {
pan(1, 1);
pan(-1, -1);
}
MapItemView {
model: xxxx
delegate: MapQuickItem {
id: marker
anchorPoint.x: image.width/4
anchorPoint.y: image.height
coordinate: object.coordinate
sourceItem: Image {
id: image
source: "xxxx.png"
}
}
}
}
Hope this helps.
I am trying to rotate a rectangle from-to a specified angle, but I'm not sure I understand the docs. My code below runs, and my started and completed slots print the correct angles. But the rectangle has not rotated onscreen. What am I missing?
Rectangle {
width: 100
height: 100
RotationAnimation {
id: rotateCritter
duration: 1000
property real lastAngle: 0
onStarted: {
lastAngle = to;
console.log("Rotating from "+from+" to "+to)
}
onStopped: {
console.log("Done rotating from "+from+" to "+to)
from = lastAngle;
}
}
}
// On click totate the critter to the new angle
rotateCritter.to = 45
rotateCritter.start()
Your RotationAnimation is missing a target. Though it is the child of the Rectangle, this relationship does not automatically make the Rectangle the target of the animation; it must be explicit. I have given the Rectangle an id and color, and made this the target of the animation:
Rectangle {
id: critter
width: 100
height: 100
color: "red"
RotationAnimation {
id: rotateCritter
target: critter
duration: 1000
property real lastAngle: 0
onStarted: {
lastAngle = to;
console.log("Rotating from "+from+" to "+to)
}
onStopped: {
console.log("Done rotating from "+from+" to "+to)
from = lastAngle;
}
}
}
Another idea besides using a RotationAnimation object is to just animate on the Rectangle's own rotation property using a Behavior.
Rectangle {
id: rect
width: 100
height: 100
Behavior on rotation {
NumberAnimation {
duration: 1000
}
}
}
rect.rotation = 45 // Animates to 45 degrees
...
rect.rotation = 0 // Animates back to 0 degrees
I want to create some sort of Vocabulary Trainer.
I have a Card QML File what shoud represent some kind of a record card where you can see the Vocabulary. When you've answered, the card should turn around 180° and a new Word/Text should be visible on it.
So far I've created a Rectangle for the Card and a Transformation for the Rotation split up in two PropertyAnimations.
For the sake of simplicity I just want the animation to happen when I'm clicking on the Card. Then the Card turns from 0 to 90 degrees. Afterwards the text should be changed. And at last the Card should turn from -90 to 0 degrees. So I'm looking for a logic that allows me to execute an animation, changes a property (text) instantly and executing another animation as a sequence.
Here is my Code so far:
import QtQuick 2.2
import QtGraphicalEffects 1.0
Item {
Rectangle {
id: card
anchors.fill: parent
border.width: 1
border.color: "grey"
antialiasing: true
Text {
id: question
text: "test test test"
anchors.centerIn: card
}
transform: Rotation {
id: rotation
origin.x: (card.width / 2)
origin.y: (card.height / 2)
axis {
x: 0
y: 1
z: 0
}
angle: 0
}
MouseArea {
anchors.fill: card
onClicked: {
// Code for Turning Card arround
rotate_away.start()
question.text = "abcabcabc"
rotate_new.start()
}
}
PropertyAnimation {
id: rotate_away
target: rotation
properties: "angle"
from: 0
to: 90
duration: 1000
}
PropertyAnimation {
id: rotate_new
target: rotation
properties: "angle"
from: -90
to: 0
duration: 1000
}
}
}
So the problem is this part:
rotate_away.start()
question.text = "abcabcabc"
rotate_new.start()
The text changes but only the 2'nd animation will be executed.
I tried
while (rotate_away.running) {}
to wait for the 1st animation but then the application gets stuck.
I think the animations should be played sequently by using SequentialAnimation. Please revisit your code as follows:
MouseArea {
anchors.fill: card
onClicked: {
// Code for Turning Card around
// rotate_away.start()
// question.text = "abcabcabc"
// rotate_new.start()
fullRotate.start();
}
}
SequentialAnimation {
id: fullRotate
PropertyAnimation {
id: rotate_away
target: rotation
properties: "angle"
from: 0
to: 90
duration: 1000
}
PropertyAction {
target: question
property: "text"
value: "abcabcabc"
}
PropertyAnimation {
id: rotate_new
target: rotation
properties: "angle"
from: -90
to: 0
duration: 1000
}
}
Also, I recommend Flipable which is meant for flipping effects.