diff --git a/src/Animation.cpp b/src/Animation.cpp index 93dc394..adef173 100644 --- a/src/Animation.cpp +++ b/src/Animation.cpp @@ -57,15 +57,15 @@ Animation::~Animation() { void Animation::start(std::shared_ptr target) { if (!target) return; - + targetWeak = target; elapsed = 0.0f; callbackTriggered = false; // Reset callback state - + // Capture start value from target std::visit([this, &target](const auto& targetVal) { using T = std::decay_t; - + if constexpr (std::is_same_v) { float value; if (target->getProperty(targetProperty, value)) { @@ -73,9 +73,15 @@ void Animation::start(std::shared_ptr target) { } } else if constexpr (std::is_same_v) { - int value; - if (target->getProperty(targetProperty, value)) { - startValue = value; + // Most UI properties use float, so try float first, then int + float fvalue; + if (target->getProperty(targetProperty, fvalue)) { + startValue = static_cast(fvalue); + } else { + int ivalue; + if (target->getProperty(targetProperty, ivalue)) { + startValue = ivalue; + } } } else if constexpr (std::is_same_v>) { @@ -104,19 +110,29 @@ void Animation::start(std::shared_ptr target) { } } }, targetValue); + + // For zero-duration animations, apply final value immediately + if (duration <= 0.0f) { + AnimationValue finalValue = interpolate(1.0f); + applyValue(target.get(), finalValue); + if (pythonCallback && !callbackTriggered) { + triggerCallback(); + } + callbackTriggered = true; + } } void Animation::startEntity(std::shared_ptr target) { if (!target) return; - + entityTargetWeak = target; elapsed = 0.0f; callbackTriggered = false; // Reset callback state - + // Capture the starting value from the entity std::visit([this, target](const auto& val) { using T = std::decay_t; - + if constexpr (std::is_same_v) { float value = 0.0f; if (target->getProperty(targetProperty, value)) { @@ -131,6 +147,16 @@ void Animation::startEntity(std::shared_ptr target) { } // 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); + applyValue(target.get(), finalValue); + if (pythonCallback && !callbackTriggered) { + triggerCallback(); + } + callbackTriggered = true; + } } bool Animation::hasValidTarget() const { @@ -169,39 +195,55 @@ bool Animation::update(float deltaTime) { // Try to lock weak_ptr to get shared_ptr std::shared_ptr target = targetWeak.lock(); std::shared_ptr entity = entityTargetWeak.lock(); - + // If both are null, target was destroyed if (!target && !entity) { return false; // Remove this animation } - + + // Handle already-complete animations (e.g., duration=0) + // Apply final value once before returning if (isComplete()) { + if (!callbackTriggered) { + // Apply final value for zero-duration animations + AnimationValue finalValue = interpolate(1.0f); + if (target) { + applyValue(target.get(), finalValue); + } else if (entity) { + applyValue(entity.get(), finalValue); + } + // Trigger callback + if (pythonCallback) { + triggerCallback(); + } + callbackTriggered = true; + } return false; } - + elapsed += deltaTime; elapsed = std::min(elapsed, duration); - + // Calculate easing value (0.0 to 1.0) float t = duration > 0 ? elapsed / duration : 1.0f; float easedT = easingFunc(t); - + // Get interpolated value AnimationValue currentValue = interpolate(easedT); - + // Apply to whichever target is valid if (target) { applyValue(target.get(), currentValue); } else if (entity) { applyValue(entity.get(), currentValue); } - + // Trigger callback when animation completes // Check pythonCallback again in case it was cleared during update if (isComplete() && !callbackTriggered && pythonCallback) { triggerCallback(); } - + return !isComplete(); } @@ -310,15 +352,19 @@ AnimationValue Animation::interpolate(float t) const { void Animation::applyValue(UIDrawable* target, const AnimationValue& value) { if (!target) return; - + std::visit([this, target](const auto& val) { using T = std::decay_t; - + if constexpr (std::is_same_v) { target->setProperty(targetProperty, val); } else if constexpr (std::is_same_v) { - target->setProperty(targetProperty, val); + // Most UI properties use float setProperty, so try float first + if (!target->setProperty(targetProperty, static_cast(val))) { + // Fall back to int if float didn't work + target->setProperty(targetProperty, val); + } } else if constexpr (std::is_same_v) { target->setProperty(targetProperty, val); diff --git a/src/PyAnimation.cpp b/src/PyAnimation.cpp index 952aefc..9044b9f 100644 --- a/src/PyAnimation.cpp +++ b/src/PyAnimation.cpp @@ -113,6 +113,48 @@ void PyAnimation::dealloc(PyAnimationObject* self) { Py_TYPE(self)->tp_free((PyObject*)self); } +PyObject* PyAnimation::repr(PyAnimationObject* self) { + if (!self->data) { + return PyUnicode_FromString(""); + } + + std::string property = self->data->getTargetProperty(); + float duration = self->data->getDuration(); + float elapsed = self->data->getElapsed(); + bool complete = self->data->isComplete(); + bool delta = self->data->isDelta(); + bool hasTarget = self->data->hasValidTarget(); + + // Format: + // or: + // or: + // or: + + std::string status; + if (!hasTarget) { + status = "(no target)"; + } else if (complete) { + status = "complete"; + } else { + char buf[32]; + snprintf(buf, sizeof(buf), "elapsed=%.2fs", elapsed); + status = buf; + } + + char result[256]; + if (delta) { + snprintf(result, sizeof(result), + "", + property.c_str(), duration, status.c_str()); + } else { + snprintf(result, sizeof(result), + "", + property.c_str(), duration, status.c_str()); + } + + return PyUnicode_FromString(result); +} + PyObject* PyAnimation::get_property(PyAnimationObject* self, void* closure) { return PyUnicode_FromString(self->data->getTargetProperty().c_str()); } diff --git a/src/PyAnimation.h b/src/PyAnimation.h index 964844e..ec463f9 100644 --- a/src/PyAnimation.h +++ b/src/PyAnimation.h @@ -16,6 +16,7 @@ public: static PyObject* create(PyTypeObject* type, PyObject* args, PyObject* kwds); static int init(PyAnimationObject* self, PyObject* args, PyObject* kwds); static void dealloc(PyAnimationObject* self); + static PyObject* repr(PyAnimationObject* self); // Properties static PyObject* get_property(PyAnimationObject* self, void* closure); @@ -42,8 +43,59 @@ namespace mcrfpydef { .tp_basicsize = sizeof(PyAnimationObject), .tp_itemsize = 0, .tp_dealloc = (destructor)PyAnimation::dealloc, + .tp_repr = (reprfunc)PyAnimation::repr, .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Animation object for animating UI properties"), + .tp_doc = PyDoc_STR( + "Animation(property: str, target: Any, duration: float, easing: str = 'linear', delta: bool = False, callback: Callable = None)\n" + "\n" + "Create an animation that interpolates a property value over time.\n" + "\n" + "Args:\n" + " property: Property name to animate. Valid properties depend on target type:\n" + " - Position/Size: 'x', 'y', 'w', 'h', 'pos', 'size'\n" + " - Appearance: 'fill_color', 'outline_color', 'outline', 'opacity'\n" + " - Sprite: 'sprite_index', 'sprite_number', 'scale'\n" + " - Grid: 'center', 'zoom'\n" + " - Caption: 'text'\n" + " - Sub-properties: 'fill_color.r', 'fill_color.g', 'fill_color.b', 'fill_color.a'\n" + " target: Target value for the animation. Type depends on property:\n" + " - float: For numeric properties (x, y, w, h, scale, opacity, zoom)\n" + " - int: For integer properties (sprite_index)\n" + " - tuple (r, g, b[, a]): For color properties\n" + " - tuple (x, y): For vector properties (pos, size, center)\n" + " - list[int]: For sprite animation sequences\n" + " - str: For text animation\n" + " duration: Animation duration in seconds.\n" + " easing: Easing function name. Options:\n" + " - 'linear' (default)\n" + " - 'easeIn', 'easeOut', 'easeInOut'\n" + " - 'easeInQuad', 'easeOutQuad', 'easeInOutQuad'\n" + " - 'easeInCubic', 'easeOutCubic', 'easeInOutCubic'\n" + " - 'easeInQuart', 'easeOutQuart', 'easeInOutQuart'\n" + " - 'easeInSine', 'easeOutSine', 'easeInOutSine'\n" + " - 'easeInExpo', 'easeOutExpo', 'easeInOutExpo'\n" + " - 'easeInCirc', 'easeOutCirc', 'easeInOutCirc'\n" + " - 'easeInElastic', 'easeOutElastic', 'easeInOutElastic'\n" + " - '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" + "\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" + ), .tp_methods = PyAnimation::methods, .tp_getset = PyAnimation::getsetters, .tp_init = (initproc)PyAnimation::init, diff --git a/src/PyColor.h b/src/PyColor.h index c5cb2fb..0ade897 100644 --- a/src/PyColor.h +++ b/src/PyColor.h @@ -47,7 +47,34 @@ namespace mcrfpydef { .tp_repr = PyColor::repr, .tp_hash = PyColor::hash, .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("SFML Color Object"), + .tp_doc = PyDoc_STR( + "Color(r: int = 0, g: int = 0, b: int = 0, a: int = 255)\n" + "\n" + "RGBA color representation.\n" + "\n" + "Args:\n" + " r: Red component (0-255)\n" + " g: Green component (0-255)\n" + " b: Blue component (0-255)\n" + " a: Alpha component (0-255, default 255 = opaque)\n" + "\n" + "Note:\n" + " When accessing colors from UI elements (e.g., frame.fill_color),\n" + " you receive a COPY of the color. Modifying it doesn't affect the\n" + " original. To change a component:\n" + "\n" + " # This does NOT work:\n" + " frame.fill_color.r = 255 # Modifies a temporary copy\n" + "\n" + " # Do this instead:\n" + " c = frame.fill_color\n" + " c.r = 255\n" + " frame.fill_color = c\n" + "\n" + " # Or use Animation for sub-properties:\n" + " anim = mcrfpy.Animation('fill_color.r', 255, 0.5, 'linear')\n" + " anim.start(frame)\n" + ), .tp_methods = PyColor::methods, .tp_getset = PyColor::getsetters, .tp_init = (initproc)PyColor::init, diff --git a/src/UICaption.cpp b/src/UICaption.cpp index 2434916..2a76175 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -268,8 +268,12 @@ PyGetSetDef UICaption::getsetters[] = { //{"w", (getter)PyUIFrame_get_float_member, (setter)PyUIFrame_set_float_member, "width of the rectangle", (void*)2}, //{"h", (getter)PyUIFrame_get_float_member, (setter)PyUIFrame_set_float_member, "height of the rectangle", (void*)3}, {"outline", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Thickness of the border", (void*)4}, - {"fill_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member, "Fill color of the text", (void*)0}, - {"outline_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member, "Outline color of the text", (void*)1}, + {"fill_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member, + "Fill color of the text. Returns a copy; modifying components requires reassignment. " + "For animation, use 'fill_color.r', 'fill_color.g', etc.", (void*)0}, + {"outline_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member, + "Outline color of the text. Returns a copy; modifying components requires reassignment. " + "For animation, use 'outline_color.r', 'outline_color.g', etc.", (void*)1}, //{"children", (getter)PyUIFrame_get_children, NULL, "UICollection of objects on top of this one", NULL}, {"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL}, {"font_size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Font size (integer) in points", (void*)5}, diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index 4a74123..b667c78 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -434,8 +434,12 @@ PyGetSetDef UIFrame::getsetters[] = { {"w", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "width of the rectangle", (void*)((intptr_t)PyObjectsEnum::UIFRAME << 8 | 2)}, {"h", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "height of the rectangle", (void*)((intptr_t)PyObjectsEnum::UIFRAME << 8 | 3)}, {"outline", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "Thickness of the border", (void*)4}, - {"fill_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Fill color of the rectangle", (void*)0}, - {"outline_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Outline color of the rectangle", (void*)1}, + {"fill_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, + "Fill color of the rectangle. Returns a copy; modifying components requires reassignment. " + "For animation, use 'fill_color.r', 'fill_color.g', etc.", (void*)0}, + {"outline_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, + "Outline color of the rectangle. Returns a copy; modifying components requires reassignment. " + "For animation, use 'outline_color.r', 'outline_color.g', etc.", (void*)1}, {"children", (getter)UIFrame::get_children, NULL, "UICollection of objects on top of this one", NULL}, {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, MCRF_PROPERTY(on_click, diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 7b59b12..c3de431 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -2059,7 +2059,9 @@ PyGetSetDef UIGrid::getsetters[] = { ), (void*)PyObjectsEnum::UIGRID}, {"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5 - {"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color, "Background fill color of the grid", NULL}, + {"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color, + "Background fill color of the grid. Returns a copy; modifying components requires reassignment. " + "For animation, use 'fill_color.r', 'fill_color.g', etc.", NULL}, {"perspective", (getter)UIGrid::get_perspective, (setter)UIGrid::set_perspective, "Entity whose perspective to use for FOV rendering (None for omniscient view). " "Setting an entity automatically enables perspective mode.", NULL}, diff --git a/stubs/mcrfpy.pyi b/stubs/mcrfpy.pyi index 55c9886..e685e48 100644 --- a/stubs/mcrfpy.pyi +++ b/stubs/mcrfpy.pyi @@ -12,26 +12,44 @@ Transition = Union[str, None] # Classes class Color: - """SFML Color Object for RGBA colors.""" - + """RGBA color representation. + + Note: + When accessing colors from UI elements (e.g., frame.fill_color), + you receive a COPY of the color. Modifying it doesn't affect the + original. To change a component: + + # This does NOT work: + frame.fill_color.r = 255 # Modifies a temporary copy + + # Do this instead: + c = frame.fill_color + c.r = 255 + frame.fill_color = c + + # Or use Animation for sub-properties: + anim = mcrfpy.Animation('fill_color.r', 255, 0.5, 'linear') + anim.start(frame) + """ + r: int g: int b: int a: int - + @overload def __init__(self) -> None: ... @overload def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ... - + def from_hex(self, hex_string: str) -> 'Color': """Create color from hex string (e.g., '#FF0000' or 'FF0000').""" ... - + def to_hex(self) -> str: """Convert color to hex string format.""" ... - + def lerp(self, other: 'Color', t: float) -> 'Color': """Linear interpolation between two colors.""" ... @@ -534,31 +552,118 @@ class Window: ... class Animation: - """Animation object for animating UI properties.""" - - target: Any - property: str - duration: float - easing: str - loop: bool - on_complete: Optional[Callable] - - def __init__(self, target: Any, property: str, start_value: Any, end_value: Any, - duration: float, easing: str = 'linear', loop: bool = False, - on_complete: Optional[Callable] = None) -> None: ... - - def start(self) -> None: - """Start the animation.""" + """Animation for interpolating UI properties over time. + + Create an animation targeting a specific property, then call start() on a + UI element to begin the animation. The AnimationManager handles updates + automatically. + + Example: + # Move a frame to x=500 over 2 seconds with easing + anim = mcrfpy.Animation('x', 500.0, 2.0, 'easeInOut') + anim.start(my_frame) + + # Animate color with completion callback + def on_done(anim, target): + print('Fade complete!') + fade = mcrfpy.Animation('fill_color.a', 0, 1.0, callback=on_done) + fade.start(my_sprite) + """ + + @property + def property(self) -> str: + """Target property name being animated (read-only).""" ... - + + @property + def duration(self) -> float: + """Animation duration in seconds (read-only).""" + ... + + @property + def elapsed(self) -> float: + """Time elapsed since animation started in seconds (read-only).""" + ... + + @property + def is_complete(self) -> bool: + """Whether the animation has finished (read-only).""" + ... + + @property + def is_delta(self) -> bool: + """Whether animation uses delta/additive mode (read-only).""" + ... + + def __init__(self, + property: str, + target: Union[float, int, Tuple[float, float], Tuple[int, int, int], Tuple[int, int, int, int], List[int], str], + duration: float, + easing: str = 'linear', + delta: bool = False, + callback: Optional[Callable[['Animation', Any], None]] = None) -> None: + """Create an animation for a UI property. + + Args: + property: Property name to animate. Common properties: + - Position/Size: 'x', 'y', 'w', 'h', 'pos', 'size' + - Appearance: 'fill_color', 'outline_color', 'opacity' + - Sprite: 'sprite_index', 'scale' + - Grid: 'center', 'zoom' + - Sub-properties: 'fill_color.r', 'fill_color.g', etc. + target: Target value. Type depends on property: + - float: For x, y, w, h, scale, opacity, zoom + - int: For sprite_index + - (r, g, b) or (r, g, b, a): For colors + - (x, y): For pos, size, center + - [int, ...]: For sprite animation sequences + - str: For text animation + duration: Animation duration in seconds. + easing: Easing function. Options: 'linear', 'easeIn', 'easeOut', + 'easeInOut', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad', + 'easeInCubic', 'easeOutCubic', 'easeInOutCubic', + 'easeInElastic', 'easeOutElastic', 'easeInOutElastic', + 'easeInBounce', 'easeOutBounce', 'easeInOutBounce', and more. + delta: If True, target value is added to start value. + callback: Function(animation, target) called on completion. + """ + ... + + def start(self, target: UIElement, conflict_mode: str = 'replace') -> None: + """Start the animation on a UI element. + + Args: + target: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity) + conflict_mode: How to handle if property is already animating: + - 'replace': Stop existing animation, start new one (default) + - 'queue': Wait for existing animation to complete + - 'error': Raise RuntimeError if property is busy + """ + ... + def update(self, dt: float) -> bool: - """Update animation, returns True if still running.""" + """Update animation by time delta. Returns True if still running. + + Note: Normally called automatically by AnimationManager. + """ ... - + def get_current_value(self) -> Any: """Get the current interpolated value.""" ... + def complete(self) -> None: + """Complete the animation immediately, jumping to final value.""" + ... + + def hasValidTarget(self) -> bool: + """Check if the animation target still exists.""" + ... + + def __repr__(self) -> str: + """Return string representation showing property, duration, and status.""" + ... + # Module-level attributes __version__: str