fix for unexpected triggered events

fixes #7226
This PR fixes reports of events when animations are playing backward.
It considers both the timeline speed and the state machine state speed to determine direction.
It also handles not reporting an event twice if it comes from a "ping pong" loop.
It also adds a small performance improvement, skipping the binary search if possible.

Diffs=
265c00985 fix for unexpected triggered events (#7227)

Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head
index dd5879c..a329f6d 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-8a538c243fcc5b72159388a7e63669f1e6998dda
+265c009852e86492deb69277ad18aa2c394c93e5
diff --git a/include/rive/animation/keyed_object.hpp b/include/rive/animation/keyed_object.hpp
index e11306c..860e11f 100644
--- a/include/rive/animation/keyed_object.hpp
+++ b/include/rive/animation/keyed_object.hpp
@@ -19,7 +19,7 @@
     void reportKeyedCallbacks(KeyedCallbackReporter* reporter,
                               float secondsFrom,
                               float secondsTo,
-                              int secondsFromExactOffset) const;
+                              bool isAtStartFrame) const;
     void apply(Artboard* coreContext, float time, float mix);
 
     StatusCode import(ImportStack& importStack) override;
diff --git a/include/rive/animation/keyed_property.hpp b/include/rive/animation/keyed_property.hpp
index c45f3bd..1ef73ce 100644
--- a/include/rive/animation/keyed_property.hpp
+++ b/include/rive/animation/keyed_property.hpp
@@ -20,7 +20,7 @@
                               uint32_t objectId,
                               float secondsFrom,
                               float secondsTo,
-                              int secondsFromExactOffset) const;
+                              bool isAtStartFrame) const;
 
     /// Apply interpolating key frames.
     void apply(Core* object, float time, float mix);
diff --git a/include/rive/animation/linear_animation.hpp b/include/rive/animation/linear_animation.hpp
index 2efcfeb..f7aa60e 100644
--- a/include/rive/animation/linear_animation.hpp
+++ b/include/rive/animation/linear_animation.hpp
@@ -35,6 +35,7 @@
 
     /// Returns the start time/ end time of the animation in seconds, considering speed
     float startTime() const;
+    float startTime(float multiplier) const;
     float endTime() const;
 
     /// Convert a global clock to local seconds (takes into consideration
@@ -49,7 +50,9 @@
 
     void reportKeyedCallbacks(KeyedCallbackReporter* reporter,
                               float secondsFrom,
-                              float secondsTo) const;
+                              float secondsTo,
+                              float speedDirection,
+                              bool fromPong) const;
 };
 } // namespace rive
 
diff --git a/include/rive/animation/linear_animation_instance.hpp b/include/rive/animation/linear_animation_instance.hpp
index 4919bd7..316b0c7 100644
--- a/include/rive/animation/linear_animation_instance.hpp
+++ b/include/rive/animation/linear_animation_instance.hpp
@@ -101,6 +101,7 @@
 private:
     const LinearAnimation* m_animation = nullptr;
     float m_time;
+    float m_speedDirection;
     float m_totalTime;
     float m_lastTotalTime;
     float m_spilledTime;
diff --git a/src/animation/keyed_object.cpp b/src/animation/keyed_object.cpp
index bb54f88..0bb8e63 100644
--- a/src/animation/keyed_object.cpp
+++ b/src/animation/keyed_object.cpp
@@ -52,7 +52,7 @@
 void KeyedObject::reportKeyedCallbacks(KeyedCallbackReporter* reporter,
                                        float secondsFrom,
                                        float secondsTo,
-                                       int secondsFromExactOffset) const
+                                       bool isAtStartFrame) const
 {
     for (const std::unique_ptr<KeyedProperty>& property : m_keyedProperties)
     {
@@ -64,7 +64,7 @@
                                        objectId(),
                                        secondsFrom,
                                        secondsTo,
-                                       secondsFromExactOffset);
+                                       isAtStartFrame);
     }
 }
 
diff --git a/src/animation/keyed_property.cpp b/src/animation/keyed_property.cpp
index 750e976..7156fb8 100644
--- a/src/animation/keyed_property.cpp
+++ b/src/animation/keyed_property.cpp
@@ -18,13 +18,18 @@
 
 int KeyedProperty::closestFrameIndex(float seconds, int exactOffset) const
 {
-    int idx = 0;
     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;
@@ -41,19 +46,39 @@
         {
             return mid + exactOffset;
         }
-        idx = start;
     }
