In a QML script, I want to animate a real-valued property y
according to a cubic bezier spline in time,value space.
The spline is given as an input sequence of [time,value] control points
[[t0,y0], ..., [t1,y1]], where t0 is the time the animation is started, in milliseconds.
If y0 and y1 are substantially distinct, then I can do it using easing.type:Easing.BezierSpline,
after linearly re-mapping both the t values and the y values so that they range from 0 to 1, as required for the easing.bezierCurve property.
I'd create the property value as follows (abusing python list comprehension syntax even though it should be javascript):
normalized_control_points_except_first = [[(t-t0)/(t1-t0), (y-y0)/(y1-y0)] for [t,y] in control_points[1:]]
And then:
NumberAnimation on y {
duration: t1-t0
from: y0
to: y1
easing.type: Easing.BezierSpline
easing.bezierCurve: flattened(normalized_control_points_except_first)
}
But it blows up (division by zero or near-zero) if y1 happens to be the same as y0, or nearly the same.
Is there a way to specify this Animation that will work without any restriction on the y values?
I thought of maybe trying something along the lines of
"find a time t at which the evaluated y value is as far as possible from both y0 and y1,
and split the bezier spline into two sequential pieces at that t".
But that still won't work in the case that all the y's are constant, and it will be unstable
(involving huge values) if the y's are near-constant.
Here's an example qml script that shows the problem.
In this animation, five squares arc from left to right, leaving trails.
In the final picture, the middle (green) one's arc is wrong,
and lots of "QEasingCurve: Invalid bezier curve" errors get emitted to the output terminal window.
WRONG:

We can get (approximately) the desired final picture by fudging the script so that the green square's y1 value is slightly different from its y0.
RIGHT:

Here's the QML script:
import QtQuick
Rectangle {
property real w: 1000 // width of background window
property real h: 500 // height of background window
width: w
height: h
color: "black"
// strange, there seems to be no Array.prototype.flat() in qml6
function flattened(list) {
return [].concat(...list)
}
component MyBezierAnimation : NumberAnimation {
property var control_points; // [[t0,y0], ..., [t1,y1]]
property real _t0: control_points[0][0]
property real _y0: control_points[0][1]
property real _t1: control_points[control_points.length-1][0]
property real _y1: control_points[control_points.length-1][1]
property var _normalized_control_points_except_first:
control_points.slice(1).map(([t,y]) => [(t-_t0)/(_t1-_t0), (y-_y0)/(_y1-_y0)])
from: _y0
to: _y1
duration: _t1-_t0
easing.type: Easing.BezierSpline
easing.bezierCurve: flattened(_normalized_control_points_except_first)
}
// All animated x values vary linearly from left to right side of the picture.
property var x_control_points: [[0,0], [1000,w/3], [2000,w*2/3], [3000,w-25]]
// Highest arcing red square
CenteredRectangleWithTrail {
x: 0; y: h/2
width: 50; height: width
color: Qt.lighter("red")
MyBezierAnimation on x { control_points: x_control_points }
MyBezierAnimation on y { control_points: [[0,h/2], [1000,0], [2000,0], [3000,25]] }
}
// High arcing yellow square
CenteredRectangleWithTrail {
x: 0; y: h/2
width: 50; height: width
color: Qt.lighter("yellow")
MyBezierAnimation on x { control_points: x_control_points }
MyBezierAnimation on y { control_points: [[0,h/2], [1000,0], [2000,0], [3000,h/4]] }
}
// Medium arcing green square
CenteredRectangleWithTrail {
x: 0; y: h/2
width: 50; height: width
color: Qt.lighter("green")
MyBezierAnimation on x { control_points: x_control_points }
MyBezierAnimation on y { control_points: [[0,h/2], [1000,0], [2000,0], [3000,h/2]] }
}
// Low arcing cyan square
CenteredRectangleWithTrail {
x: 0; y: h/2
width: 50; height: width
color: Qt.lighter("cyan")
MyBezierAnimation on x { control_points: x_control_points }
MyBezierAnimation on y { control_points: [[0,h/2], [1000,0], [2000,0], [3000,h*3/4]] }
}
// Lowest arcing blue square
CenteredRectangleWithTrail {
x: 0; y: h/2
width: 50; height: width
color: Qt.lighter("blue")
MyBezierAnimation on x { control_points: x_control_points }
MyBezierAnimation on y { control_points: [[0,h/2], [1000,0], [2000,0], [3000,h-25]] }
}
// A Rectangle with its center (instead of its upper-left corner) at its x,y.
component CenteredRectangle: Item {
property string color
Rectangle {
x: -width/2
y: -height/2
width: parent.width
height: parent.height
color: parent.color
}
}
// A CenteredRectangle that leaves trail particles behind it
// at each new x position.
// (Note that pure vertical motion will leave no trail.)
component CenteredRectangleWithTrail: CenteredRectangle {
onXChanged: {
more_centered_rectangles_list_model.append({x:x, y:y, width:4, height:4, color:color});
}
}
Repeater {
model: ListModel { id: more_centered_rectangles_list_model } // append objects here!
// When you say
// more_centered_rectangles_list_model.append({x:1,y:2,width:3,height=4,color:"red"}),
// one of the following gets added to the root rectangle.
delegate: CenteredRectangle {
x: model.x
y: model.y
width: model.width
height: model.height
color: model.color
}
}
}