Easing functions as enum

This commit is contained in:
John McCardle 2026-01-04 12:59:28 -05:00
commit d878c8684d
4 changed files with 278 additions and 9 deletions

View file

@ -10,6 +10,7 @@
#include "PySceneObject.h"
#include "PyFOV.h"
#include "PyTransition.h"
#include "PyEasing.h"
#include "PySound.h"
#include "PyMusic.h"
#include "PyKeyboard.h"
@ -429,6 +430,13 @@ PyObject* PyInit_mcrfpy()
// Note: default_transition and default_transition_duration are handled via
// mcrfpy_module_getattr/setattro using PyTransition::default_transition/default_duration
// Add Easing enum class (uses Python's IntEnum)
PyObject* easing_class = PyEasing::create_enum_class(m);
if (!easing_class) {
// If enum creation fails, continue without it (non-fatal)
PyErr_Clear();
}
// Add automation submodule
PyObject* automation_module = McRFPy_Automation::init_automation_module();
if (automation_module != NULL) {

View file

@ -1,6 +1,7 @@
#include "PyAnimation.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
#include "PyEasing.h"
#include "UIDrawable.h"
#include "UIFrame.h"
#include "UICaption.h"
@ -20,16 +21,16 @@ 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};
const char* property_name;
PyObject* target_value;
float duration;
const char* easing_name = "linear";
PyObject* easing_arg = Py_None;
int delta = 0;
PyObject* callback = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast<char**>(keywords),
&property_name, &target_value, &duration, &easing_name, &delta, &callback)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpO", const_cast<char**>(keywords),
&property_name, &target_value, &duration, &easing_arg, &delta, &callback)) {
return -1;
}
@ -98,10 +99,13 @@ int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
PyErr_SetString(PyExc_TypeError, "Target value must be float, int, list, tuple, or string");
return -1;
}
// Get easing function
EasingFunction easingFunc = EasingFunctions::getByName(easing_name);
// Get easing function from argument (enum, string, int, or None)
EasingFunction easingFunc;
if (!PyEasing::from_arg(easing_arg, &easingFunc, nullptr)) {
return -1; // Error already set by from_arg
}
// Create the Animation
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);

228
src/PyEasing.cpp Normal file
View file

