Skip to content

Commit

Permalink
invlerp and remap implementation (#2654)
Browse files Browse the repository at this point in the history
* invlerp & remap impl

---------

Co-authored-by: Dan Lawrence <danintheshed@gmail.com>
Co-authored-by: Ankith <itsankith26@gmail.com>
  • Loading branch information
3 people authored Jun 2, 2024
1 parent 92ee77a commit 19a17c9
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 0 deletions.
2 changes: 2 additions & 0 deletions buildconfig/stubs/pygame/math.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@ class Vector3(_GenericVector):


def lerp(a: float, b: float, weight: float, do_clamp: bool = True, /) -> float: ...
def invlerp(a: float, b: float, weight: float, /) -> float: ...
def remap(i_min: float, i_max: float, o_min: float, o_max: float, value: float, /) -> float: ...
def smoothstep(a: float, b: float, weight: float, /) -> float: ...


Expand Down
53 changes: 53 additions & 0 deletions docs/reST/ref/math.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,37 @@ Multiple coordinates can be set using slices or swizzling

.. ## math.lerp ##
.. function:: invlerp

| :sl:`returns value inverse interpolated between a and b`
| :sg:`invlerp(a, b, value, /) -> float`
Returns a number which is an inverse interpolation between ``a``
and ``b``. The third parameter ``value`` is the result of the linear interpolation
between a and b with a certain coefficient. In other words, this coefficient
will be the result of this function.
If ``b - a`` is equal to 0, it raises a ``ZeroDivisionError``.

The formula is:

``(v - a)/(b - a)``.

This is an example explaining what is above :

.. code-block:: python
> a = 10
> b = 20
> pygame.math.invlerp(10, 20, 11.5)
> 0.15
> pygame.math.lerp(10, 20, 0.15)
> 11.5
.. versionadded:: 2.5.0

.. ## math.invlerp ##
.. function:: smoothstep

| :sl:`returns value smoothly interpolated between a and b.`
Expand All @@ -102,6 +133,28 @@ Multiple coordinates can be set using slices or swizzling

.. ## math.smoothstep ##
.. function:: remap

| :sl:`remaps value from i_range to o_range`
| :sg:`remap(i_min, i_max, o_min, o_max, value, /) -> float`
Returns a number which is the value remapped from ``i_range`` to
``o_range``.
If ``i_max - i_min`` is equal to 0, it raises a ``ZeroDivisionError``.

Example:

.. code-block:: python
> value = 50
> pygame.math.remap(0, 100, 0, 200, value)
> 100.0
.. versionadded:: 2.5.0

.. ## math.remap ##
.. class:: Vector2

| :sl:`a 2-Dimensional Vector`
Expand Down
2 changes: 2 additions & 0 deletions src_c/doc/math_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
#define DOC_MATH "pygame module for vector classes"
#define DOC_MATH_CLAMP "clamp(value, min, max, /) -> float\nreturns value clamped to min and max."
#define DOC_MATH_LERP "lerp(a, b, value, do_clamp=True, /) -> float\nreturns value linearly interpolated between a and b"
#define DOC_MATH_INVLERP "invlerp(a, b, value, /) -> float\nreturns value inverse interpolated between a and b"
#define DOC_MATH_SMOOTHSTEP "smoothstep(a, b, value, /) -> float\nreturns value smoothly interpolated between a and b."
#define DOC_MATH_REMAP "remap(i_min, i_max, o_min, o_max, value, /) -> float\nremaps value from i_range to o_range"
#define DOC_MATH_VECTOR2 "Vector2() -> Vector2(0, 0)\nVector2(int) -> Vector2\nVector2(float) -> Vector2\nVector2(Vector2) -> Vector2\nVector2(x, y) -> Vector2\nVector2((x, y)) -> Vector2\na 2-Dimensional Vector"
#define DOC_MATH_VECTOR2_DOT "dot(Vector2, /) -> float\ncalculates the dot- or scalar-product with the other vector"
#define DOC_MATH_VECTOR2_CROSS "cross(Vector2, /) -> float\ncalculates the cross- or vector-product"
Expand Down
80 changes: 80 additions & 0 deletions src_c/math.c
Original file line number Diff line number Diff line change
Expand Up @@ -4192,6 +4192,18 @@ vector_elementwise(pgVector *vec, PyObject *_null)
return (PyObject *)proxy;
}

inline double
lerp(double a, double b, double v)
{
return a + (b - a) * v;
}

inline double
invlerp(double a, double b, double v)
{
return (v - a) / (b - a);
}

static PyObject *
math_clamp(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
{
Expand Down Expand Up @@ -4233,6 +4245,72 @@ math_clamp(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
return value;
}

#define RAISE_ARG_TYPE_ERROR(var) \
if (PyErr_Occurred()) { \
return RAISE(PyExc_TypeError, \
"The argument '" var "' must be a real number"); \
}

static PyObject *
math_invlerp(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
{
if (nargs != 3)
return RAISE(PyExc_TypeError,
"invlerp requires exactly 3 numeric arguments");

double a = PyFloat_AsDouble(args[0]);
RAISE_ARG_TYPE_ERROR("a")
double b = PyFloat_AsDouble(args[1]);
RAISE_ARG_TYPE_ERROR("b")
double t = PyFloat_AsDouble(args[2]);
RAISE_ARG_TYPE_ERROR("value")

if (PyErr_Occurred())
return RAISE(PyExc_ValueError,
"invalid argument values passed to invlerp, numbers "
"might be too small or too big");

if (b - a == 0)
return RAISE(PyExc_ValueError,
"the result of b - a needs to be different from zero");

return PyFloat_FromDouble(invlerp(a, b, t));
}

#

static PyObject *
math_remap(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
{
if (nargs != 5)
return RAISE(PyExc_TypeError,
"remap requires exactly 5 numeric arguments");

PyObject *i_min = args[0];
PyObject *i_max = args[1];
PyObject *o_min = args[2];
PyObject *o_max = args[3];
PyObject *value = args[4];

double v = PyFloat_AsDouble(value);
RAISE_ARG_TYPE_ERROR("value")
double a = PyFloat_AsDouble(i_min);
RAISE_ARG_TYPE_ERROR("i_min")
double b = PyFloat_AsDouble(i_max);
RAISE_ARG_TYPE_ERROR("i_max")
double c = PyFloat_AsDouble(o_min);
RAISE_ARG_TYPE_ERROR("o_min")
double d = PyFloat_AsDouble(o_max);
RAISE_ARG_TYPE_ERROR("o_max")

if (b - a == 0)
return RAISE(
PyExc_ValueError,
"the result of i_max - i_min needs to be different from zero");

return PyFloat_FromDouble(lerp(c, d, invlerp(a, b, v)));
}

static PyObject *
math_lerp(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
{
Expand Down Expand Up @@ -4343,6 +4421,8 @@ math_disable_swizzling(pgVector *self, PyObject *_null)
static PyMethodDef _math_methods[] = {
{"clamp", (PyCFunction)math_clamp, METH_FASTCALL, DOC_MATH_CLAMP},
{"lerp", (PyCFunction)math_lerp, METH_FASTCALL, DOC_MATH_LERP},
{"invlerp", (PyCFunction)math_invlerp, METH_FASTCALL, DOC_MATH_INVLERP},
{"remap", (PyCFunction)math_remap, METH_FASTCALL, DOC_MATH_REMAP},
{"smoothstep", (PyCFunction)math_smoothstep, METH_FASTCALL,
DOC_MATH_SMOOTHSTEP},
{"enable_swizzling", (PyCFunction)math_enable_swizzling, METH_NOARGS,
Expand Down
101 changes: 101 additions & 0 deletions test/math_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,107 @@ def test_lerp(self):
b = 2
pygame.math.lerp(a, b, Vector2(0, 0))

def test_invlerp(self):
a = 0.0
b = 10.0
self.assertEqual(pygame.math.invlerp(a, b, 5.0), 0.5)

a = 0.0
b = 10.0
self.assertEqual(pygame.math.invlerp(a, b, 0.1), 0.01)

a = -10.0
b = 10.0
self.assertEqual(pygame.math.invlerp(a, b, 0.5), 0.525)

a = -10.0
b = 10.0
self.assertEqual(pygame.math.invlerp(a, b, 1.5), 0.575)

a = 0.0
b = 100.0
self.assertEqual(pygame.math.invlerp(a, b, 0.25), 0.0025)

with self.assertRaises(TypeError):
a = Vector2(0, 0)
b = Vector2(10.0, 10.0)
pygame.math.invlerp(a, b, 0.5)

with self.assertRaises(TypeError):
a = 1
b = 2
pygame.math.invlerp(a, b, Vector2(0, 0))

with self.assertRaises(ValueError):
a = 5
b = 5
pygame.math.invlerp(a, b, 5)

with self.assertRaises(TypeError):
a = 12**300
b = 11**30
pygame.math.invlerp(a, b, 1)

def test_remap(self):
a = 0.0
b = 10.0
c = 0.0
d = 100.0
self.assertEqual(pygame.math.remap(a, b, c, d, 1.0), 10.0)

a = 0.0
b = 10.0
c = 0.0
d = 100.0
self.assertEqual(pygame.math.remap(a, b, c, d, -1.0), -10.0)

a = -10.0
b = 10.0
c = -20.0
d = 20.0
self.assertEqual(pygame.math.remap(a, b, c, d, 0.0), 0.0)

a = -10.0
b = 10.0
c = 10.0
d = 110.0
self.assertEqual(pygame.math.remap(a, b, c, d, -8.0), 20.0)

with self.assertRaises(TypeError):
a = Vector2(0, 0)
b = "fish"
c = "durk"
d = Vector2(100, 100)
pygame.math.remap(a, b, c, d, 10)

with self.assertRaises(TypeError):
a = 1
b = 2
c = 10
d = 20
pygame.math.remap(a, b, c, d, Vector2(0, 0))

with self.assertRaises(ValueError):
a = 5
b = 5
c = 0
d = 100
pygame.math.remap(a, b, c, d, 10)

with self.assertRaises(TypeError):
a = 12**300
b = 11**30
c = 20
d = 30
pygame.math.remap(a, b, c, d, 100 * 50)

with self.assertRaises(TypeError):
a = 12j
b = 11j
c = 10j
d = 9j
pygame.math.remap(a, b, c, d, 50j)

def test_smoothstep(self):
a = 0.0
b = 10.0
Expand Down

0 comments on commit 19a17c9

Please sign in to comment.