Can we do Easing.BezierSpline animation when initial and final values are equal or almost equal?
16:22 26 Dec 2025

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: bad final image: the green arc is straight

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: good final image: the green arc is curved

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
    }
  }
}
qml qtquick2 qt-quick