@ -0,0 +1,228 @@
#include "PyEasing.h"
#include "McRFPy_API.h"
// Static storage for cached enum class reference
PyObject* PyEasing::easing_enum_class = nullptr;
// Easing function table - maps enum value to function and name
struct EasingEntry {
const char* name;
int value;
EasingFunction func;
};
static const EasingEntry easing_table[] = {
{"LINEAR", 0, EasingFunctions::linear},
{"EASE_IN", 1, EasingFunctions::easeIn},
{"EASE_OUT", 2, EasingFunctions::easeOut},
{"EASE_IN_OUT", 3, EasingFunctions::easeInOut},
{"EASE_IN_QUAD", 4, EasingFunctions::easeInQuad},
{"EASE_OUT_QUAD", 5, EasingFunctions::easeOutQuad},
{"EASE_IN_OUT_QUAD", 6, EasingFunctions::easeInOutQuad},
{"EASE_IN_CUBIC", 7, EasingFunctions::easeInCubic},
{"EASE_OUT_CUBIC", 8, EasingFunctions::easeOutCubic},
{"EASE_IN_OUT_CUBIC", 9, EasingFunctions::easeInOutCubic},
{"EASE_IN_QUART", 10, EasingFunctions::easeInQuart},
{"EASE_OUT_QUART", 11, EasingFunctions::easeOutQuart},
{"EASE_IN_OUT_QUART", 12, EasingFunctions::easeInOutQuart},
{"EASE_IN_SINE", 13, EasingFunctions::easeInSine},
{"EASE_OUT_SINE", 14, EasingFunctions::easeOutSine},
{"EASE_IN_OUT_SINE", 15, EasingFunctions::easeInOutSine},
{"EASE_IN_EXPO", 16, EasingFunctions::easeInExpo},
{"EASE_OUT_EXPO", 17, EasingFunctions::easeOutExpo},
{"EASE_IN_OUT_EXPO", 18, EasingFunctions::easeInOutExpo},
{"EASE_IN_CIRC", 19, EasingFunctions::easeInCirc},
{"EASE_OUT_CIRC", 20, EasingFunctions::easeOutCirc},
{"EASE_IN_OUT_CIRC", 21, EasingFunctions::easeInOutCirc},
{"EASE_IN_ELASTIC", 22, EasingFunctions::easeInElastic},
{"EASE_OUT_ELASTIC", 23, EasingFunctions::easeOutElastic},
{"EASE_IN_OUT_ELASTIC", 24, EasingFunctions::easeInOutElastic},
{"EASE_IN_BACK", 25, EasingFunctions::easeInBack},
{"EASE_OUT_BACK", 26, EasingFunctions::easeOutBack},
{"EASE_IN_OUT_BACK", 27, EasingFunctions::easeInOutBack},
{"EASE_IN_BOUNCE", 28, EasingFunctions::easeInBounce},
{"EASE_OUT_BOUNCE", 29, EasingFunctions::easeOutBounce},
{"EASE_IN_OUT_BOUNCE", 30, EasingFunctions::easeInOutBounce},
};
// Old string names (for backwards compatibility)
static const char* legacy_names[] = {
"linear", "easeIn", "easeOut", "easeInOut",
"easeInQuad", "easeOutQuad", "easeInOutQuad",
"easeInCubic", "easeOutCubic", "easeInOutCubic",
"easeInQuart", "easeOutQuart", "easeInOutQuart",
"easeInSine", "easeOutSine", "easeInOutSine",
"easeInExpo", "easeOutExpo", "easeInOutExpo",
"easeInCirc", "easeOutCirc", "easeInOutCirc",
"easeInElastic", "easeOutElastic", "easeInOutElastic",
"easeInBack", "easeOutBack", "easeInOutBack",
"easeInBounce", "easeOutBounce", "easeInOutBounce"
};
static const int NUM_EASING_ENTRIES = sizeof(easing_table) / sizeof(easing_table[0]);
const char* PyEasing::easing_name(int value) {
if (value >= 0 && value < NUM_EASING_ENTRIES) {
return easing_table[value].name;
}
return "LINEAR";
}
PyObject* PyEasing::create_enum_class(PyObject* module) {
// Import IntEnum from enum module
PyObject* enum_module = PyImport_ImportModule("enum");
if (!enum_module) {
return NULL;
}
PyObject* int_enum = PyObject_GetAttrString(enum_module, "IntEnum");
Py_DECREF(enum_module);
if (!int_enum) {
return NULL;
}
// Create dict of enum members
PyObject* members = PyDict_New();
if (!members) {
Py_DECREF(int_enum);
return NULL;
}
// Add all easing function members
for (int i = 0; i < NUM_EASING_ENTRIES; i++) {
PyObject* value = PyLong_FromLong(easing_table[i].value);
if (!value) {
Py_DECREF(members);
Py_DECREF(int_enum);
return NULL;
}
if (PyDict_SetItemString(members, easing_table[i].name, value) < 0) {
Py_DECREF(value);
Py_DECREF(members);
Py_DECREF(int_enum);
return NULL;
}
Py_DECREF(value);
}
// Call IntEnum("Easing", members) to create the enum class
PyObject* name = PyUnicode_FromString("Easing");
if (!name) {
Py_DECREF(members);
Py_DECREF(int_enum);
return NULL;
}
// IntEnum(name, members) using functional API
PyObject* args = PyTuple_Pack(2, name, members);
Py_DECREF(name);
Py_DECREF(members);
if (!args) {
Py_DECREF(int_enum);
return NULL;
}
PyObject* easing_class = PyObject_Call(int_enum, args, NULL);
Py_DECREF(args);
Py_DECREF(int_enum);
if (!easing_class) {
return NULL;
}
// Cache the reference for fast type checking
easing_enum_class = easing_class;
Py_INCREF(easing_enum_class);
// Add to module
if (PyModule_AddObject(module, "Easing", easing_class) < 0) {
Py_DECREF(easing_class);
easing_enum_class = nullptr;
return NULL;
}
return easing_class;
}
int PyEasing::from_arg(PyObject* arg, EasingFunction* out_func, bool* was_none) {
if (was_none) *was_none = false;
// Accept None -> default to linear
if (arg == Py_None) {
if (was_none) *was_none = true;
*out_func = EasingFunctions::linear;
return 1;
}
// Accept Easing enum member (check if it's an instance of our enum)
if (easing_enum_class && PyObject_IsInstance(arg, easing_enum_class)) {
// IntEnum members have a 'value' attribute
PyObject* value = PyObject_GetAttrString(arg, "value");
if (!value) {
return 0;
}
long val = PyLong_AsLong(value);
Py_DECREF(value);
if (val == -1 && PyErr_Occurred()) {
return 0;
}
if (val >= 0 && val < NUM_EASING_ENTRIES) {
*out_func = easing_table[val].func;
return 1;
}
PyErr_Format(PyExc_ValueError,
"Invalid Easing value: %ld. Must be 0-%d.", val, NUM_EASING_ENTRIES - 1);
return 0;
}
// Accept int (for backwards compatibility and direct enum value access)
if (PyLong_Check(arg)) {
long val = PyLong_AsLong(arg);
if (val == -1 && PyErr_Occurred()) {
return 0;
}
if (val >= 0 && val < NUM_EASING_ENTRIES) {
*out_func = easing_table[val].func;
return 1;
}
PyErr_Format(PyExc_ValueError,
"Invalid easing value: %ld. Must be 0-%d or use mcrfpy.Easing enum.",
val, NUM_EASING_ENTRIES - 1);
return 0;
}
// Accept string (for backwards compatibility)
if (PyUnicode_Check(arg)) {
const char* name = PyUnicode_AsUTF8(arg);
if (!name) {
return 0;
}
// Check legacy string names first
for (int i = 0; i < NUM_EASING_ENTRIES; i++) {
if (strcmp(name, legacy_names[i]) == 0) {
*out_func = easing_table[i].func;
return 1;
}
}
// Also check enum-style names (EASE_IN_OUT, etc.)
for (int i = 0; i < NUM_EASING_ENTRIES; i++) {
if (strcmp(name, easing_table[i].name) == 0) {
*out_func = easing_table[i].func;
return 1;
}
}
// Build error message with available options
PyErr_Format(PyExc_ValueError,
"Unknown easing function: '%s'. Use mcrfpy.Easing enum (e.g., Easing.EASE_IN_OUT) "
"or legacy string names: 'linear', 'easeIn', 'easeOut', 'easeInOut', 'easeInQuad', etc.",
name);
return 0;
}
PyErr_SetString(PyExc_TypeError,
"Easing must be mcrfpy.Easing enum member, string, int, or None");
return 0;
}

29
src/PyEasing.h Normal file
View file

@ -0,0 +1,29 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "Animation.h"
// Module-level Easing enum class (created at runtime using Python's IntEnum)
// Stored as a module attribute: mcrfpy.Easing
class PyEasing {
public:
// Create the Easing enum class and add to module
// Returns the enum class (new reference), or NULL on error
static PyObject* create_enum_class(PyObject* module);
// Helper to extract easing function from Python arg
// Accepts Easing enum, string (for backwards compatibility), int, or None
// Returns 1 on success, 0 on error (with exception set)
// If arg is None, sets *out_func to linear and sets *was_none to true
static int from_arg(PyObject* arg, EasingFunction* out_func, bool* was_none = nullptr);
// Convert easing enum value to string name
static const char* easing_name(int value);
// Cached reference to the Easing enum class for fast type checking
static PyObject* easing_enum_class;
// Number of easing functions
static const int NUM_EASING_FUNCTIONS = 32;
};