Files
aego/scene/mutate/anim.go
T
sanspie 515f4ca1b3 Stage 11a: animation (D333–D340, D344, D354–D357)
Clips (.aeanim) and controllers (.aectrl) with importers, AnimationPlayer and Animator with crossfades, root motion and events on fixed steps, rewind support. Editor: Animation panel (dope sheet, curves, record, preview, trajectory), Animator panel, sprite sheet slicer and flipbooks, Programming workspace, export panel redesign. Motion 2D ledge climb. 11c: the component and Create palette.
2026-10-25 15:31:22 +03:00

490 lines
13 KiB
Go

package mutate
import (
"math"
"slices"
"strconv"
"aego/anim"
"aego/core/errs"
"aego/core/log"
"aego/format/aetext"
)
const (
KindAnimKeySet = "anim-key-set"
KindAnimKeyRemove = "anim-key-remove"
KindAnimKeyMove = "anim-key-move"
KindAnimTrackRemove = "anim-track-remove"
KindAnimEventAdd = "anim-event-add"
KindAnimEventRemove = "anim-event-remove"
KindAnimEventMove = "anim-event-move"
KindAnimClipSet = "anim-clip-set"
KindAnimFramesExpand = "anim-frames-expand"
KindAnimSplice = "anim-splice"
timeEps = 1e-5
)
var animKinds = map[string]bool{
KindAnimKeySet: true, KindAnimKeyRemove: true, KindAnimKeyMove: true, KindAnimTrackRemove: true,
KindAnimEventAdd: true, KindAnimEventRemove: true, KindAnimEventMove: true, KindAnimClipSet: true,
KindAnimFramesExpand: true, KindAnimSplice: true,
}
func AnimKeySet(target, prop, time string, v aetext.Value) Op {
return Op{Kind: KindAnimKeySet, Node: target, Path: prop, Key: time, Value: v, HasValue: true}
}
func AnimKeyRemove(target, prop, time string) Op {
return Op{Kind: KindAnimKeyRemove, Node: target, Path: prop, Key: time}
}
func AnimKeyMove(target, prop, from, to string) Op {
return Op{Kind: KindAnimKeyMove, Node: target, Path: prop, Key: from, Name: to}
}
func AnimTrackRemove(target, prop string) Op {
return Op{Kind: KindAnimTrackRemove, Node: target, Path: prop}
}
func AnimEventAdd(time, name string) Op {
return Op{Kind: KindAnimEventAdd, Key: time, Name: name}
}
func AnimEventRemove(i int) Op {
return Op{Kind: KindAnimEventRemove, Index: i, HasIndex: true}
}
func AnimEventMove(i int, time string) Op {
return Op{Kind: KindAnimEventMove, Index: i, HasIndex: true, Key: time}
}
func AnimClipSet(attr string, v aetext.Value) Op {
return Op{Kind: KindAnimClipSet, Path: attr, Value: v, HasValue: true}
}
func AnimClipClear(attr string) Op {
return Op{Kind: KindAnimClipSet, Path: attr}
}
func AnimFramesExpand(target string) Op {
return Op{Kind: KindAnimFramesExpand, Node: target}
}
type AnimDocument struct {
Name string
b blocks
}
func LoadAnim(src []byte, name string) (*AnimDocument, error) {
root, err := aetext.Parse(src, name)
if err != nil {
return nil, err
}
if _, err := anim.Decode(root); err != nil {
return nil, err
}
return &AnimDocument{Name: name, b: blocks{root: root, splice: KindAnimSplice}}, nil
}
func (d *AnimDocument) Root() *aetext.Node {
return d.b.root
}
func (d *AnimDocument) Bytes() []byte {
return aetext.Write(d.b.root)
}
func (d *AnimDocument) Clip() (*anim.Clip, error) {
return anim.Decode(d.b.root)
}
func (d *AnimDocument) Apply(tx Tx) (*Result, error) {
return runChecked(d, tx, false, d.Problems)
}
func (d *AnimDocument) DryRun(tx Tx) (*Result, error) {
return runChecked(d, tx, true, d.Problems)
}
func (d *AnimDocument) Problems() []Problem {
c, err := anim.Decode(d.b.root)
if err != nil {
return []Problem{{Error: true, Message: err.Error()}}
}
var out []Problem
for _, p := range c.Check(nil) {
out = append(out, Problem{Error: true, Message: clipProblem(c, p)})
}
return out
}
func trackLabel(target, prop string) string {
if target == "" {
return prop
}
return target + ": " + prop
}
func clipProblem(c *anim.Clip, p anim.Problem) string {
track := func() string {
t := &c.Tracks[p.Track]
return trackLabel(t.Target, t.Property)
}
switch p.Kind {
case anim.ProblemLength:
return "ANM002: the clip length must be positive"
case anim.ProblemRoot:
return "ANM001: track " + track() + ": root motion needs the node's own transform.position with kind vec2"
case anim.ProblemBadKind:
return "ANM001: track " + track() + " has a kind that cannot be animated"
case anim.ProblemEmpty:
return "ANM001: track " + track() + " has no keys"
case anim.ProblemKeyRange:
return "ANM002: track " + track() + " has a key at " + strconv.FormatFloat(float64(p.T), 'g', -1, 32) + ", outside the clip"
case anim.ProblemEventRange:
return "ANM002: event " + c.Events[p.Event].Name + " is outside the clip"
}
return "ANM003: an event has no name"
}
func (d *AnimDocument) do(op Op, _ *Result) (step, error) {
backup := d.b.root.Clone()
st, err := d.apply(op)
if err == nil {
_, err = anim.Decode(d.b.root)
}
if err != nil {
d.b.root = backup
return step{}, err
}
return st, nil
}
func (d *AnimDocument) apply(op Op) (step, error) {
switch op.Kind {
case KindAnimKeySet:
return d.keySet(op)
case KindAnimKeyRemove:
return d.keyRemove(op)
case KindAnimKeyMove:
return d.keyMove(op)
case KindAnimTrackRemove:
i, err := d.trackOf(op.Node, op.Path)
if err != nil {
return step{}, err
}
return d.b.replace(i, 1, nil, Change{Op: op.Kind, Node: op.Node, Path: op.Path})
case KindAnimEventAdd:
return d.eventAdd(op)
case KindAnimEventRemove, KindAnimEventMove:
return d.eventEdit(op)
case KindAnimClipSet:
return d.clipSet(op)
case KindAnimFramesExpand:
return d.framesExpand(op)
case KindAnimSplice:
return d.b.applySplice(op)
}
return step{}, errs.New(errs.EditorCommand, op.Kind+" does not apply to animation clips")
}
func keyTime(s string) (float32, error) {
f, err := strconv.ParseFloat(s, 32)
if err != nil || math.IsNaN(f) || math.IsInf(f, 0) {
return 0, errs.New(errs.EditorProps, "a time is a number of seconds", log.F("time", s))
}
return float32(f), nil
}
func timeOf(n *aetext.Node) float32 {
v, _ := n.Attr("t")
return float32(v.AsFloat())
}
func sameTime(a, b float32) bool {
return math.Abs(float64(a-b)) <= timeEps
}
func keyAt(tr *aetext.Node, t float32) int {
for i, k := range tr.Blocks {
if k.Head == "key" && sameTime(timeOf(k), t) {
return i
}
}
return -1
}
func insertKey(tr, k *aetext.Node) {
t := timeOf(k)
at := len(tr.Blocks)
for i, b := range tr.Blocks {
if b.Head == "key" && timeOf(b) > t {
at = i
break
}
}
tr.Blocks = slices.Insert(tr.Blocks, at, k)
}
func newKey(t float32, v aetext.Value, extra, base *aetext.Node) *aetext.Node {
k := &aetext.Node{Head: "key"}
if base != nil {
k = base.Clone()
}
setAttr(k, "t", aetext.Float32(t))
setAttr(k, "v", v.Clone())
if extra != nil {
for _, f := range extra.Attrs {
if f.Name != "t" && f.Name != "v" {
setAttr(k, f.Name, f.Value.Clone())
}
}
}
return k
}
func (d *AnimDocument) track(target, prop string) int {
for i, b := range d.b.root.Blocks {
if b.Head != "track" || b.Label != target {
continue
}
if v, ok := b.Attr("prop"); ok && wordOf(v) == prop {
return i
}
}
return -1
}
func (d *AnimDocument) framesOwn(target, prop string) error {
for _, b := range d.b.root.Blocks {
if b.Head != "frames" || b.Label != target {
continue
}
sprite, flip := anim.SpriteProp, anim.FlipProp
if v, ok := b.Attr("prop"); ok {
sprite = wordOf(v)
}
if v, ok := b.Attr("flipProp"); ok {
flip = wordOf(v)
}
_, hasFlip := b.Field("flipX")
if prop == sprite || (hasFlip && prop == flip) {
return errs.New(errs.EditorProps, "track "+trackLabel(target, prop)+" comes from a frames block; expand the frames into keys first")
}
}
return nil
}
func (d *AnimDocument) trackOf(target, prop string) (int, error) {
if i := d.track(target, prop); i >= 0 {
return i, nil
}
if err := d.framesOwn(target, prop); err != nil {
return -1, err
}
return -1, errs.New(errs.EditorProps, "the clip has no track "+trackLabel(target, prop))
}
func (d *AnimDocument) keySet(op Op) (step, error) {
t, err := keyTime(op.Key)
if err != nil {
return step{}, err
}
v, err := op.value()
if err != nil {
return step{}, err
}
ch := Change{Op: op.Kind, Node: op.Node, Path: op.Path, After: FormatValue(v)}
i := d.track(op.Node, op.Path)
if i < 0 {
if err := d.framesOwn(op.Node, op.Path); err != nil {
return step{}, err
}
if _, ok := anim.ParseKind(op.Comp); !ok {
return step{}, errs.New(errs.EditorProps, "a new track needs a kind such as float32, vec2 or color", log.F("kind", op.Comp))
}
tr := &aetext.Node{Head: "track", Label: op.Node, Attrs: []aetext.Field{
{Name: "prop", Value: aetext.String(op.Path)},
{Name: "kind", Value: aetext.Ident(op.Comp)},
}}
tr.Blocks = []*aetext.Node{newKey(t, v, op.Values, nil)}
return d.b.replace(d.b.first("event"), 0, []*aetext.Node{tr}, ch)
}
tr := d.b.root.Blocks[i].Clone()
if k := keyAt(tr, t); k >= 0 {
if ov, ok := tr.Blocks[k].Attr("v"); ok {
ch.Before = FormatValue(ov)
}
tr.Blocks[k] = newKey(t, v, op.Values, tr.Blocks[k])
} else {
insertKey(tr, newKey(t, v, op.Values, nil))
}
return d.b.replace(i, 1, []*aetext.Node{tr}, ch)
}
func (d *AnimDocument) keyRemove(op Op) (step, error) {
t, err := keyTime(op.Key)
if err != nil {
return step{}, err
}
i, err := d.trackOf(op.Node, op.Path)
if err != nil {
return step{}, err
}
tr := d.b.root.Blocks[i].Clone()
k := keyAt(tr, t)
if k < 0 {
return step{}, ErrNothing
}
ch := Change{Op: op.Kind, Node: op.Node, Path: op.Path}
if v, ok := tr.Blocks[k].Attr("v"); ok {
ch.Before = FormatValue(v)
}
tr.Blocks = slices.Delete(tr.Blocks, k, k+1)
if keyAt(tr, t) < 0 && slices.IndexFunc(tr.Blocks, func(b *aetext.Node) bool { return b.Head == "key" }) < 0 {
return d.b.replace(i, 1, nil, ch)
}
return d.b.replace(i, 1, []*aetext.Node{tr}, ch)
}
func (d *AnimDocument) keyMove(op Op) (step, error) {
from, err := keyTime(op.Key)
if err != nil {
return step{}, err
}
to, err := keyTime(op.Name)
if err != nil {
return step{}, err
}
i, err := d.trackOf(op.Node, op.Path)
if err != nil {
return step{}, err
}
tr := d.b.root.Blocks[i].Clone()
k := keyAt(tr, from)
if k < 0 {
return step{}, ErrNothing
}
if sameTime(from, to) {
return step{}, ErrNothing
}
if keyAt(tr, to) >= 0 {
return step{}, errs.New(errs.EditorProps, "the track already has a key at "+op.Name)
}
kb := tr.Blocks[k]
tr.Blocks = slices.Delete(tr.Blocks, k, k+1)
setAttr(kb, "t", aetext.Float32(to))
insertKey(tr, kb)
return d.b.replace(i, 1, []*aetext.Node{tr}, Change{Op: op.Kind, Node: op.Node, Path: op.Path, Before: op.Key, After: op.Name})
}
func (d *AnimDocument) eventAdd(op Op) (step, error) {
t, err := keyTime(op.Key)
if err != nil {
return step{}, err
}
if op.Name == "" {
return step{}, errs.New(errs.EditorProps, "an event needs a name")
}
n := &aetext.Node{Head: "event", Attrs: []aetext.Field{
{Name: "t", Value: aetext.Float32(t)},
{Name: "name", Value: aetext.String(op.Name)},
}}
if op.hasValue() {
v, err := op.value()
if err != nil {
return step{}, err
}
setAttr(n, "payload", v)
}
if op.Level != "" {
setAttr(n, "mode", aetext.Ident(op.Level))
}
return d.b.replace(len(d.b.root.Blocks), 0, []*aetext.Node{n}, Change{Op: op.Kind, Node: op.Name, After: op.Key})
}
func (d *AnimDocument) eventEdit(op Op) (step, error) {
i := d.b.nth("event", op.Index)
if !op.HasIndex || i < 0 {
return step{}, errs.New(errs.EditorProps, "the clip has no event "+strconv.Itoa(op.Index+1))
}
ev := d.b.root.Blocks[i]
ch := Change{Op: op.Kind}
if v, ok := ev.Attr("name"); ok {
ch.Node = wordOf(v)
}
if op.Kind == KindAnimEventRemove {
return d.b.replace(i, 1, nil, ch)
}
t, err := keyTime(op.Key)
if err != nil {
return step{}, err
}
n := ev.Clone()
if v, ok := n.Attr("t"); ok {
ch.Before = FormatValue(v)
}
setAttr(n, "t", aetext.Float32(t))
ch.After = op.Key
return d.b.replace(i, 1, []*aetext.Node{n}, ch)
}
func (d *AnimDocument) clipSet(op Op) (step, error) {
switch op.Path {
case "length", "loop", "fps":
default:
return step{}, errs.New(errs.EditorProps, "a clip has length, loop and fps", log.F("attr", op.Path))
}
root := d.b.root
old, had := root.Attr(op.Path)
inv := Op{Kind: op.Kind, Path: op.Path}
ch := Change{Op: op.Kind, Path: op.Path}
if had {
inv.Value, inv.HasValue = old.Clone(), true
ch.Before = FormatValue(old)
}
redo := Op{Kind: op.Kind, Path: op.Path}
if !op.hasValue() {
if op.Path == "length" {
return step{}, errs.New(errs.EditorProps, "a clip needs a length")
}
if !had {
return step{}, ErrNothing
}
dropAttr(root, op.Path)
return step{inv: inv, redo: redo, change: ch}, nil
}
v, err := op.value()
if err != nil {
return step{}, err
}
setAttr(root, op.Path, v.Clone())
ch.After = FormatValue(v)
redo.Value, redo.HasValue = v.Clone(), true
return step{inv: inv, redo: redo, change: ch}, nil
}
func (d *AnimDocument) framesExpand(op Op) (step, error) {
root := d.b.root
i := slices.IndexFunc(root.Blocks, func(b *aetext.Node) bool { return b.Head == "frames" && b.Label == op.Node })
if i < 0 {
return step{}, errs.New(errs.EditorProps, "the clip has no frames block for "+trackLabel(op.Node, "frames"))
}
tmp := &aetext.Node{Head: anim.Head, Blocks: []*aetext.Node{root.Blocks[i].Clone()}}
if v, ok := root.Attr("length"); ok {
tmp.Attrs = []aetext.Field{{Name: "length", Value: v}}
}
c, err := anim.Decode(tmp)
if err != nil {
return step{}, err
}
var tracks []*aetext.Node
for _, b := range anim.Encode(c).Blocks {
if b.Head == "track" {
tracks = append(tracks, b)
}
}
return d.b.replace(i, 1, tracks, Change{Op: op.Kind, Node: op.Node})
}