animation loop parameter

This commit is contained in:
John McCardle 2026-02-27 22:11:29 -05:00
commit 29fe135161
12 changed files with 563 additions and 54 deletions

View file

@ -1141,19 +1141,20 @@ PyObject* Entity3D::py_update_visibility(PyEntity3DObject* self, PyObject* args)
PyObject* Entity3D::py_animate(PyEntity3DObject* self, PyObject* args, PyObject* kwds)
{
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", "conflict_mode", nullptr};
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "loop", "callback", "conflict_mode", nullptr};
const char* property_name;
PyObject* target_value;
float duration;
PyObject* easing_arg = Py_None;
int delta = 0;
int loop_val = 0;
PyObject* callback = nullptr;
const char* conflict_mode_str = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpOs", const_cast<char**>(keywords),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OppOs", const_cast<char**>(keywords),
&property_name, &target_value, &duration,
&easing_arg, &delta, &callback, &conflict_mode_str)) {
&easing_arg, &delta, &loop_val, &callback, &conflict_mode_str)) {
return NULL;
}
@ -1216,7 +1217,7 @@ PyObject* Entity3D::py_animate(PyEntity3DObject* self, PyObject* args, PyObject*
}
// Create the Animation
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, loop_val != 0, callback);
// Start on this entity (uses startEntity3D)
animation->startEntity3D(self->data);

View file

