MuseScore/src/engraving/libmscore/easeInOut.cpp

124 lines
5.2 KiB
C++

/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2021 MuseScore BVBA and others
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
\file
Implementation of class EaseInOut for implementing transfer curve with
parametrerable ease-In and ease-Out.
*/
#include "easeInOut.h"
#include <cmath>
using namespace mu;
namespace Ms {
//-------------------------------------------------------------------------------------------------
// The following function is inspired by "A Primer on Bézier Curve" sections 17 and 23 by Pomax:
// https://pomax.github.io/bezierinfo/
// However, the function is greatly specialized, simplified and optimized for use as an ease-in and
// ease-out transfer curve for bends, glissandi and portamenti in MuseScore. The function computes
// Y from X by performing a cubic root finding to compute t from X on the X component of the
// transfer curve and then a cubic Bezier evaluation to compute Y from t on the Y component of the
// transfer curve. The code could be simplified because the Bezier curve is constrained as a well
// defined transfer function. It is not suitable for general or arbitrary Bezier curve work. -YP
//-------------------------------------------------------------------------------------------------
qreal EaseInOut::tFromX(const qreal x) const
{
const qreal pi = 3.14159265358979323846;
qreal w1 = _easeIn - x;
qreal w2 = 1.0 - _easeOut - x;
qreal d = x + 3.0 * w1 - 3.0 * w2 + (1.0 - x);
qreal a = (-3.0 * x - 6.0 * w1 + 3 * w2) / d;
qreal b = (3.0 * x + 3.0 * w1) / d;
qreal c = -x / d;
qreal a2 = a * a;
qreal p = (3.0 * b - a2) / 3.0;
qreal q = (2.0 * a2 * a - 9.0 * a * b + 27.0 * c) / 27.0;
qreal discr = (q * q) / 4.0 + (p * p * p) / 27.0;
qreal t = 0.0;
// Crazy idea to first test the least probable case with such an expensive test but...
if (qFuzzyIsNull(discr)) {
// Case that happens extremely rarely --> 2 roots.
qreal q2 = q / 2.0;
qreal u = q2 < 0.0 ? std::pow(-q2, 1.0 / 3.0) : -std::pow(q2, 1.0 / 3.0);
// Find the only root that is within the 0 to 1 interval.
t = 2.0 * u - a / 3.0;
if (0.0 > t || t > 1.0) {
t = -u - a / 3.0;
}
} else if (discr < 0.0) {
// Case that happens about 75% of the time --> 3 roots.
qreal mp3 = -p / 3.0;
qreal r = std::sqrt(mp3 * mp3 * mp3);
qreal phi = std::acos(std::min(std::max(-1.0, -q / (2.0 * r)), 1.0));
qreal t1 = 2.0 * std::pow(r, 1.0 / 3.0);
// Find the only root that is within the 0 to 1 interval.
t = t1 * std::cos(phi / 3.0) - a / 3.0;
if (0.0 > t || t > 1.0) {
t = t1 * std::cos((phi + 2.0 * pi) / 3.0) - a / 3.0;
if (0.0 > t || t > 1.0) {
t = t1 * std::cos((phi + 4.0 * pi) / 3.0) - a / 3.0;
}
}
} else if (discr > 0.0) {
// Case that happens about 25% of the time --> 1 root.
qreal q2 = q / 2.0;
qreal sd = std::sqrt(discr);
qreal u = std::pow(-q2 + sd, 1.0 / 3.0);
qreal v = std::pow(q2 + sd, 1.0 / 3.0);
t = u - v - a / 3.0;
}
return t;
}
//-------------------------------------------------------------------------------------------------
// This function is even more simplified and optimized because all the Bezier control points are
// constant. Thus, when computing the Y root there is only one case and the math simplifies to the
// following simple expression.
//-------------------------------------------------------------------------------------------------
qreal EaseInOut::tFromY(const qreal y) const
{
return 0.5 + std::cos((4.0 * M_PI + std::acos(1.0 - 2.0 * y)) / 3.0);
}
//-------------------------------------------------------------------------------------------------
// Given a number of note to place within the given duration, return the list of on-times for each
// note given the current ease-in and ease-out parameters. The first note is at time 0 while the
//-------------------------------------------------------------------------------------------------
void EaseInOut::timeList(const int nbNotes, const int duration, std::vector<int>* times) const
{
qreal nNotes = qreal(nbNotes);
qreal space = qreal(duration);
if (_easeIn == 0.0 && _easeOut == 0.0) {
for (int n = 0; n <= nbNotes; n++) {
times->push_back(static_cast<int>(std::lround((static_cast<qreal>(n) / nNotes) * space)));
}
} else {
for (int n = 0; n <= nbNotes; n++) {
times->push_back(static_cast<int>(std::lround(XfromY(static_cast<qreal>(n) / nNotes) * space)));
}
}
}
}