334 lines
5.8 KiB
Go
334 lines
5.8 KiB
Go
package curve
|
|
|
|
import (
|
|
"cmp"
|
|
"math"
|
|
"slices"
|
|
|
|
"aego/core/mathx"
|
|
)
|
|
|
|
type Interp uint8
|
|
|
|
const (
|
|
InterpBezier Interp = iota
|
|
InterpLinear
|
|
InterpStep
|
|
)
|
|
|
|
type Tangent uint8
|
|
|
|
const (
|
|
TangentAuto Tangent = iota
|
|
TangentLinear
|
|
TangentFree
|
|
)
|
|
|
|
type Wrap uint8
|
|
|
|
const (
|
|
WrapClamp Wrap = iota
|
|
WrapLoop
|
|
WrapPingPong
|
|
)
|
|
|
|
var (
|
|
interpNames = []string{"bezier", "linear", "step"}
|
|
tangentNames = []string{"auto", "linear", "free"}
|
|
wrapNames = []string{"clamp", "loop", "pingPong"}
|
|
)
|
|
|
|
func name(list []string, i uint8) string {
|
|
if int(i) < len(list) {
|
|
return list[i]
|
|
}
|
|
return list[0]
|
|
}
|
|
|
|
func parse(list []string, s string) (uint8, bool) {
|
|
for i, n := range list {
|
|
if n == s {
|
|
return uint8(i), true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func (i Interp) String() string { return name(interpNames, uint8(i)) }
|
|
func (t Tangent) String() string { return name(tangentNames, uint8(t)) }
|
|
func (w Wrap) String() string { return name(wrapNames, uint8(w)) }
|
|
|
|
func ParseInterp(s string) (Interp, bool) {
|
|
v, ok := parse(interpNames, s)
|
|
return Interp(v), ok
|
|
}
|
|
|
|
func ParseTangent(s string) (Tangent, bool) {
|
|
v, ok := parse(tangentNames, s)
|
|
return Tangent(v), ok
|
|
}
|
|
|
|
func ParseWrap(s string) (Wrap, bool) {
|
|
v, ok := parse(wrapNames, s)
|
|
return Wrap(v), ok
|
|
}
|
|
|
|
type Key struct {
|
|
T float32
|
|
V float32
|
|
In float32
|
|
Out float32
|
|
Interp Interp
|
|
Tangent Tangent
|
|
}
|
|
|
|
type Curve struct {
|
|
Ease Easing
|
|
Keys []Key
|
|
Pre Wrap
|
|
Post Wrap
|
|
}
|
|
|
|
func Constant(v float32) Curve {
|
|
return Curve{Keys: []Key{{V: v}}}
|
|
}
|
|
|
|
func Line(t0, v0, t1, v1 float32) Curve {
|
|
return Keyed(Key{T: t0, V: v0, Tangent: TangentLinear}, Key{T: t1, V: v1, Tangent: TangentLinear})
|
|
}
|
|
|
|
func FromEasing(e Easing) Curve {
|
|
return Curve{Ease: e}
|
|
}
|
|
|
|
func Keyed(keys ...Key) Curve {
|
|
c := Curve{Keys: slices.Clone(keys)}
|
|
c.Fix()
|
|
return c
|
|
}
|
|
|
|
func (c Curve) Clone() Curve {
|
|
c.Keys = slices.Clone(c.Keys)
|
|
return c
|
|
}
|
|
|
|
func (c Curve) Equal(o Curve) bool {
|
|
return c.Ease == o.Ease && c.Pre == o.Pre && c.Post == o.Post && slices.Equal(c.Keys, o.Keys)
|
|
}
|
|
|
|
func (c *Curve) Fix() {
|
|
slices.SortStableFunc(c.Keys, func(a, b Key) int { return cmp.Compare(a.T, b.T) })
|
|
out := c.Keys[:0]
|
|
for _, k := range c.Keys {
|
|
if k.T != k.T || k.V != k.V {
|
|
continue
|
|
}
|
|
if n := len(out); n > 0 && out[n-1].T == k.T {
|
|
out[n-1] = k
|
|
continue
|
|
}
|
|
out = append(out, k)
|
|
}
|
|
clear(c.Keys[len(out):])
|
|
c.Keys = out
|
|
c.Smooth()
|
|
}
|
|
|
|
func secant(a, b *Key) float32 {
|
|
if d := b.T - a.T; d > 0 {
|
|
return (b.V - a.V) / d
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func auto(l, r float32) float32 {
|
|
if l*r <= 0 {
|
|
return 0
|
|
}
|
|
s := (l + r) / 2
|
|
if lim := 3 * min(mathx.Abs(l), mathx.Abs(r)); mathx.Abs(s) > lim {
|
|
return mathx.Sign(s) * lim
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (c *Curve) Smooth() {
|
|
ks := c.Keys
|
|
n := len(ks)
|
|
for i := range ks {
|
|
k := &ks[i]
|
|
if k.Tangent == TangentFree {
|
|
continue
|
|
}
|
|
hasL, hasR := i > 0, i < n-1
|
|
var l, r float32
|
|
if hasL {
|
|
l = secant(&ks[i-1], k)
|
|
}
|
|
if hasR {
|
|
r = secant(k, &ks[i+1])
|
|
}
|
|
switch {
|
|
case !hasL && !hasR:
|
|
k.In, k.Out = 0, 0
|
|
case !hasL:
|
|
k.In, k.Out = r, r
|
|
case !hasR:
|
|
k.In, k.Out = l, l
|
|
case k.Tangent == TangentLinear:
|
|
k.In, k.Out = l, r
|
|
default:
|
|
s := auto(l, r)
|
|
k.In, k.Out = s, s
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Curve) Span() (float32, float32) {
|
|
if c.Ease != None || len(c.Keys) == 0 {
|
|
return 0, 1
|
|
}
|
|
return c.Keys[0].T, c.Keys[len(c.Keys)-1].T
|
|
}
|
|
|
|
func (c *Curve) Duration() float32 {
|
|
t0, t1 := c.Span()
|
|
return t1 - t0
|
|
}
|
|
|
|
func (c *Curve) Eval(t float32) float32 {
|
|
t0, t1 := c.Span()
|
|
switch {
|
|
case t != t:
|
|
t = t0
|
|
case t < t0:
|
|
t = wrap(t, t0, t1, c.Pre)
|
|
case t > t1:
|
|
t = wrap(t, t0, t1, c.Post)
|
|
}
|
|
return c.at(t)
|
|
}
|
|
|
|
func (c *Curve) EvalClamped(t float32) float32 {
|
|
t0, t1 := c.Span()
|
|
if t != t {
|
|
t = t0
|
|
}
|
|
return c.at(mathx.Clamp(t, t0, t1))
|
|
}
|
|
|
|
func wrap(t, t0, t1 float32, w Wrap) float32 {
|
|
d := float64(t1 - t0)
|
|
if d <= 0 || w == WrapClamp {
|
|
return mathx.Clamp(t, t0, t1)
|
|
}
|
|
period := d
|
|
if w == WrapPingPong {
|
|
period = 2 * d
|
|
}
|
|
m := math.Mod(float64(t-t0), period)
|
|
if m < 0 {
|
|
m += period
|
|
}
|
|
if m > d {
|
|
m = 2*d - m
|
|
}
|
|
return t0 + float32(m)
|
|
}
|
|
|
|
func (c *Curve) at(t float32) float32 {
|
|
if c.Ease != None {
|
|
return Ease(c.Ease, t)
|
|
}
|
|
ks := c.Keys
|
|
n := len(ks)
|
|
switch {
|
|
case n == 0:
|
|
return 0
|
|
case t <= ks[0].T:
|
|
return ks[0].V
|
|
case t >= ks[n-1].T:
|
|
return ks[n-1].V
|
|
}
|
|
lo, hi := 0, n-1
|
|
for hi-lo > 1 {
|
|
m := (lo + hi) / 2
|
|
if ks[m].T <= t {
|
|
lo = m
|
|
} else {
|
|
hi = m
|
|
}
|
|
}
|
|
return segment(&ks[lo], &ks[hi], t)
|
|
}
|
|
|
|
func segment(a, b *Key, t float32) float32 {
|
|
d := b.T - a.T
|
|
if d <= 0 {
|
|
return b.V
|
|
}
|
|
switch a.Interp {
|
|
case InterpStep:
|
|
return a.V
|
|
case InterpLinear:
|
|
return a.V + (b.V-a.V)*(t-a.T)/d
|
|
}
|
|
u := (t - a.T) / d
|
|
u2 := u * u
|
|
u3 := u2 * u
|
|
h00 := 2*u3 - 3*u2 + 1
|
|
h10 := u3 - 2*u2 + u
|
|
h01 := 3*u2 - 2*u3
|
|
h11 := u3 - u2
|
|
return h00*a.V + h10*d*a.Out + h01*b.V + h11*d*b.In
|
|
}
|
|
|
|
func (c *Curve) Normalize() {
|
|
if c.Ease != None || len(c.Keys) < 2 {
|
|
return
|
|
}
|
|
t0, t1 := c.Span()
|
|
d := t1 - t0
|
|
if d <= 0 {
|
|
return
|
|
}
|
|
for i := range c.Keys {
|
|
k := &c.Keys[i]
|
|
k.T = (k.T - t0) / d
|
|
k.In *= d
|
|
k.Out *= d
|
|
}
|
|
c.Keys[0].T = 0
|
|
c.Keys[len(c.Keys)-1].T = 1
|
|
}
|
|
|
|
func (c *Curve) Range(samples int) (lo, hi float32) {
|
|
t0, t1 := c.Span()
|
|
samples = max(samples, 2)
|
|
lo, hi = mathx.Inf(), -mathx.Inf()
|
|
for i := 0; i < samples; i++ {
|
|
v := c.at(t0 + (t1-t0)*float32(i)/float32(samples-1))
|
|
lo, hi = min(lo, v), max(hi, v)
|
|
}
|
|
for _, k := range c.Keys {
|
|
lo, hi = min(lo, k.V), max(hi, k.V)
|
|
}
|
|
return lo, hi
|
|
}
|
|
|
|
func (c *Curve) Bake(samples int) Curve {
|
|
if c.Ease == None {
|
|
return c.Clone()
|
|
}
|
|
samples = max(samples, 2)
|
|
const h = 1e-3
|
|
out := Curve{Pre: c.Pre, Post: c.Post, Keys: make([]Key, samples)}
|
|
for i := range out.Keys {
|
|
t := float32(i) / float32(samples-1)
|
|
a, b := max(t-h, 0), min(t+h, 1)
|
|
s := (Ease(c.Ease, b) - Ease(c.Ease, a)) / (b - a)
|
|
out.Keys[i] = Key{T: t, V: Ease(c.Ease, t), In: s, Out: s, Tangent: TangentFree}
|
|
}
|
|
return out
|
|
}
|