@ -28,17 +28,19 @@ namespace mcrfpydef {
}
// Animation implementation
Animation::Animation(const std::string& targetProperty,
Animation::Animation(const std::string& targetProperty,
const AnimationValue& targetValue,
float duration,
EasingFunction easingFunc,
bool delta,
bool loop,
PyObject* callback)
: targetProperty(targetProperty)
, targetValue(targetValue)
, duration(duration)
, easingFunc(easingFunc)
, delta(delta)
, loop(loop)
, pythonCallback(callback)
{
// Increase reference count for Python callback
@ -123,7 +125,7 @@ void Animation::start(std::shared_ptr<UIDrawable> target) {
// For zero-duration animations, apply final value immediately
if (duration <= 0.0f) {
AnimationValue finalValue = interpolate(1.0f);
AnimationValue finalValue = interpolate(easingFunc(1.0f));
applyValue(target.get(), finalValue);
if (pythonCallback && !callbackTriggered) {
triggerCallback();
@ -155,12 +157,18 @@ void Animation::startEntity(std::shared_ptr<UIEntity> target) {
startValue = target->sprite.getSpriteIndex();
}
}
else if constexpr (std::is_same_v<T, std::vector<int>>) {
// For sprite animation frame lists, get current sprite index
if (targetProperty == "sprite_index" || targetProperty == "sprite_number") {
startValue = target->sprite.getSpriteIndex();
}
}
// Entities don't support other types yet
}, targetValue);
// For zero-duration animations, apply final value immediately
if (duration <= 0.0f) {
AnimationValue finalValue = interpolate(1.0f);
AnimationValue finalValue = interpolate(easingFunc(1.0f));
applyValue(target.get(), finalValue);
if (pythonCallback && !callbackTriggered) {
triggerCallback();
@ -198,7 +206,7 @@ void Animation::startEntity3D(std::shared_ptr<mcrf::Entity3D> target) {
// For zero-duration animations, apply final value immediately
if (duration <= 0.0f) {
AnimationValue finalValue = interpolate(1.0f);
AnimationValue finalValue = interpolate(easingFunc(1.0f));
applyValue(target.get(), finalValue);
if (pythonCallback && !callbackTriggered) {
triggerCallback();
@ -228,17 +236,20 @@ void Animation::complete() {
// Jump to end of animation
elapsed = duration;
// Apply final value
// Apply final value through easing function
// For standard easings, easingFunc(1.0) = 1.0 (no change)
// For ping-pong easings, easingFunc(1.0) = 0.0 (returns to start value)
float finalT = easingFunc(1.0f);
if (auto target = targetWeak.lock()) {
AnimationValue finalValue = interpolate(1.0f);
AnimationValue finalValue = interpolate(finalT);
applyValue(target.get(), finalValue);
}
else if (auto entity = entityTargetWeak.lock()) {
AnimationValue finalValue = interpolate(1.0f);
AnimationValue finalValue = interpolate(finalT);
applyValue(entity.get(), finalValue);
}
else if (auto entity3d = entity3dTargetWeak.lock()) {
AnimationValue finalValue = interpolate(1.0f);
AnimationValue finalValue = interpolate(finalT);
applyValue(entity3d.get(), finalValue);
}
}
@ -269,8 +280,9 @@ bool Animation::update(float deltaTime) {
// Apply final value once before returning
if (isComplete()) {
if (!callbackTriggered) {
// Apply final value for zero-duration animations
AnimationValue finalValue = interpolate(1.0f);
// Apply final value through easing function
float finalT = easingFunc(1.0f);
AnimationValue finalValue = interpolate(finalT);
if (target) {
applyValue(target.get(), finalValue);
} else if (entity) {
@ -288,7 +300,11 @@ bool Animation::update(float deltaTime) {
}
elapsed += deltaTime;
elapsed = std::min(elapsed, duration);
if (loop && duration > 0.0f) {
while (elapsed >= duration) elapsed -= duration;
} else {
elapsed = std::min(elapsed, duration);
}
// Calculate easing value (0.0 to 1.0)
float t = duration > 0 ? elapsed / duration : 1.0f;
@ -722,8 +738,9 @@ void Animation::triggerCallback() {
return;
}
// Final value (interpolated at t=1.0)
PyObject* valueObj = animationValueToPython(interpolate(1.0f));
// Final value (interpolated through easing function at t=1.0)
// For ping-pong easings, this returns the start value (easingFunc(1.0) = 0.0)
PyObject* valueObj = animationValueToPython(interpolate(easingFunc(1.0f)));
if (!valueObj) {
Py_DECREF(targetObj);
Py_DECREF(propertyObj);
@ -956,6 +973,38 @@ float easeInOutBounce(float t) {
}
}
// Ping-pong easing functions (0 -> 1 -> 0)
// These are designed for looping animations where the value should
// smoothly return to the start position each cycle.
float pingPong(float t) {
// Linear triangle wave: 0 -> 1 -> 0
return 1.0f - std::fabs(2.0f * t - 1.0f);
}
float pingPongSmooth(float t) {
// Sine bell curve: smooth acceleration and deceleration
return std::sin(static_cast<float>(M_PI) * t);
}
float pingPongEaseIn(float t) {
// Quadratic ease at rest positions (smooth departure/return, sharp peak)
float pp = 1.0f - std::fabs(2.0f * t - 1.0f);
return pp * pp;
}
float pingPongEaseOut(float t) {
// Ease-out at peak (sharp departure, smooth turnaround)
float pp = 1.0f - std::fabs(2.0f * t - 1.0f);
return pp * (2.0f - pp);
}
float pingPongEaseInOut(float t) {
// sin^2: smooth everywhere including at loop seam
float s = std::sin(static_cast<float>(M_PI) * t);
return s * s;
}
// Get easing function by name
EasingFunction getByName(const std::string& name) {
static std::unordered_map<std::string, EasingFunction> easingMap = {
@ -989,7 +1038,12 @@ EasingFunction getByName(const std::string& name) {
{"easeInOutBack", easeInOutBack},
{"easeInBounce", easeInBounce},
{"easeOutBounce", easeOutBounce},
{"easeInOutBounce", easeInOutBounce}
{"easeInOutBounce", easeInOutBounce},
{"pingPong", pingPong},
{"pingPongSmooth", pingPongSmooth},
{"pingPongEaseIn", pingPongEaseIn},
{"pingPongEaseOut", pingPongEaseOut},
{"pingPongEaseInOut", pingPongEaseInOut}
};
auto it = easingMap.find(name);

View file

@ -44,11 +44,12 @@ typedef std::variant<
class Animation {
public:
// Constructor
Animation(const std::string& targetProperty,
Animation(const std::string& targetProperty,
const AnimationValue& targetValue,
float duration,
EasingFunction easingFunc = EasingFunctions::linear,
bool delta = false,
bool loop = false,
PyObject* callback = nullptr);
// Destructor - cleanup Python callback reference
@ -86,9 +87,10 @@ public:
std::string getTargetProperty() const { return targetProperty; }
float getDuration() const { return duration; }
float getElapsed() const { return elapsed; }
bool isComplete() const { return elapsed >= duration || stopped; }
bool isComplete() const { return (!loop && elapsed >= duration) || stopped; }
bool isStopped() const { return stopped; }
bool isDelta() const { return delta; }
bool isLooping() const { return loop; }
// Get raw target pointer for property locking (#120)
void* getTargetPtr() const {
@ -106,6 +108,7 @@ private:
float elapsed = 0.0f; // Elapsed time
EasingFunction easingFunc; // Easing function to use
bool delta; // If true, targetValue is relative to start
bool loop; // If true, animation repeats from start when complete
bool stopped = false; // If true, animation was stopped without completing
// RAII: Use weak_ptr for safe target tracking
@ -177,7 +180,14 @@ namespace EasingFunctions {
float easeInBounce(float t);
float easeOutBounce(float t);
float easeInOutBounce(float t);
// Ping-pong easing functions (0 -> 1 -> 0, for looping animations)
float pingPong(float t);
float pingPongSmooth(float t);
float pingPongEaseIn(float t);
float pingPongEaseOut(float t);
float pingPongEaseInOut(float t);
// Get easing function by name
EasingFunction getByName(const std::string& name);
}

View file

@ -20,17 +20,18 @@ PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds
}
int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr};
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "loop", "callback", nullptr};
const char* property_name;
PyObject* target_value;
float duration;
PyObject* easing_arg = Py_None;
int delta = 0;
int loop_val = 0;
PyObject* callback = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpO", const_cast<char**>(keywords),
&property_name, &target_value, &duration, &easing_arg, &delta, &callback)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OppO", const_cast<char**>(keywords),
&property_name, &target_value, &duration, &easing_arg, &delta, &loop_val, &callback)) {
return -1;
}
@ -107,8 +108,8 @@ int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
}
// Create the Animation
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, loop_val != 0, callback);
return 0;
}
@ -179,6 +180,10 @@ PyObject* PyAnimation::get_is_delta(PyAnimationObject* self, void* closure) {
return PyBool_FromLong(self->data->isDelta());
}
PyObject* PyAnimation::get_is_looping(PyAnimationObject* self, void* closure) {
return PyBool_FromLong(self->data->isLooping());
}
// Helper to convert Python string to AnimationConflictMode
static bool parseConflictMode(const char* mode_str, AnimationConflictMode& mode) {
if (!mode_str || strcmp(mode_str, "replace") == 0) {
@ -356,6 +361,8 @@ PyGetSetDef PyAnimation::getsetters[] = {
MCRF_PROPERTY(is_complete, "Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called."), NULL},
{"is_delta", (getter)get_is_delta, NULL,
MCRF_PROPERTY(is_delta, "Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value."), NULL},
{"is_looping", (getter)get_is_looping, NULL,
MCRF_PROPERTY(is_looping, "Whether animation loops (bool, read-only). Looping animations repeat from the start when they reach the end."), NULL},
{NULL}
};

View file

@ -24,6 +24,7 @@ public:
static PyObject* get_elapsed(PyAnimationObject* self, void* closure);
static PyObject* get_is_complete(PyAnimationObject* self, void* closure);
static PyObject* get_is_delta(PyAnimationObject* self, void* closure);
static PyObject* get_is_looping(PyAnimationObject* self, void* closure);
// Methods
static PyObject* start(PyAnimationObject* self, PyObject* args, PyObject* kwds);
@ -47,7 +48,7 @@ namespace mcrfpydef {
.tp_repr = (reprfunc)PyAnimation::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"Animation(property: str, target: Any, duration: float, easing: str = 'linear', delta: bool = False, callback: Callable = None)\n"
"Animation(property: str, target: Any, duration: float, easing: str = 'linear', delta: bool = False, loop: bool = False, callback: Callable = None)\n"
"\n"
"Create an animation that interpolates a property value over time.\n"
"\n"
@ -80,22 +81,18 @@ namespace mcrfpydef {
" - 'easeInBack', 'easeOutBack', 'easeInOutBack'\n"
" - 'easeInBounce', 'easeOutBounce', 'easeInOutBounce'\n"
" delta: If True, target is relative to start value (additive). Default False.\n"
" callback: Function(animation, target) called when animation completes.\n"
" loop: If True, animation repeats from start when it reaches the end. Default False.\n"
" callback: Function(target, property, value) called when animation completes.\n"
" Not called for looping animations (since they never complete).\n"
"\n"
"Example:\n"
" # Move a frame from current position to x=500 over 2 seconds\n"
" anim = mcrfpy.Animation('x', 500.0, 2.0, 'easeInOut')\n"
" anim.start(my_frame)\n"
"\n"
" # Fade out with callback\n"
" def on_done(anim, target):\n"
" print('Animation complete!')\n"
" fade = mcrfpy.Animation('fill_color.a', 0, 1.0, callback=on_done)\n"
" fade.start(my_sprite)\n"
"\n"
" # Animate through sprite frames\n"
" walk_cycle = mcrfpy.Animation('sprite_index', [0,1,2,3,2,1], 0.5, 'linear')\n"
" walk_cycle.start(my_entity)\n"
" # Looping sprite animation\n"
" walk = mcrfpy.Animation('sprite_index', [0,1,2,3,2,1], 0.6, loop=True)\n"
" walk.start(my_sprite)\n"
),
.tp_methods = PyAnimation::methods,
.tp_getset = PyAnimation::getsetters,

View file

@ -43,6 +43,11 @@ static const EasingEntry easing_table[] = {
{"EASE_IN_BOUNCE", 28, EasingFunctions::easeInBounce},
{"EASE_OUT_BOUNCE", 29, EasingFunctions::easeOutBounce},
{"EASE_IN_OUT_BOUNCE", 30, EasingFunctions::easeInOutBounce},
{"PING_PONG", 31, EasingFunctions::pingPong},
{"PING_PONG_SMOOTH", 32, EasingFunctions::pingPongSmooth},
{"PING_PONG_EASE_IN", 33, EasingFunctions::pingPongEaseIn},
{"PING_PONG_EASE_OUT", 34, EasingFunctions::pingPongEaseOut},
{"PING_PONG_EASE_IN_OUT", 35, EasingFunctions::pingPongEaseInOut},
};
// Old string names (for backwards compatibility)
@ -56,7 +61,9 @@ static const char* legacy_names[] = {
"easeInCirc", "easeOutCirc", "easeInOutCirc",
"easeInElastic", "easeOutElastic", "easeInOutElastic",
"easeInBack", "easeOutBack", "easeInOutBack",
"easeInBounce", "easeOutBounce", "easeInOutBounce"
"easeInBounce", "easeOutBounce", "easeInOutBounce",
"pingPong", "pingPongSmooth", "pingPongEaseIn",
"pingPongEaseOut", "pingPongEaseInOut"
};
static const int NUM_EASING_ENTRIES = sizeof(easing_table) / sizeof(easing_table[0]);

View file

@ -110,7 +110,7 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds)
UIDRAWABLE_METHODS_BASE, \
{"animate", (PyCFunction)UIDrawable_animate<PyObjectType>, METH_VARARGS | METH_KEYWORDS, \
MCRF_METHOD(Drawable, animate, \
MCRF_SIG("(property: str, target: Any, duration: float, easing=None, delta=False, callback=None, conflict_mode='replace')", "Animation"), \
MCRF_SIG("(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace')", "Animation"), \
MCRF_DESC("Create and start an animation on this drawable's property."), \
MCRF_ARGS_START \
MCRF_ARG("property", "Name of the property to animate (e.g., 'x', 'fill_color', 'opacity')") \
@ -118,7 +118,8 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds)
MCRF_ARG("duration", "Animation duration in seconds") \
MCRF_ARG("easing", "Easing function: Easing enum value, string name, or None for linear") \
MCRF_ARG("delta", "If True, target is relative to current value; if False, target is absolute") \
MCRF_ARG("callback", "Optional callable invoked when animation completes") \
MCRF_ARG("loop", "If True, animation repeats from start when it reaches the end (default False)") \
MCRF_ARG("callback", "Optional callable invoked when animation completes (not called for looping animations)") \
MCRF_ARG("conflict_mode", "'replace' (default), 'queue', or 'error' if property already animating") \
MCRF_RETURNS("Animation object for monitoring progress") \
MCRF_RAISES("ValueError", "If property name is not valid for this drawable type") \

View file

@ -1845,19 +1845,20 @@ int UIDrawable::set_on_move(PyObject* self, PyObject* value, void* closure) {
// Animation shorthand helper - creates and starts an animation on a UIDrawable
// This is a free function (not a member) to avoid incomplete type issues in UIBase.h template
PyObject* UIDrawable_animate_impl(std::shared_ptr<UIDrawable> self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", "conflict_mode", nullptr};
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "loop", "callback", "conflict_mode", nullptr};
const char* property_name;
PyObject* target_value;
float duration;
PyObject* easing_arg = Py_None;
int delta = 0;
int loop_val = 0;
PyObject* callback = nullptr;
const char* conflict_mode_str = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpOs", const_cast<char**>(keywords),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OppOs", const_cast<char**>(keywords),
&property_name, &target_value, &duration,
&easing_arg, &delta, &callback, &conflict_mode_str)) {
&easing_arg, &delta, &loop_val, &callback, &conflict_mode_str)) {
return NULL;
}
@ -1961,7 +1962,7 @@ PyObject* UIDrawable_animate_impl(std::shared_ptr<UIDrawable> self, PyObject* ar
}
// Create the Animation
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, loop_val != 0, callback);
// Start on this drawable
animation->start(self);

View file

@ -945,19 +945,21 @@ PyMethodDef UIEntity_all_methods[] = {
UIDRAWABLE_METHODS_BASE,
{"animate", (PyCFunction)UIEntity::animate, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(Entity, animate,
MCRF_SIG("(property: str, target: Any, duration: float, easing=None, delta=False, callback=None, conflict_mode='replace')", "Animation"),
MCRF_SIG("(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace')", "Animation"),
MCRF_DESC("Create and start an animation on this entity's property."),
MCRF_ARGS_START
MCRF_ARG("property", "Name of the property to animate: 'draw_x', 'draw_y' (tile coords), 'sprite_scale', 'sprite_index'")
MCRF_ARG("target", "Target value - float or int depending on property")
MCRF_ARG("target", "Target value - float, int, or list of int (for sprite frame sequences)")
MCRF_ARG("duration", "Animation duration in seconds")
MCRF_ARG("easing", "Easing function: Easing enum value, string name, or None for linear")
MCRF_ARG("delta", "If True, target is relative to current value; if False, target is absolute")
MCRF_ARG("callback", "Optional callable invoked when animation completes")
MCRF_ARG("loop", "If True, animation repeats from start when it reaches the end (default False)")
MCRF_ARG("callback", "Optional callable invoked when animation completes (not called for looping animations)")
MCRF_ARG("conflict_mode", "'replace' (default), 'queue', or 'error' if property already animating")
MCRF_RETURNS("Animation object for monitoring progress")
MCRF_RAISES("ValueError", "If property name is not valid for Entity (draw_x, draw_y, sprite_scale, sprite_index)")
MCRF_NOTE("Use 'draw_x'/'draw_y' to animate tile coordinates for smooth movement between grid cells.")
MCRF_NOTE("Use 'draw_x'/'draw_y' to animate tile coordinates for smooth movement between grid cells. "
"Use list target with loop=True for repeating sprite frame animations.")
)},
{"at", (PyCFunction)UIEntity::at, METH_VARARGS | METH_KEYWORDS,
"at(x, y) or at(pos) -> GridPointState\n\n"
@ -1136,19 +1138,20 @@ bool UIEntity::hasProperty(const std::string& name) const {
// Animation shorthand for Entity - creates and starts an animation
PyObject* UIEntity::animate(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", "conflict_mode", nullptr};
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "loop", "callback", "conflict_mode", nullptr};
const char* property_name;
PyObject* target_value;
float duration;
PyObject* easing_arg = Py_None;
int delta = 0;
int loop_val = 0;
PyObject* callback = nullptr;
const char* conflict_mode_str = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpOs", const_cast<char**>(keywords),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OppOs", const_cast<char**>(keywords),
&property_name, &target_value, &duration,
&easing_arg, &delta, &callback, &conflict_mode_str)) {
&easing_arg, &delta, &loop_val, &callback, &conflict_mode_str)) {
return NULL;
}
@ -1173,7 +1176,7 @@ PyObject* UIEntity::animate(PyUIEntityObject* self, PyObject* args, PyObject* kw
}
// Convert Python target value to AnimationValue
// Entity only supports float and int properties
// Entity supports float, int, and list of int (for sprite frame animation)
AnimationValue animValue;
if (PyFloat_Check(target_value)) {
@ -1182,8 +1185,23 @@ PyObject* UIEntity::animate(PyUIEntityObject* self, PyObject* args, PyObject* kw
else if (PyLong_Check(target_value)) {
animValue = static_cast<int>(PyLong_AsLong(target_value));
}
else if (PyList_Check(target_value)) {
// List of integers for sprite animation
std::vector<int> indices;
Py_ssize_t size = PyList_Size(target_value);
for (Py_ssize_t i = 0; i < size; i++) {
PyObject* item = PyList_GetItem(target_value, i);
if (PyLong_Check(item)) {
indices.push_back(PyLong_AsLong(item));
} else {
PyErr_SetString(PyExc_TypeError, "Sprite animation list must contain only integers");
return NULL;
}
}
animValue = indices;
}
else {
PyErr_SetString(PyExc_TypeError, "Entity animations only support float or int target values");
PyErr_SetString(PyExc_TypeError, "Entity animations support float, int, or list of int target values");
return NULL;
}
@ -1210,7 +1228,7 @@ PyObject* UIEntity::animate(PyUIEntityObject* self, PyObject* args, PyObject* kw
}
// Create the Animation
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, loop_val != 0, callback);
// Start on this entity (uses startEntity, not start)
animation->startEntity(self->data);