初始与最终值相等时,如何实现QML的Easing.BezierSpline动画?
解决QML中三次贝塞尔样条动画无y值限制的实现方案
问题根源
当动画起始y值y0与结束y值y1相等或接近时,线性归一化y值到0-1范围会触发除零/近零除法错误,导致QEasingCurve: Invalid bezier curve异常,进而使动画失效或表现异常。
无y值限制的实现方案
方案1:手动实现三次贝塞尔插值
绕过QML内置的Easing.BezierSpline,直接基于原始控制点序列计算y值,完全避免归一化操作:
- 将原始的
1+3*n个控制点拆分为n个三次样段,每个样段对应4个连续控制点[t0,y0], [t1,y1], [t2,y2], [t3,y3] - 利用三次贝塞尔插值公式,在动画进度变化时实时计算目标y值:
公式:B(s) = (1-s)³*y0 + 3*(1-s)²*s*y1 + 3*(1-s)*s²*y2 + s³*y3
其中s是当前样段内的归一化时间(0到1),由全局时间映射得到 - QML示例代码:
PropertyAnimation { target: targetItem property: "y" duration: controlPoints[controlPoints.length-1][0] - controlPoints[0][0] onProgressChanged: { const globalTime = controlPoints[0][0] + progress * duration; // 定位当前时间所在的样段 let segIdx = 0; while (segIdx < n && controlPoints[3*(segIdx+1)][0] < globalTime) { segIdx++; } const seg = controlPoints.slice(3*segIdx, 3*segIdx+4); const segStartT = seg[0][0]; const segEndT = seg[3][0]; // 计算样段内的归一化进度s const s = (globalTime - segStartT) / (segEndT - segStartT); // 插值计算y值 const s2 = s * s; const s3 = s2 * s; const invS = 1 - s; const invS2 = invS * invS; const invS3 = invS2 * invS; targetItem.y = invS3*seg[0][1] + 3*invS2*s*seg[1][1] + 3*invS*s2*seg[2][1] + s3*seg[3][1]; } }
- 优势:支持任意y值(包括全程恒定),兼容非单调时间序列,无归一化相关错误
方案2:带容错的控制点归一化
若想保留QML内置动画的性能优势,可对y值归一化逻辑添加容错处理:
- 设定一个极小阈值(如
1e-6),当|y1 - y0|小于阈值时,将所有y控制点映射为固定值(如0),避免除零 - 否则正常执行线性归一化:
normalizedY = (y - y0)/(y1 - y0) - QML示例代码:
function getNormalizedBezier(points) { const y0 = points[0][1]; const y1 = points[points.length-1][1]; const yDiff = y1 - y0; const normPoints = []; const totalTDiff = points[points.length-1][0] - points[0][0]; for (const p of points) { const normT = (p[0] - points[0][0]) / totalTDiff; const normY = Math.abs(yDiff) < 1e-6 ? 0 : (p[1] - y0) / yDiff; normPoints.push([normT, normY]); } return normPoints; } PropertyAnimation { target: targetItem property: "y" from: controlPoints[0][1] to: controlPoints[controlPoints.length-1][1] duration: controlPoints[controlPoints.length-1][0] - controlPoints[0][0] easing.type: Easing.BezierSpline easing.bezierCurve: getNormalizedBezier(controlPoints) }
- 优势:代码简洁,复用内置动画系统,适合时间序列单调的场景
方案选择建议
- 若需支持非单调时间序列或极端y值场景,优先选方案1
- 若场景中时间序列始终单调,且仅需处理偶尔的y值恒定情况,方案2更高效
内容的提问来源于stack exchange,提问作者Don Hatch
相关产品推荐
相关产品推荐

