animation loop parameter
This commit is contained in:
parent
550201d365
commit
29fe135161
12 changed files with 563 additions and 54 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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") \
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue