Shade (merchant-shade.itch.io) entity animation tests
This commit is contained in:
parent
2681cbd957
commit
6fdf7279ce
10 changed files with 1813 additions and 3 deletions
|
|
@ -581,6 +581,9 @@ PyObject* PyInit_mcrfpy()
|
|||
mcrfpydef::PyWangSetType.tp_methods = PyWangSet::methods;
|
||||
mcrfpydef::PyWangSetType.tp_getset = PyWangSet::getsetters;
|
||||
|
||||
// Texture methods (from_bytes, composite, hsl_shift)
|
||||
mcrfpydef::PyTextureType.tp_methods = PyTexture::methods;
|
||||
|
||||
// LDtk types
|
||||
mcrfpydef::PyLdtkProjectType.tp_methods = PyLdtkProject::methods;
|
||||
mcrfpydef::PyLdtkProjectType.tp_getset = PyLdtkProject::getsetters;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
#include "PyTexture.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include "PyTypeCache.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h)
|
||||
: source(filename), sprite_width(sprite_w), sprite_height(sprite_h), sheet_width(0), sheet_height(0)
|
||||
|
|
@ -90,13 +93,13 @@ sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s)
|
|||
|
||||
PyObject* PyTexture::pyObject()
|
||||
{
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture");
|
||||
PyTypeObject* type = PyTypeCache::Texture();
|
||||
if (!type) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Failed to get Texture type from module");
|
||||
PyErr_SetString(PyExc_RuntimeError, "Failed to get Texture type from cache");
|
||||
return NULL;
|
||||
}
|
||||
PyObject* obj = PyTexture::pynew(type, Py_None, Py_None);
|
||||
Py_DECREF(type); // GetAttrString returns new reference
|
||||
// PyTypeCache returns borrowed reference — no DECREF needed
|
||||
|
||||
if (!obj) {
|
||||
return NULL;
|
||||
|
|
@ -209,3 +212,298 @@ PyGetSetDef PyTexture::getsetters[] = {
|
|||
MCRF_PROPERTY(source, "Source filename path (str, read-only). The path used to load this texture."), NULL},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Texture.from_bytes(data, width, height, sprite_w, sprite_h, name) classmethod
|
||||
// ============================================================================
|
||||
PyObject* PyTexture::from_bytes(PyObject* cls, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* keywords[] = {"data", "width", "height", "sprite_width", "sprite_height", "name", nullptr};
|
||||
Py_buffer buf;
|
||||
int width, height, sprite_w, sprite_h;
|
||||
const char* name = "<generated>";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "y*iiii|s",
|
||||
const_cast<char**>(keywords),
|
||||
&buf, &width, &height, &sprite_w, &sprite_h, &name))
|
||||
return NULL;
|
||||
|
||||
Py_ssize_t expected = (Py_ssize_t)width * height * 4;
|
||||
if (buf.len != expected) {
|
||||
PyBuffer_Release(&buf);
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"Expected %zd bytes (width=%d * height=%d * 4), got %zd",
|
||||
expected, width, height, buf.len);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sf::Image img;
|
||||
img.create(width, height, (const sf::Uint8*)buf.buf);
|
||||
PyBuffer_Release(&buf);
|
||||
|
||||
auto ptex = PyTexture::from_image(img, sprite_w, sprite_h, name);
|
||||
return ptex->pyObject();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Texture.composite(layers, sprite_w, sprite_h, name) classmethod
|
||||
// ============================================================================
|
||||
PyObject* PyTexture::composite(PyObject* cls, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* keywords[] = {"layers", "sprite_width", "sprite_height", "name", nullptr};
|
||||
PyObject* layers_list;
|
||||
int sprite_w, sprite_h;
|
||||
const char* name = "<composite>";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "Oii|s",
|
||||
const_cast<char**>(keywords),
|
||||
&layers_list, &sprite_w, &sprite_h, &name))
|
||||
return NULL;
|
||||
|
||||
if (!PyList_Check(layers_list)) {
|
||||
PyErr_SetString(PyExc_TypeError, "layers must be a list of Texture objects");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_ssize_t count = PyList_Size(layers_list);
|
||||
if (count == 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "layers list must not be empty");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Validate all elements are Texture objects and collect images
|
||||
std::vector<sf::Image> images;
|
||||
unsigned int tex_w = 0, tex_h = 0;
|
||||
|
||||
// Use PyTypeCache for reliable, leak-free isinstance check
|
||||
PyTypeObject* texture_type = PyTypeCache::Texture();
|
||||
if (!texture_type) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Failed to get Texture type from cache");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (Py_ssize_t i = 0; i < count; i++) {
|
||||
PyObject* item = PyList_GetItem(layers_list, i);
|
||||
if (!PyObject_IsInstance(item, (PyObject*)texture_type)) {
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
"layers[%zd] is not a Texture object", i);
|
||||
return NULL;
|
||||
}
|
||||
auto& ptex = ((PyTextureObject*)item)->data;
|
||||
if (!ptex) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"layers[%zd] has invalid internal data", i);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sf::Image img = ptex->texture.copyToImage();
|
||||
auto size = img.getSize();
|
||||
|
||||
if (i == 0) {
|
||||
tex_w = size.x;
|
||||
tex_h = size.y;
|
||||
} else if (size.x != tex_w || size.y != tex_h) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"All layers must have same dimensions. "
|
||||
"Layer 0 is %ux%u, layer %zd is %ux%u",
|
||||
tex_w, tex_h, i, size.x, size.y);
|
||||
return NULL;
|
||||
}
|
||||
images.push_back(std::move(img));
|
||||
}
|
||||
// PyTypeCache returns borrowed reference — no DECREF needed
|
||||
|
||||
// Alpha-composite all layers bottom-to-top
|
||||
sf::Image result;
|
||||
result.create(tex_w, tex_h, sf::Color::Transparent);
|
||||
|
||||
for (unsigned int y = 0; y < tex_h; y++) {
|
||||
for (unsigned int x = 0; x < tex_w; x++) {
|
||||
// Start with first layer
|
||||
sf::Color dst = images[0].getPixel(x, y);
|
||||
|
||||
// Composite each subsequent layer on top
|
||||
for (size_t i = 1; i < images.size(); i++) {
|
||||
sf::Color src = images[i].getPixel(x, y);
|
||||
if (src.a == 0) continue;
|
||||
if (src.a == 255 || dst.a == 0) {
|
||||
dst = src;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Standard alpha compositing (Porter-Duff "over")
|
||||
float sa = src.a / 255.0f;
|
||||
float da = dst.a / 255.0f;
|
||||
float out_a = sa + da * (1.0f - sa);
|
||||
|
||||
if (out_a > 0.0f) {
|
||||
dst.r = (sf::Uint8)((src.r * sa + dst.r * da * (1.0f - sa)) / out_a);
|
||||
dst.g = (sf::Uint8)((src.g * sa + dst.g * da * (1.0f - sa)) / out_a);
|
||||
dst.b = (sf::Uint8)((src.b * sa + dst.b * da * (1.0f - sa)) / out_a);
|
||||
dst.a = (sf::Uint8)(out_a * 255.0f);
|
||||
}
|
||||
}
|
||||
result.setPixel(x, y, dst);
|
||||
}
|
||||
}
|
||||
|
||||
auto ptex = PyTexture::from_image(result, sprite_w, sprite_h, name);
|
||||
return ptex->pyObject();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HSL conversion helpers (internal)
|
||||
// ============================================================================
|
||||
namespace {
|
||||
|
||||
struct HSL {
|
||||
float h, s, l;
|
||||
};
|
||||
|
||||
HSL rgb_to_hsl(sf::Uint8 r, sf::Uint8 g, sf::Uint8 b)
|
||||
{
|
||||
float rf = r / 255.0f, gf = g / 255.0f, bf = b / 255.0f;
|
||||
float mx = std::max({rf, gf, bf});
|
||||
float mn = std::min({rf, gf, bf});
|
||||
float l = (mx + mn) / 2.0f;
|
||||
|
||||
if (mx == mn) return {0.0f, 0.0f, l};
|
||||
|
||||
float d = mx - mn;
|
||||
float s = (l > 0.5f) ? d / (2.0f - mx - mn) : d / (mx + mn);
|
||||
float h;
|
||||
if (mx == rf) {
|
||||
h = (gf - bf) / d + (gf < bf ? 6.0f : 0.0f);
|
||||
} else if (mx == gf) {
|
||||
h = (bf - rf) / d + 2.0f;
|
||||
} else {
|
||||
h = (rf - gf) / d + 4.0f;
|
||||
}
|
||||
h *= 60.0f;
|
||||
return {h, s, l};
|
||||
}
|
||||
|
||||
static float hue_to_rgb(float p, float q, float t)
|
||||
{
|
||||
if (t < 0.0f) t += 1.0f;
|
||||
if (t > 1.0f) t -= 1.0f;
|
||||
if (t < 1.0f/6.0f) return p + (q - p) * 6.0f * t;
|
||||
if (t < 1.0f/2.0f) return q;
|
||||
if (t < 2.0f/3.0f) return p + (q - p) * (2.0f/3.0f - t) * 6.0f;
|
||||
return p;
|
||||
}
|
||||
|
||||
sf::Color hsl_to_rgb(float h, float s, float l, sf::Uint8 a)
|
||||
{
|
||||
if (s <= 0.0f) {
|
||||
sf::Uint8 v = (sf::Uint8)(l * 255.0f);
|
||||
return sf::Color(v, v, v, a);
|
||||
}
|
||||
|
||||
float hn = h / 360.0f;
|
||||
float q = (l < 0.5f) ? l * (1.0f + s) : l + s - l * s;
|
||||
float p = 2.0f * l - q;
|
||||
|
||||
float r = hue_to_rgb(p, q, hn + 1.0f/3.0f);
|
||||
float g = hue_to_rgb(p, q, hn);
|
||||
float b = hue_to_rgb(p, q, hn - 1.0f/3.0f);
|
||||
|
||||
return sf::Color(
|
||||
(sf::Uint8)(r * 255.0f),
|
||||
(sf::Uint8)(g * 255.0f),
|
||||
(sf::Uint8)(b * 255.0f),
|
||||
a);
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ============================================================================
|
||||
// texture.hsl_shift(hue_shift, sat_shift, lit_shift) instance method
|
||||
// ============================================================================
|
||||
PyObject* PyTexture::hsl_shift(PyTextureObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* keywords[] = {"hue_shift", "sat_shift", "lit_shift", nullptr};
|
||||
float hue_shift, sat_shift = 0.0f, lit_shift = 0.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "f|ff",
|
||||
const_cast<char**>(keywords),
|
||||
&hue_shift, &sat_shift, &lit_shift))
|
||||
return NULL;
|
||||
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Texture has invalid internal data");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sf::Image img = self->data->texture.copyToImage();
|
||||
auto size = img.getSize();
|
||||
|
||||
for (unsigned int y = 0; y < size.y; y++) {
|
||||
for (unsigned int x = 0; x < size.x; x++) {
|
||||
sf::Color px = img.getPixel(x, y);
|
||||
if (px.a == 0) continue; // skip transparent
|
||||
|
||||
HSL hsl = rgb_to_hsl(px.r, px.g, px.b);
|
||||
|
||||
// Apply shifts
|
||||
hsl.h = std::fmod(hsl.h + hue_shift, 360.0f);
|
||||
if (hsl.h < 0.0f) hsl.h += 360.0f;
|
||||
|
||||
hsl.s = std::clamp(hsl.s + sat_shift, 0.0f, 1.0f);
|
||||
hsl.l = std::clamp(hsl.l + lit_shift, 0.0f, 1.0f);
|
||||
|
||||
img.setPixel(x, y, hsl_to_rgb(hsl.h, hsl.s, hsl.l, px.a));
|
||||
}
|
||||
}
|
||||
|
||||
auto ptex = PyTexture::from_image(img,
|
||||
self->data->sprite_width, self->data->sprite_height,
|
||||
self->data->source + "+hsl");
|
||||
return ptex->pyObject();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Methods table
|
||||
// ============================================================================
|
||||
PyMethodDef PyTexture::methods[] = {
|
||||
{"from_bytes", (PyCFunction)PyTexture::from_bytes, METH_VARARGS | METH_KEYWORDS | METH_CLASS,
|
||||
MCRF_METHOD(Texture, from_bytes,
|
||||
MCRF_SIG("(data: bytes, width: int, height: int, sprite_width: int, sprite_height: int, name: str = '<generated>')", "Texture"),
|
||||
MCRF_DESC("Create a Texture from raw RGBA pixel data."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("data", "Raw RGBA bytes (length must equal width * height * 4)")
|
||||
MCRF_ARG("width", "Image width in pixels")
|
||||
MCRF_ARG("height", "Image height in pixels")
|
||||
MCRF_ARG("sprite_width", "Width of each sprite cell")
|
||||
MCRF_ARG("sprite_height", "Height of each sprite cell")
|
||||
MCRF_ARG("name", "Optional name for the texture (default: '<generated>')")
|
||||
MCRF_RETURNS("Texture: New texture containing the pixel data")
|
||||
MCRF_RAISES("ValueError", "If data length does not match width * height * 4")
|
||||
MCRF_NOTE("This is a class method. Useful for procedurally generated textures.")
|
||||
)},
|
||||
{"composite", (PyCFunction)PyTexture::composite, METH_VARARGS | METH_KEYWORDS | METH_CLASS,
|
||||
MCRF_METHOD(Texture, composite,
|
||||
MCRF_SIG("(layers: list[Texture], sprite_width: int, sprite_height: int, name: str = '<composite>')", "Texture"),
|
||||
MCRF_DESC("Alpha-composite multiple texture layers into a single texture."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("layers", "List of Texture objects, composited bottom-to-top")
|
||||
MCRF_ARG("sprite_width", "Width of each sprite cell in the result")
|
||||
MCRF_ARG("sprite_height", "Height of each sprite cell in the result")
|
||||
MCRF_ARG("name", "Optional name for the composite texture")
|
||||
MCRF_RETURNS("Texture: New texture with all layers composited")
|
||||
MCRF_RAISES("ValueError", "If layers have different dimensions or list is empty")
|
||||
MCRF_NOTE("This is a class method. Uses Porter-Duff 'over' alpha compositing.")
|
||||
)},
|
||||
{"hsl_shift", (PyCFunction)PyTexture::hsl_shift, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Texture, hsl_shift,
|
||||
MCRF_SIG("(hue_shift: float, sat_shift: float = 0.0, lit_shift: float = 0.0)", "Texture"),
|
||||
MCRF_DESC("Create a new texture with HSL color adjustments applied."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("hue_shift", "Hue rotation in degrees [0.0, 360.0)")
|
||||
MCRF_ARG("sat_shift", "Saturation adjustment [-1.0, 1.0] (default 0.0)")
|
||||
MCRF_ARG("lit_shift", "Lightness adjustment [-1.0, 1.0] (default 0.0)")
|
||||
MCRF_RETURNS("Texture: New texture with color-shifted pixels")
|
||||
MCRF_NOTE("Preserves alpha channel. Skips fully transparent pixels.")
|
||||
)},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
|
|
|||
|
|
@ -51,6 +51,12 @@ public:
|
|||
static PyObject* get_source(PyTextureObject* self, void* closure);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
|
||||
// Methods (classmethods and instance methods)
|
||||
static PyObject* from_bytes(PyObject* cls, PyObject* args, PyObject* kwds);
|
||||
static PyObject* composite(PyObject* cls, PyObject* args, PyObject* kwds);
|
||||
static PyObject* hsl_shift(PyTextureObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyMethodDef methods[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue