feat(core): add tween_curves — Penner easing functions
16 easing curves (linear, quad, cubic, expo, elastic, bounce in/out/inOut) header-mostly so the compiler inlines on hot paths. Pure, no I/O, no state. Includes: - tween::apply(Ease, t) dispatcher for data-driven uses (timeline keyframe.ease, dropdown selection) - tween::name(Ease) for UI labels - tween::ease_count for iteration Tested values documented in tween_curves.md.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
#include "core/tween_curves.h"
|
||||
|
||||
namespace fn::tween {
|
||||
|
||||
float apply(Ease e, float t) {
|
||||
switch (e) {
|
||||
case Ease::Linear: return linear(t);
|
||||
case Ease::InQuad: return in_quad(t);
|
||||
case Ease::OutQuad: return out_quad(t);
|
||||
case Ease::InOutQuad: return in_out_quad(t);
|
||||
case Ease::InCubic: return in_cubic(t);
|
||||
case Ease::OutCubic: return out_cubic(t);
|
||||
case Ease::InOutCubic: return in_out_cubic(t);
|
||||
case Ease::InExpo: return in_expo(t);
|
||||
case Ease::OutExpo: return out_expo(t);
|
||||
case Ease::InOutExpo: return in_out_expo(t);
|
||||
case Ease::InElastic: return in_elastic(t);
|
||||
case Ease::OutElastic: return out_elastic(t);
|
||||
case Ease::InOutElastic: return in_out_elastic(t);
|
||||
case Ease::InBounce: return in_bounce(t);
|
||||
case Ease::OutBounce: return out_bounce(t);
|
||||
case Ease::InOutBounce: return in_out_bounce(t);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
const char* name(Ease e) {
|
||||
switch (e) {
|
||||
case Ease::Linear: return "Linear";
|
||||
case Ease::InQuad: return "InQuad";
|
||||
case Ease::OutQuad: return "OutQuad";
|
||||
case Ease::InOutQuad: return "InOutQuad";
|
||||
case Ease::InCubic: return "InCubic";
|
||||
case Ease::OutCubic: return "OutCubic";
|
||||
case Ease::InOutCubic: return "InOutCubic";
|
||||
case Ease::InExpo: return "InExpo";
|
||||
case Ease::OutExpo: return "OutExpo";
|
||||
case Ease::InOutExpo: return "InOutExpo";
|
||||
case Ease::InElastic: return "InElastic";
|
||||
case Ease::OutElastic: return "OutElastic";
|
||||
case Ease::InOutElastic: return "InOutElastic";
|
||||
case Ease::InBounce: return "InBounce";
|
||||
case Ease::OutBounce: return "OutBounce";
|
||||
case Ease::InOutBounce: return "InOutBounce";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
} // namespace fn::tween
|
||||
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
// tween_curves — easing functions (Penner) for animations and interpolations.
|
||||
// Header-mostly so the compiler inlines on hot paths. Pure: no I/O, no state.
|
||||
//
|
||||
// Reference: easings.net (Penner formulas).
|
||||
// Conventions:
|
||||
// - All functions take t in [0,1] and return f(t).
|
||||
// - linear, *_quad, *_cubic, *_expo: f(0)=0, f(1)=1 exactly.
|
||||
// - elastic / bounce: f(0)=0, f(1)=1 but f overshoots/undershoots in between.
|
||||
//
|
||||
// Usage:
|
||||
// #include "core/tween_curves.h"
|
||||
// float y = fn::tween::out_cubic(t);
|
||||
// float y2 = fn::tween::apply(fn::tween::Ease::OutElastic, t);
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace fn::tween {
|
||||
|
||||
enum class Ease {
|
||||
Linear,
|
||||
InQuad, OutQuad, InOutQuad,
|
||||
InCubic, OutCubic, InOutCubic,
|
||||
InExpo, OutExpo, InOutExpo,
|
||||
InElastic, OutElastic, InOutElastic,
|
||||
InBounce, OutBounce, InOutBounce,
|
||||
};
|
||||
|
||||
// --- Linear -----------------------------------------------------------------
|
||||
|
||||
inline float linear(float t) { return t; }
|
||||
|
||||
// --- Quadratic (t^2) --------------------------------------------------------
|
||||
|
||||
inline float in_quad(float t) { return t * t; }
|
||||
inline float out_quad(float t) { return 1.0f - (1.0f - t) * (1.0f - t); }
|
||||
inline float in_out_quad(float t) {
|
||||
return (t < 0.5f) ? (2.0f * t * t)
|
||||
: (1.0f - 0.5f * (2.0f - 2.0f * t) * (2.0f - 2.0f * t));
|
||||
}
|
||||
|
||||
// --- Cubic (t^3) ------------------------------------------------------------
|
||||
|
||||
inline float in_cubic(float t) { return t * t * t; }
|
||||
inline float out_cubic(float t) { float u = 1.0f - t; return 1.0f - u * u * u; }
|
||||
inline float in_out_cubic(float t) {
|
||||
if (t < 0.5f) return 4.0f * t * t * t;
|
||||
float u = -2.0f * t + 2.0f;
|
||||
return 1.0f - 0.5f * u * u * u;
|
||||
}
|
||||
|
||||
// --- Exponential ------------------------------------------------------------
|
||||
|
||||
inline float in_expo(float t) {
|
||||
return (t <= 0.0f) ? 0.0f : std::pow(2.0f, 10.0f * t - 10.0f);
|
||||
}
|
||||
inline float out_expo(float t) {
|
||||
return (t >= 1.0f) ? 1.0f : 1.0f - std::pow(2.0f, -10.0f * t);
|
||||
}
|
||||
inline float in_out_expo(float t) {
|
||||
if (t <= 0.0f) return 0.0f;
|
||||
if (t >= 1.0f) return 1.0f;
|
||||
return (t < 0.5f) ? 0.5f * std::pow(2.0f, 20.0f * t - 10.0f)
|
||||
: 1.0f - 0.5f * std::pow(2.0f, -20.0f * t + 10.0f);
|
||||
}
|
||||
|
||||
// --- Elastic (oscillation) --------------------------------------------------
|
||||
|
||||
inline float in_elastic(float t) {
|
||||
if (t <= 0.0f) return 0.0f;
|
||||
if (t >= 1.0f) return 1.0f;
|
||||
constexpr float c4 = 6.283185307179586f / 3.0f; // 2*pi/3
|
||||
return -std::pow(2.0f, 10.0f * t - 10.0f) * std::sin((t * 10.0f - 10.75f) * c4);
|
||||
}
|
||||
inline float out_elastic(float t) {
|
||||
if (t <= 0.0f) return 0.0f;
|
||||
if (t >= 1.0f) return 1.0f;
|
||||
constexpr float c4 = 6.283185307179586f / 3.0f;
|
||||
return std::pow(2.0f, -10.0f * t) * std::sin((t * 10.0f - 0.75f) * c4) + 1.0f;
|
||||
}
|
||||
inline float in_out_elastic(float t) {
|
||||
if (t <= 0.0f) return 0.0f;
|
||||
if (t >= 1.0f) return 1.0f;
|
||||
constexpr float c5 = 6.283185307179586f / 4.5f; // 2*pi/4.5
|
||||
if (t < 0.5f) {
|
||||
return -0.5f * std::pow(2.0f, 20.0f * t - 10.0f) * std::sin((20.0f * t - 11.125f) * c5);
|
||||
}
|
||||
return 0.5f * std::pow(2.0f, -20.0f * t + 10.0f) * std::sin((20.0f * t - 11.125f) * c5) + 1.0f;
|
||||
}
|
||||
|
||||
// --- Bounce -----------------------------------------------------------------
|
||||
|
||||
inline float out_bounce(float t) {
|
||||
constexpr float n1 = 7.5625f;
|
||||
constexpr float d1 = 2.75f;
|
||||
if (t < 1.0f / d1) {
|
||||
return n1 * t * t;
|
||||
} else if (t < 2.0f / d1) {
|
||||
float u = t - 1.5f / d1;
|
||||
return n1 * u * u + 0.75f;
|
||||
} else if (t < 2.5f / d1) {
|
||||
float u = t - 2.25f / d1;
|
||||
return n1 * u * u + 0.9375f;
|
||||
} else {
|
||||
float u = t - 2.625f / d1;
|
||||
return n1 * u * u + 0.984375f;
|
||||
}
|
||||
}
|
||||
inline float in_bounce(float t) { return 1.0f - out_bounce(1.0f - t); }
|
||||
inline float in_out_bounce(float t) {
|
||||
return (t < 0.5f) ? 0.5f * (1.0f - out_bounce(1.0f - 2.0f * t))
|
||||
: 0.5f * (1.0f + out_bounce(2.0f * t - 1.0f));
|
||||
}
|
||||
|
||||
// --- Dispatch ---------------------------------------------------------------
|
||||
|
||||
// Apply easing by enum. Useful when the curve is data-driven (Keyframe.ease,
|
||||
// dropdown selection in UI).
|
||||
float apply(Ease e, float t);
|
||||
|
||||
// Human-readable name for UI dropdowns.
|
||||
const char* name(Ease e);
|
||||
|
||||
// Total number of easing modes (for iterating dropdowns).
|
||||
constexpr int ease_count = 16;
|
||||
|
||||
} // namespace fn::tween
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
name: tween_curves
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "float fn::tween::apply(fn::tween::Ease e, float t) + ~16 named easing functions (linear, in_quad, out_quad, ...)"
|
||||
description: "Set de funciones de easing puras (Penner) para animaciones e interpolaciones: linear, quad, cubic, expo, elastic, bounce, en variantes in/out/inOut. Header-mostly: el compilador inlinea cada curva en el sitio de llamada. Sin estado, sin I/O."
|
||||
tags: [animation, easing, tween, penner, math, header-only]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/core/tween_curves.cpp"
|
||||
framework: ""
|
||||
params:
|
||||
- name: e
|
||||
desc: "Curva a aplicar (enum Ease): Linear, InQuad, OutQuad, InOutQuad, InCubic, OutCubic, InOutCubic, InExpo, OutExpo, InOutExpo, InElastic, OutElastic, InOutElastic, InBounce, OutBounce, InOutBounce"
|
||||
- name: t
|
||||
desc: "Progreso normalizado en [0,1]. Para t<0 o t>1 algunas curvas extrapolan razonablemente, otras saturan."
|
||||
output: "f(t) — valor de la curva en t. Para curvas no oscilantes f(0)=0 y f(1)=1. Para elastic/bounce f(0)=0 y f(1)=1 pero f puede salir del rango [0,1] en valores intermedios."
|
||||
---
|
||||
|
||||
# tween_curves
|
||||
|
||||
Funciones de easing al estilo Penner. Permiten convertir un progreso lineal `t in [0,1]` en una curva temporal con feel concreto (acelerar, frenar, rebotar, oscilar). Son el ladrillo basico de toda animacion: timeline interpolation, transiciones UI, smooth lerp de uniforms, ramps de color.
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace fn::tween {
|
||||
|
||||
enum class Ease {
|
||||
Linear,
|
||||
InQuad, OutQuad, InOutQuad,
|
||||
InCubic, OutCubic, InOutCubic,
|
||||
InExpo, OutExpo, InOutExpo,
|
||||
InElastic, OutElastic, InOutElastic,
|
||||
InBounce, OutBounce, InOutBounce,
|
||||
};
|
||||
|
||||
// Funciones inline named (todas (float)->float, t en [0,1]):
|
||||
inline float linear(float t);
|
||||
inline float in_quad(float t); inline float out_quad(float t); inline float in_out_quad(float t);
|
||||
inline float in_cubic(float t); inline float out_cubic(float t); inline float in_out_cubic(float t);
|
||||
inline float in_expo(float t); inline float out_expo(float t); inline float in_out_expo(float t);
|
||||
inline float in_elastic(float t); inline float out_elastic(float t); inline float in_out_elastic(float t);
|
||||
inline float in_bounce(float t); inline float out_bounce(float t); inline float in_out_bounce(float t);
|
||||
|
||||
// Dispatch dinamico por enum (util para data-driven: Keyframe.ease, dropdown UI)
|
||||
float apply(Ease e, float t);
|
||||
const char* name(Ease e); // "Linear", "InQuad", ...
|
||||
|
||||
constexpr int ease_count = 16;
|
||||
|
||||
} // namespace fn::tween
|
||||
```
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
#include "core/tween_curves.h"
|
||||
|
||||
// Lerp con easing entre dos colores
|
||||
float t = elapsed_ms / duration_ms;
|
||||
float k = fn::tween::out_cubic(t);
|
||||
ImVec4 c = lerp(c0, c1, k);
|
||||
|
||||
// Data-driven (timeline keyframe)
|
||||
fn::tween::Ease ease = key.ease;
|
||||
float k = fn::tween::apply(ease, t_normalized);
|
||||
|
||||
// Dropdown ImGui con todas las curvas
|
||||
static int idx = 0;
|
||||
const char* labels[fn::tween::ease_count];
|
||||
for (int i = 0; i < fn::tween::ease_count; i++)
|
||||
labels[i] = fn::tween::name((fn::tween::Ease)i);
|
||||
ImGui::Combo("Ease", &idx, labels, fn::tween::ease_count);
|
||||
```
|
||||
|
||||
## Valores conocidos (smoke tests)
|
||||
|
||||
| Curva | t=0 | t=0.5 | t=1 |
|
||||
|-------------|-------|--------|-------|
|
||||
| Linear | 0.000 | 0.500 | 1.000 |
|
||||
| InQuad | 0.000 | 0.250 | 1.000 |
|
||||
| OutQuad | 0.000 | 0.750 | 1.000 |
|
||||
| InOutQuad | 0.000 | 0.500 | 1.000 |
|
||||
| InCubic | 0.000 | 0.125 | 1.000 |
|
||||
| OutCubic | 0.000 | 0.875 | 1.000 |
|
||||
| InOutCubic | 0.000 | 0.500 | 1.000 |
|
||||
| InExpo | 0.000 | ~0.031 | 1.000 |
|
||||
| OutExpo | 0.000 | ~0.969 | 1.000 |
|
||||
| InOutExpo | 0.000 | 0.500 | 1.000 |
|
||||
| OutBounce | 0.000 | ~0.766 | 1.000 |
|
||||
|
||||
Para todas las curvas: `f(0) == 0` y `f(1) == 1`. Las curvas elastic/bounce pueden salir de [0,1] en valores intermedios — no es un bug, es el overshoot deseado.
|
||||
|
||||
## Decisiones
|
||||
|
||||
- **Header-mostly**: las 16 funciones son `inline` en `.h` para que el compilador inline llamadas en hot paths (animaciones por frame, miles de muestras por curva en plots).
|
||||
- **`apply(enum, t)` en .cpp**: para no forzar `<switch>` enorme en headers; util para casos data-driven.
|
||||
- **Sin clamp interior**: el caller decide si saturar t. Esto permite efectos de overshoot deliberado.
|
||||
- **Sin templates**: los easings se usan masivamente con `float`; un `T` genérico complicaría el header sin beneficio.
|
||||
|
||||
## Referencias
|
||||
|
||||
- easings.net (visualizaciones interactivas y formulas)
|
||||
- Robert Penner — *Motion, Tweening, and Easing* (libro original)
|
||||
Reference in New Issue
Block a user