-    return idx;
+    return start;
 }
 
 void KeyedProperty::reportKeyedCallbacks(KeyedCallbackReporter* reporter,
                                          uint32_t objectId,
                                          float secondsFrom,
                                          float secondsTo,
-                                         int secondsFromExactOffset) const
+                                         bool isAtStartFrame) const
 {
-    int idx = closestFrameIndex(secondsFrom, secondsFromExactOffset);
-    int idxTo = closestFrameIndex(secondsTo, 1);
+    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)
     {
diff --git a/src/animation/linear_animation.cpp b/src/animation/linear_animation.cpp
index c2ade3b..21d795d 100644
--- a/src/animation/linear_animation.cpp
+++ b/src/animation/linear_animation.cpp
@@ -86,6 +86,10 @@
 }
 
 float LinearAnimation::startTime() const { return (speed() >= 0) ? startSeconds() : endSeconds(); }
+float LinearAnimation::startTime(float multiplier) const
+{
+    return ((speed() * multiplier) >= 0) ? startSeconds() : endSeconds();
+}
 float LinearAnimation::endTime() const { return (speed() >= 0) ? endSeconds() : startSeconds(); }
 float LinearAnimation::durationSeconds() const { return std::abs(endSeconds() - startSeconds()); }
 
@@ -119,16 +123,18 @@
 
 void LinearAnimation::reportKeyedCallbacks(KeyedCallbackReporter* reporter,
                                            float secondsFrom,
-                                           float secondsTo) const
+                                           float secondsTo,
+                                           float speedDirection,
+                                           bool fromPong) const
 {
-    int secondsFromExactOffset =
-        startTime() == secondsFrom &&
-                (speed() >= 0 ? secondsFrom < secondsTo : secondsFrom < secondsTo)
-            ? 0
-            : 1;
+    float startingTime = startTime(speedDirection);
+    bool isAtStartFrame = startingTime == secondsFrom;
 
-    for (const auto& object : m_KeyedObjects)
+    if (!isAtStartFrame || !fromPong)
     {
-        object->reportKeyedCallbacks(reporter, secondsFrom, secondsTo, secondsFromExactOffset);
+        for (const auto& object : m_KeyedObjects)
+        {
+            object->reportKeyedCallbacks(reporter, secondsFrom, secondsTo, isAtStartFrame);
+        }
     }
 }
\ No newline at end of file
diff --git a/src/animation/linear_animation_instance.cpp b/src/animation/linear_animation_instance.cpp
index ff7fe55..f40f8af 100644
--- a/src/animation/linear_animation_instance.cpp
+++ b/src/animation/linear_animation_instance.cpp
@@ -13,6 +13,7 @@
     Scene(instance),
     m_animation((assert(animation != nullptr), animation)),
     m_time((speedMultiplier >= 0) ? animation->startTime() : animation->endTime()),
+    m_speedDirection((speedMultiplier >= 0) ? 1 : -1),
     m_totalTime(0.0f),
     m_lastTotalTime(0.0f),
     m_spilledTime(0.0f),
@@ -23,6 +24,7 @@
     Scene(lhs),
     m_animation(lhs.m_animation),
     m_time(lhs.m_time),
+    m_speedDirection(lhs.m_speedDirection),
     m_totalTime(lhs.m_totalTime),
     m_lastTotalTime(lhs.m_lastTotalTime),
     m_spilledTime(lhs.m_spilledTime),
@@ -64,7 +66,7 @@
     m_time += deltaSeconds;
     if (reporter != nullptr)
     {
-        animation.reportKeyedCallbacks(reporter, lastTime, m_time);
+        animation.reportKeyedCallbacks(reporter, lastTime, m_time, m_speedDirection, false);
     }
 
     int fps = animation.fps();
@@ -107,7 +109,7 @@
                 didLoop = true;
                 if (reporter != nullptr)
                 {
-                    animation.reportKeyedCallbacks(reporter, 0.0f, m_time);
+                    animation.reportKeyedCallbacks(reporter, 0.0f, m_time, m_speedDirection, false);
                 }
             }
             else if (direction == -1 && frames <= start)
@@ -119,11 +121,16 @@
                 didLoop = true;
                 if (reporter != nullptr)
                 {
-                    animation.reportKeyedCallbacks(reporter, end / (float)fps, m_time);
+                    animation.reportKeyedCallbacks(reporter,
+                                                   end / (float)fps,
+                                                   m_time,
+                                                   m_speedDirection,
+                                                   false);
                 }
             }
             break;
         case Loop::pingPong:
+            bool fromPong = true;
             while (true)
             {
                 if (direction == 1 && frames >= end)
@@ -153,8 +160,13 @@
                 didLoop = true;
                 if (reporter != nullptr)
                 {
-                    animation.reportKeyedCallbacks(reporter, lastTime, m_time);
+                    animation.reportKeyedCallbacks(reporter,
+                                                   lastTime,
+                                                   m_time,
+                                                   m_speedDirection,
+                                                   fromPong);
                 }
+                fromPong = !fromPong;
             }
             break;
     }