blob: 512fdc01fb7acff742bd41fb263f928512a55068 [file]
#include "rive/animation/keyed_property.hpp"
#include "rive/animation/keyed_object.hpp"
#include "rive/animation/keyframe.hpp"
#include "rive/animation/keyframe_interpolator.hpp"
#include "rive/animation/interpolating_keyframe.hpp"
#include "rive/animation/keyed_callback_reporter.hpp"
#include "rive/importers/import_stack.hpp"
#include "rive/importers/keyed_object_importer.hpp"
#include <algorithm>
using namespace rive;
KeyedProperty::KeyedProperty() {}
KeyedProperty::~KeyedProperty() {}
void KeyedProperty::addKeyFrame(std::unique_ptr<KeyFrame> keyframe)
{
m_keyFrames.push_back(std::move(keyframe));
}
// `addKeyFrameForEditor`, `clearEditorKeyFrames`, `sortEditorKeyFrames`
// and `editorClosestFrameIndex` live in the editor_native package
// (`src/editor/animation/keyed_property_editor.cpp`) so the runtime-
// only build never compiles or links them. The declarations stay on
// `KeyedProperty` (under `#ifdef WITH_RIVE_EDITOR` in the header)
// because `apply()` below still uses `m_editorKeyFrames` directly when
// the editor list is populated — the runtime build just sees an empty
// vector and falls through to the runtime-owned `m_keyFrames`.
int KeyedProperty::closestFrameIndex(float seconds, int exactOffset) const
{
int mid = 0;
float closestSeconds = 0;
int start = 0;
auto numKeyFrames = static_cast<int>(m_keyFrames.size());
int end = numKeyFrames - 1;
// If it's the last keyframe, we skip the binary search
if (seconds > m_keyFrames[end]->seconds())
{
return end + 1;
}
while (start <= end)
{
mid = (start + end) >> 1;
closestSeconds = m_keyFrames[mid]->seconds();
if (closestSeconds < seconds)
{
start = mid + 1;
}
else if (closestSeconds > seconds)
{
end = mid - 1;
}
else
{
return mid + exactOffset;
}
}
return start;
}
void KeyedProperty::reportKeyedCallbacks(KeyedCallbackReporter* reporter,
uint32_t objectId,
float secondsFrom,
float secondsTo,
bool isAtStartFrame) const
{
if (secondsFrom == secondsTo)
{
return;
}
bool isForward = secondsFrom <= secondsTo;
int fromExactOffset = 0;
int toExactOffset = isForward ? 1 : 0;
if (isForward)
{
if (!isAtStartFrame)
{
fromExactOffset = 1;
}
}
else
{
if (isAtStartFrame)
{
fromExactOffset = 1;
}
}
int idx = closestFrameIndex(secondsFrom, fromExactOffset);
int idxTo = closestFrameIndex(secondsTo, toExactOffset);
if (idxTo < idx)
{
auto swap = idx;
idx = idxTo;
idxTo = swap;
}
while (idxTo > idx)
{
const std::unique_ptr<KeyFrame>& frame = m_keyFrames[idx];
reporter->reportKeyedCallback(objectId,
propertyKey(),
secondsTo - frame->seconds());
idx++;
}
}
void KeyedProperty::apply(Core* object,
float seconds,
float mix,
const LinearAnimationInstance* context)
{
#ifdef WITH_RIVE_EDITOR
// A given KeyedProperty instance is either runtime-loaded (uses
// m_keyFrames unique_ptr owned) or coop-loaded (uses
// m_editorKeyFrames non-owning). Pick the populated one; the
// binary-search / interpolate logic below operates on whichever
// side holds the data. Both-populated can't happen in practice
// — a single property either lives in the importer chain or in
// the arena, never both.
const bool useEditor = m_keyFrames.empty() && !m_editorKeyFrames.empty();
const size_t frameCount =
useEditor ? m_editorKeyFrames.size() : m_keyFrames.size();
// Editor edge case: the user just deleted every kf in this
// property (e.g. via kDeleteKeyFrameSelection). The KeyedProperty
// Core itself stays alive — it's only orphaned when its parent
// KeyedObject gets deleted — but its editor list is empty, and
// the binary search below would read `m_editorKeyFrames[-1]`.
// Skip with no overlay so the property reverts to its design-
// time value on the next frame.
if (frameCount == 0)
return;
auto frameAt = [this, useEditor](int i) -> InterpolatingKeyFrame* {
return static_cast<InterpolatingKeyFrame*>(
useEditor ? m_editorKeyFrames[i] : m_keyFrames[i].get());
};
const int closestIdx = useEditor ? editorClosestFrameIndex(seconds)
: closestFrameIndex(seconds);
#else
assert(!m_keyFrames.empty());
const size_t frameCount = m_keyFrames.size();
auto frameAt = [this](int i) -> InterpolatingKeyFrame* {
return static_cast<InterpolatingKeyFrame*>(m_keyFrames[i].get());
};
const int closestIdx = closestFrameIndex(seconds);
#endif
auto interpolatorHost = InterpolatorHost::from(object);
auto actualMix = mix;
if (interpolatorHost != nullptr &&
interpolatorHost->overridesKeyedInterpolation(propertyKey()))
{
actualMix = 1.0f;
}
int idx = closestIdx;
int pk = propertyKey();
if (idx == 0)
{
frameAt(0)->apply(object, pk, actualMix, context);
}
else
{
if (idx < static_cast<int>(frameCount))
{
InterpolatingKeyFrame* fromFrame = frameAt(idx - 1);
InterpolatingKeyFrame* toFrame = frameAt(idx);
if (seconds == toFrame->seconds())
{
toFrame->apply(object, pk, actualMix, context);
}
else
{
if (fromFrame->interpolationType() == 0)
{
fromFrame->apply(object, pk, actualMix, context);
}
else
{
fromFrame->applyInterpolation(object,
pk,
seconds,
toFrame,
actualMix,
context);
}
}
}
else
{
frameAt(idx - 1)->apply(object, pk, actualMix, context);
}
}
}
StatusCode KeyedProperty::onAddedDirty(CoreContext* context)
{
StatusCode code;
for (auto& keyframe : m_keyFrames)
{
if ((code = keyframe->onAddedDirty(context)) != StatusCode::Ok)
{
return code;
}
}
#ifdef WITH_RIVE_EDITOR
for (auto* keyframe : m_editorKeyFrames)
{
keyframe->onAddedDirty(context);
}
#endif
return StatusCode::Ok;
}
StatusCode KeyedProperty::onAddedClean(CoreContext* context)
{
StatusCode code;
for (auto& keyframe : m_keyFrames)
{
if ((code = keyframe->onAddedClean(context)) != StatusCode::Ok)
{
return code;
}
}
#ifdef WITH_RIVE_EDITOR
for (auto* keyframe : m_editorKeyFrames)
{
keyframe->onAddedClean(context);
}
#endif
return StatusCode::Ok;
}
StatusCode KeyedProperty::import(ImportStack& importStack)
{
auto importer =
importStack.latest<KeyedObjectImporter>(KeyedObjectBase::typeKey);
if (importer == nullptr)
{
return StatusCode::MissingObject;
}
importer->addKeyedProperty(std::unique_ptr<KeyedProperty>(this));
return Super::import(importStack);
}