diff --git a/include/hyprutils/animation/BezierCurve.hpp b/include/hyprutils/animation/BezierCurve.hpp new file mode 100644 index 0000000..c332742 --- /dev/null +++ b/include/hyprutils/animation/BezierCurve.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "../math/Vector2D.hpp" + +constexpr int BAKEDPOINTS = 255; +constexpr float INVBAKEDPOINTS = 1.f / BAKEDPOINTS; + +namespace Hyprutils { + namespace Animation { + + /* An implementation of a cubic bezier curve. */ + class CBezierCurve { + public: + /* Calculates a cubic bezier curve based on 2 control points (EXCLUDES the 0,0 and 1,1 points). */ + void setup(const std::array& points); + + float getYForT(float const& t) const; + float getXForT(float const& t) const; + float getYForPoint(float const& x) const; + + private: + /* this INCLUDES the 0,0 and 1,1 points. */ + std::vector m_vPoints; + + std::array m_aPointsBaked; + }; + } +} diff --git a/src/animation/BezierCurve.cpp b/src/animation/BezierCurve.cpp new file mode 100644 index 0000000..a5b5c78 --- /dev/null +++ b/src/animation/BezierCurve.cpp @@ -0,0 +1,88 @@ +#include + +#include +#include + +using namespace Hyprutils::Animation; +using namespace Hyprutils::Math; + +void CBezierCurve::setup(const std::array& pVec) { + //const auto BEGIN = std::chrono::high_resolution_clock::now(); + + // Avoid reallocations by reserving enough memory upfront + m_vPoints.resize(pVec.size() + 2); + m_vPoints = { + Vector2D(0, 0), // Start point + pVec[0], pVec[1], // Control points + Vector2D(1, 1) // End point + }; + + if (m_vPoints.size() != 4) + std::abort(); + + // bake BAKEDPOINTS points for faster lookups + // T -> X ( / BAKEDPOINTS ) + for (int i = 0; i < BAKEDPOINTS; ++i) { + float const t = (i + 1) / (float)BAKEDPOINTS; + m_aPointsBaked[i] = Vector2D(getXForT(t), getYForT(t)); + } + + //const auto ELAPSEDUS = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - BEGIN).count() / 1000.f; + //const auto POINTSSIZE = m_aPointsBaked.size() * sizeof(m_aPointsBaked[0]) / 1000.f; + + //const auto BEGINCALC = std::chrono::high_resolution_clock::now(); + for (int j = 1; j < 10; ++j) { + float i = j / 10.0f; + getYForPoint(i); + } + //const auto ELAPSEDCALCAVG = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - BEGINCALC).count() / 1000.f / 10.f; + + //Debug::log(LOG, "Created a bezier curve, baked {} points, mem usage: {:.2f}kB, time to bake: {:.2f}µs. Estimated average calc time: {:.2f}µs.", BAKEDPOINTS, POINTSSIZE, + // ELAPSEDUS, ELAPSEDCALCAVG); +} + +float CBezierCurve::getXForT(float const& t) const { + float t2 = t * t; + float t3 = t2 * t; + + return 3 * t * (1 - t) * (1 - t) * m_vPoints[1].x + 3 * t2 * (1 - t) * m_vPoints[2].x + t3 * m_vPoints[3].x; +} + +float CBezierCurve::getYForT(float const& t) const { + float t2 = t * t; + float t3 = t2 * t; + + return 3 * t * (1 - t) * (1 - t) * m_vPoints[1].y + 3 * t2 * (1 - t) * m_vPoints[2].y + t3 * m_vPoints[3].y; +} + +// Todo: this probably can be done better and faster +float CBezierCurve::getYForPoint(float const& x) const { + if (x >= 1.f) + return 1.f; + if (x <= 0.f) + return 0.f; + + int index = 0; + bool below = true; + for (int step = (BAKEDPOINTS + 1) / 2; step > 0; step /= 2) { + if (below) + index += step; + else + index -= step; + + below = m_aPointsBaked[index].x < x; + } + + int lowerIndex = index - (!below || index == BAKEDPOINTS - 1); + + // in the name of performance i shall make a hack + const auto LOWERPOINT = &m_aPointsBaked[lowerIndex]; + const auto UPPERPOINT = &m_aPointsBaked[lowerIndex + 1]; + + const auto PERCINDELTA = (x - LOWERPOINT->x) / (UPPERPOINT->x - LOWERPOINT->x); + + if (std::isnan(PERCINDELTA) || std::isinf(PERCINDELTA)) // can sometimes happen for VERY small x + return 0.f; + + return LOWERPOINT->y + (UPPERPOINT->y - LOWERPOINT->y) * PERCINDELTA; +}