blob: ce0d3ff6a43b07fdae528c92462dd281599a0386 [file]
#include "rive/constraints/scrolling/elastic_scroll_physics.hpp"
#include "math.h"
using namespace rive;
Vec2D ElasticScrollPhysics::advance(float elapsedSeconds)
{
// Capture per-axis position before advancing so we can derive current
// velocity from the actual frame-to-frame motion and write it into the
// base m_speed (used for velocity reporting).
float prevX = m_physicsX != nullptr ? m_physicsX->current() : 0.0f;
float prevY = m_physicsY != nullptr ? m_physicsY->current() : 0.0f;
float advanceX =
m_physicsX != nullptr ? m_physicsX->advance(elapsedSeconds) : 0.0f;
float advanceY =
m_physicsY != nullptr ? m_physicsY->advance(elapsedSeconds) : 0.0f;
if (elapsedSeconds > 0)
{
m_speed = Vec2D((advanceX - prevX) / elapsedSeconds,
(advanceY - prevY) / elapsedSeconds);
}
bool isRunningX = m_physicsX != nullptr && m_physicsX->isRunning();
bool isRunningY = m_physicsY != nullptr && m_physicsY->isRunning();
if (!isRunningX && !isRunningY)
{
reset();
}
return Vec2D(advanceX, advanceY);
}
Vec2D ElasticScrollPhysics::clamp(Vec2D rangeMin, Vec2D rangeMax, Vec2D value)
{
float clampX = m_physicsX != nullptr
? m_physicsX->clamp(rangeMin.x, rangeMax.x, value.x)
: 0.0f;
float clampY = m_physicsY != nullptr
? m_physicsY->clamp(rangeMin.y, rangeMax.y, value.y)
: 0.0f;
return Vec2D(clampX, clampY);
}
void ElasticScrollPhysics::run(Vec2D rangeMin,
Vec2D rangeMax,
Vec2D value,
std::vector<Vec2D> snappingPoints,
float contentSize,
float viewportSize)
{
Super::run(rangeMin,
rangeMax,
value,
snappingPoints,
contentSize,
viewportSize);
std::vector<float> xPoints;
std::vector<float> yPoints;
for (auto pt : snappingPoints)
{
xPoints.push_back(pt.x);
yPoints.push_back(pt.y);
}
if (m_physicsX != nullptr)
{
m_physicsX->run(m_acceleration.x,
rangeMin.x,
rangeMax.x,
value.x,
xPoints,
contentSize,
viewportSize);
}
if (m_physicsY != nullptr)
{
m_physicsY->run(m_acceleration.y,
rangeMin.y,
rangeMax.y,
value.y,
yPoints,
contentSize,
viewportSize);
}
}
void ElasticScrollPhysics::prepare(DraggableConstraintDirection dir)
{
Super::prepare(dir);
if (dir == DraggableConstraintDirection::horizontal ||
dir == DraggableConstraintDirection::all)
{
m_physicsX =
std::make_unique<ElasticScrollPhysicsHelper>(friction(),
speedMultiplier(),
elasticFactor());
}
if (dir == DraggableConstraintDirection::vertical ||
dir == DraggableConstraintDirection::all)
{
m_physicsY =
std::make_unique<ElasticScrollPhysicsHelper>(friction(),
speedMultiplier(),
elasticFactor());
}
}
void ElasticScrollPhysics::reset()
{
Super::reset();
m_physicsX.reset();
m_physicsY.reset();
}
float ElasticScrollPhysicsHelper::advance(float elapsedSeconds)
{
if (m_speed != 0)
{
m_current += m_speed * elapsedSeconds;
if (m_current < m_runRangeMin)
{
m_friction *= 4;
}
else if (m_current > m_runRangeMax)
{
m_friction *= 4;
}
m_speed += -m_speed * std::min(1.0f, elapsedSeconds * m_friction);
if (abs(m_speed) < 5)
{
m_speed = 0;
if (m_current < m_runRangeMin)
{
m_target = m_runRangeMin;
}
else if (m_current > m_runRangeMax)
{
m_target = m_runRangeMax;
}
else
{
m_target = m_current;
}
}
return m_current;
}
auto diff = m_target - m_current;
if (abs(diff) < 0.1)
{
m_current = std::isnan(m_snapTarget) ? m_target : m_snapTarget;
m_isRunning = false;
}
else
{
m_current += diff * std::min(1.0f, elapsedSeconds * 15.0f);
}
return m_current;
}
float ElasticScrollPhysicsHelper::clamp(float rangeMin,
float rangeMax,
float value)
{
if (value < rangeMin)
{
return rangeMin - pow(-(value - rangeMin), m_elasticFactor);
}
else if (value > rangeMax)
{
return rangeMax + pow(value + rangeMax, m_elasticFactor);
}
return value;
}
void ElasticScrollPhysicsHelper::run(float acceleration,
float rangeMin,
float rangeMax,
float value,
std::vector<float> snappingPoints,
float contentSize,
float viewportSize)
{
m_isRunning = true;
m_runRangeMin = rangeMin;
m_runRangeMax = rangeMax;
if (abs(acceleration) > 100)
{
m_speed = acceleration * 0.16f * 0.16f * 0.1f * m_speedMultiplier;
}
else
{
m_speed = 0;
}
if (value < rangeMin)
{
m_target = rangeMin;
}
else if (value > rangeMax)
{
m_target = rangeMax;
}
else
{
m_target = value;
}
m_current = value;
if (!snappingPoints.empty())
{
float endTarget = -(m_current + m_speed / m_friction);
float sectionSize = contentSize != 0 ? contentSize : 1;
int multiple = rangeMax == std::numeric_limits<float>::infinity()
? std::floor(endTarget / sectionSize)
: 0;
float modEndTarget = rangeMax == std::numeric_limits<float>::infinity()
? math::positive_mod(endTarget, sectionSize)
: endTarget;
// -rangeMin is the max positive scroll position. It already accounts
// for viewport trailing padding (rangeMin = maxOffset =
// viewportSize - contentSize - paddingEnd), whereas contentSize -
// viewportSize does not.
float maxTarget = rangeMax == std::numeric_limits<float>::infinity()
? std::numeric_limits<float>::infinity()
: -rangeMin;
float closest = std::numeric_limits<float>::max();
float snapTarget = 0;
for (auto snap : snappingPoints)
{
float diff = std::abs(snap - modEndTarget);
if (diff < closest)
{
closest = diff;
snapTarget = snap + (multiple * sectionSize);
}
}
// Treat the padded end-of-scroll as a candidate snap. Otherwise a
// fling past the last item snaps backward to that item's leading
// edge, dropping the trailing padding.
if (maxTarget != std::numeric_limits<float>::infinity())
{
float diff = std::abs(maxTarget - modEndTarget);
if (diff < closest)
{
closest = diff;
snapTarget = maxTarget;
}
}
snapTarget = std::min(snapTarget, maxTarget);
m_speed = -(snapTarget + m_current) * m_friction;
m_snapTarget = -snapTarget;
}
else
{
m_snapTarget = NAN;
}
}
void ElasticScrollPhysicsHelper::scrollTo(float current,
float target,
float rangeMin,
float rangeMax)
{
m_isRunning = true;
m_runRangeMin = rangeMin;
m_runRangeMax = rangeMax;
m_current = current;
m_target = target;
m_speed = 0; // Skip velocity phase, go directly to settling
m_snapTarget = NAN;
}
void ElasticScrollPhysics::scrollToPosition(Vec2D current,
Vec2D target,
Vec2D rangeMin,
Vec2D rangeMax,
bool horizontal,
bool vertical)
{
// Create physics helpers if needed
if (horizontal && m_physicsX == nullptr)
{
m_physicsX =
std::make_unique<ElasticScrollPhysicsHelper>(friction(),
speedMultiplier(),
elasticFactor());
}
if (vertical && m_physicsY == nullptr)
{
m_physicsY =
std::make_unique<ElasticScrollPhysicsHelper>(friction(),
speedMultiplier(),
elasticFactor());
}
// Initiate settling animation
if (m_physicsX != nullptr && horizontal)
{
m_physicsX->scrollTo(current.x, target.x, rangeMin.x, rangeMax.x);
}
if (m_physicsY != nullptr && vertical)
{
m_physicsY->scrollTo(current.y, target.y, rangeMin.y, rangeMax.y);
}
}