Files
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

458 lines
9.1 KiB
Go

package animedit
import (
"math"
"aego/anim"
"aego/core/errs"
"aego/core/ids"
"aego/core/mathx"
"aego/editor/core/document"
"aego/scene"
"aego/scene/mutate"
"aego/scene/propref"
"aego/schema"
)
type Mode uint8
const (
Normal Mode = iota
Preview
Record
)
type Mark struct {
Name string
T float32
}
type binding struct {
node scene.NodeHandle
ref propref.Ref
track int
pose bool
orig anim.Value
}
type Service struct {
AutoKey bool
doc *document.SceneDocument
files Files
notify func(string)
scene *scene.Scene
props *propref.Resolver
mode Mode
rel string
adoc *mutate.AnimDocument
clip *anim.Clip
target ids.NodeID
time float32
speed float32
playing bool
applied bool
binds []binding
skipped []string
posed map[scene.NodeHandle]scene.Transform2D
next map[scene.NodeHandle]scene.Transform2D
samp anim.Sampler
marks []Mark
cross []anim.Crossing
board *[]CopiedKey
flash []flash
hold bool
}
func New(doc *document.SceneDocument, files Files, notify func(string)) *Service {
return &Service{
AutoKey: true, doc: doc, files: files, notify: notify, speed: 1,
posed: map[scene.NodeHandle]scene.Transform2D{}, next: map[scene.NodeHandle]scene.Transform2D{},
}
}
func (s *Service) Mode() Mode { return s.mode }
func (s *Service) Time() float32 { return s.time }
func (s *Service) Clip() *anim.Clip { return s.clip }
func (s *Service) ClipPath() string { return s.rel }
func (s *Service) Target() ids.NodeID { return s.target }
func (s *Service) Playing() bool { return s.playing }
func (s *Service) Skipped() []string { return s.skipped }
func (s *Service) SetSpeed(v float32) { s.speed = v }
func (s *Service) Pause() { s.playing = false }
func (s *Service) say(msg string) {
if s.notify != nil {
s.notify(msg)
}
}
func decodeClip(rel string, data []byte) (*mutate.AnimDocument, *anim.Clip, error) {
d, err := mutate.LoadAnim(data, rel)
if err != nil {
return nil, nil, err
}
c, err := d.Clip()
return d, c, err
}
func (s *Service) SetClip(rel string) error {
data, err := s.files.Read(rel)
if err != nil {
return err
}
d, c, err := decodeClip(rel, data)
if err != nil {
return err
}
s.restore()
s.rel, s.adoc, s.clip, s.time = rel, d, c, 0
s.apply()
return nil
}
func (s *Service) loaded(rel string, data []byte) {
if rel != s.rel {
return
}
d, c, err := decodeClip(rel, data)
if err != nil {
s.say(err.Error())
return
}
s.restore()
s.adoc, s.clip = d, c
s.apply()
}
func (s *Service) Source() *mutate.AnimDocument {
return s.adoc
}
func (s *Service) View() View {
return BuildView(s.clip, s.adoc)
}
func (s *Service) SetTarget(id ids.NodeID) {
if id == s.target {
return
}
s.restore()
s.target = id
s.apply()
}
func (s *Service) SetMode(m Mode) {
if m == s.mode {
return
}
h := s.doc.History
if m == Normal {
s.restore()
s.mode, s.playing = Normal, false
h.SetGuard(nil)
h.SetFilter(nil)
return
}
s.mode = m
h.SetGuard(s)
if m == Record {
h.SetFilter(s.record)
} else {
h.SetFilter(nil)
}
s.apply()
}
func (s *Service) Close() {
s.SetMode(Normal)
}
func (s *Service) Before() {
s.restore()
}
func (s *Service) After() {
s.apply()
}
func (s *Service) SetTime(t float32) {
if s.clip != nil {
t = mathx.Clamp(t, 0, s.clip.Length)
}
s.time = t
if s.applied {
s.sample()
}
}
func (s *Service) Play() {
if s.mode == Normal {
s.SetMode(Preview)
}
s.playing = s.clip != nil
}
func (s *Service) TakeMarks(dst []Mark) []Mark {
dst = append(dst[:0], s.marks...)
s.marks = s.marks[:0]
return dst
}
func (s *Service) Tick(dt float32) {
if s.mode == Normal {
return
}
if s.applied && s.scene != s.doc.Scene() {
s.applied = false
clear(s.posed)
s.apply()
}
n := 0
for _, f := range s.flash {
if f.life -= dt; f.life > 0 {
s.flash[n] = f
n++
}
}
s.flash = s.flash[:n]
if !s.playing || s.clip == nil || s.clip.Length <= 0 {
return
}
c := s.clip
t0 := s.time
t1 := t0 + dt*s.speed
s.cross = anim.Crossings(c, c.Loop, float64(t0), float64(t1), false, s.cross)
for _, x := range s.cross {
if e := &c.Events[x.Event]; e.Mode == anim.EventPreview {
s.marks = append(s.marks, Mark{Name: e.Name, T: e.T})
s.flash = append(s.flash, flash{t: e.T, life: flashLife})
}
}
switch {
case c.Loop == anim.LoopOnce && (t1 >= c.Length || t1 <= 0):
t1 = mathx.Clamp(t1, 0, c.Length)
s.playing = false
case c.Loop == anim.LoopPingPong:
t1 = wrap(t1, 2*c.Length)
case c.Loop == anim.LoopRepeat:
t1 = wrap(t1, c.Length)
}
s.time = t1
s.sample()
}
func wrap(t, l float32) float32 {
return t - l*float32(math.Floor(float64(t/l)))
}
func (s *Service) local() float32 {
t, _ := s.clip.Local(s.clip.Loop, float64(s.time))
return t
}
func (s *Service) apply() {
if s.mode == Normal || s.clip == nil {
return
}
if !s.applied {
if !s.bind() {
return
}
s.applied = true
}
s.sample()
}
func trackName(t *anim.Track) string {
if t.Target == "" {
return t.Property
}
return t.Target + ": " + t.Property
}
func (s *Service) bind() bool {
sc := s.doc.Scene()
s.scene = sc
s.props = propref.New(sc, nil)
s.binds, s.skipped = s.binds[:0], s.skipped[:0]
root := sc.Handle(s.target)
if root.IsZero() || !sc.IsAlive(root) {
return false
}
s.samp.Reset(s.clip)
for i := range s.clip.Tracks {
t := &s.clip.Tracks[i]
if t.Root {
continue
}
node := root
if t.Target != "" {
node = sc.Find(root, t.Target)
}
if node.IsZero() || !sc.IsAlive(node) {
s.skipped = append(s.skipped, trackName(t)+": the node is missing")
continue
}
ref, err := s.props.ResolveAny(node, t.Property)
if err != nil {
s.skipped = append(s.skipped, trackName(t)+": "+err.Error())
continue
}
if !fits(t.Kind, &ref) {
s.skipped = append(s.skipped, trackName(t)+": the kind does not match the property")
continue
}
b := binding{node: node, ref: ref, track: i, pose: ref.Owner() == propref.OwnTransform && !ref.Three()}
if !b.pose {
b.orig = s.read(&b)
}
s.binds = append(s.binds, b)
}
return true
}
func (s *Service) sample() {
if !s.applied {
return
}
local := s.local()
clear(s.next)
for i := range s.binds {
b := &s.binds[i]
v := s.samp.Track(s.clip, b.track, local)
if !b.pose {
s.write(b, v)
continue
}
if s.hold {
continue
}
tr, ok := s.next[b.node]
if !ok {
tr = s.scene.Local(b.node)
}
applyPose(&tr, &b.ref, v)
s.next[b.node] = tr
}
for h, tr := range s.next {
s.scene.SetPresentationLocal(h, tr)
s.posed[h] = tr
}
}
func (s *Service) restore() {
if !s.applied {
return
}
for i := range s.binds {
if b := &s.binds[i]; !b.pose && s.scene.IsAlive(b.node) {
s.write(b, b.orig)
}
}
for h, tr := range s.posed {
if cur, ok := s.scene.PresentationLocal(h); ok && cur == tr {
s.scene.DropPresentationLocal(h)
}
delete(s.posed, h)
}
s.applied = false
}
// Driven reports a field the clip shows in Preview; in Record edits become keys, so nothing is driven.
func (s *Service) Driven(node ids.NodeID, t schema.TypeID, f schema.FieldID) bool {
if !s.applied || s.mode != Preview {
return false
}
h := s.scene.Handle(node)
td, ok := s.scene.Registry().ByID(t)
if !ok || h.IsZero() {
return false
}
fd, ok := td.FieldByID(f)
if !ok {
return false
}
ref, err := s.props.ResolveAny(h, td.FullName+"."+fd.Name)
if err != nil {
return false
}
for i := range s.binds {
if s.binds[i].node == h && s.binds[i].ref.Overlaps(&ref) {
return true
}
}
return false
}
func (s *Service) read(b *binding) anim.Value {
switch b.ref.Shape() {
case propref.Discrete:
i, a, _ := s.props.ReadDiscrete(&b.ref, b.node)
return anim.Value{I: i, A: a}
case propref.Int:
l, _ := s.props.Read(&b.ref, b.node)
return anim.Int(int64(math.Round(float64(l[0]))))
}
l, _ := s.props.Read(&b.ref, b.node)
return anim.Lanes(l)
}
func (s *Service) write(b *binding, v anim.Value) {
switch b.ref.Shape() {
case propref.Discrete:
s.props.WriteDiscrete(&b.ref, b.node, v.I, v.A)
case propref.Int:
s.props.Write(&b.ref, b.node, [4]float32{float32(v.I)})
default:
s.props.Write(&b.ref, b.node, v.L)
}
}
func fits(k schema.Kind, r *propref.Ref) bool {
isFloat := k == schema.KindFloat32 || k == schema.KindFloat64
if r.Lane() >= 0 {
return isFloat
}
switch r.Shape() {
case propref.Discrete:
return k == r.Kind()
case propref.Float:
return isFloat
case propref.Int:
return k == schema.KindInt32 || k == schema.KindInt64
case propref.Vec2:
return k == schema.KindVec2
case propref.Vec3:
return k == schema.KindVec3
case propref.Vec4:
return k == schema.KindVec4 || k == schema.KindColor || k == schema.KindRect
case propref.Quat:
return k == schema.KindQuat
}
return false
}
func setVec(dst *mathx.Vec2, lane int, l [4]float32) {
switch lane {
case -1:
*dst = mathx.Vec2{X: l[0], Y: l[1]}
case 0:
dst.X = l[0]
case 1:
dst.Y = l[0]
}
}
func applyPose(tr *scene.Transform2D, r *propref.Ref, v anim.Value) {
switch r.Part() {
case propref.PartPosition:
setVec(&tr.Position, r.Lane(), v.L)
case propref.PartRotation:
tr.Rotation = v.L[0]
default:
setVec(&tr.Scale, r.Lane(), v.L)
}
}
var errNoClip = errs.New(errs.EditorCommand, "open a clip and turn on Preview first")