From 0c45840151d8832526773482b399584109371905 Mon Sep 17 00:00:00 2001 From: qlli Date: Wed, 30 Oct 2024 09:27:25 +0800 Subject: [PATCH] Support property anim, based on timeline --- game.go | 31 ++- internal/timeline/interval.go | 31 +++ internal/timeline/timeline.go | 192 +++++++++++++++++ internal/timeline/timeline_group.go | 16 ++ internal/timeline/tweener.go | 308 ++++++++++++++++++++++++++++ internal/timeline/tweener_test.go | 280 +++++++++++++++++++++++++ timeline_mgr.go | 53 +++++ 7 files changed, 901 insertions(+), 10 deletions(-) create mode 100644 internal/timeline/interval.go create mode 100644 internal/timeline/timeline.go create mode 100644 internal/timeline/timeline_group.go create mode 100644 internal/timeline/tweener.go create mode 100644 internal/timeline/tweener_test.go create mode 100644 timeline_mgr.go diff --git a/game.go b/game.go index 0ee16967..7fdddb33 100644 --- a/game.go +++ b/game.go @@ -54,14 +54,16 @@ const ( DbgFlagInstr DbgFlagEvent DbgFlagPerf - DbgFlagAll = DbgFlagLoad | DbgFlagInstr | DbgFlagEvent | DbgFlagPerf + DbgFlagTimeline + DbgFlagAll = DbgFlagLoad | DbgFlagInstr | DbgFlagEvent | DbgFlagPerf | DbgFlagTimeline ) var ( - debugInstr bool - debugLoad bool - debugEvent bool - debugPerf bool + debugInstr bool + debugLoad bool + debugEvent bool + debugPerf bool + debugTimeline bool ) func SetDebug(flags dbgFlags) { @@ -69,6 +71,7 @@ func SetDebug(flags dbgFlags) { debugInstr = (flags & DbgFlagInstr) != 0 debugEvent = (flags & DbgFlagEvent) != 0 debugPerf = (flags & DbgFlagPerf) != 0 + debugTimeline = (flags & DbgFlagTimeline) != 0 } // ------------------------------------------------------------------------------------- @@ -81,11 +84,12 @@ type Game struct { fs spxfs.Dir shared *sharedImages - sounds soundMgr - turtle turtleCanvas - typs map[string]reflect.Type // map: name => sprite type, for all sprites - sprs map[string]Sprite // map: name => sprite prototype, for loaded sprites - items []Shape // shapes on stage (in Zorder), not only sprites + sounds soundMgr + turtle turtleCanvas + typs map[string]reflect.Type // map: name => sprite type, for all sprites + sprs map[string]Sprite // map: name => sprite prototype, for loaded sprites + items []Shape // shapes on stage (in Zorder), not only sprites + timelines TimelineMgr tickMgr tickMgr input inputMgr @@ -625,16 +629,23 @@ func (p *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeigh return p.windowSize_() } +var ts int64 + func (p *Game) Update() error { if !p.isLoaded { return nil } + old := ts + ts = time.Now().UnixMilli() p.updateColliders() p.input.update() p.updateMousePos() p.sounds.update() p.tickMgr.update() + if old != 0 { + p.timelines.Update((float64(ts-old) / 1000.0)) + } return nil } diff --git a/internal/timeline/interval.go b/internal/timeline/interval.go new file mode 100644 index 00000000..c0438565 --- /dev/null +++ b/internal/timeline/interval.go @@ -0,0 +1,31 @@ +package timeline + +import "fmt" + +type Interval struct { + Offset float64 + Duration float64 +} + +func (i Interval) End() float64 { + return i.Offset + i.Duration +} + +func (i *Interval) Step(time float64) { + i.Offset -= time +} + +func (i Interval) Scale(scale float32) Interval { + return Interval{ + Offset: float64(float64(i.Offset) * float64(scale)), + Duration: float64(float64(i.Duration) * float64(scale)), + } +} + +func (i Interval) Contains(time float64) bool { + return i.Offset <= time && time <= i.End() +} + +func (i Interval) String() string { + return fmt.Sprintf("Interval{offset:%.3f, duration:%.3f}", i.Offset, i.Duration) +} diff --git a/internal/timeline/timeline.go b/internal/timeline/timeline.go new file mode 100644 index 00000000..b1a7e58d --- /dev/null +++ b/internal/timeline/timeline.go @@ -0,0 +1,192 @@ +package timeline + +import ( + "fmt" + "log" + "sync" +) + +const EPSILON float64 = 0.0001 +const DEFAULT_TRANSITION float64 = 0.5 + +var debugTimeline bool = false +var idSeed int64 +var idSeedM sync.Mutex + +type ITimeline interface { + GetTimeline() *Timeline + Step(time *float64) ITimeline + SetActive(bool) +} + +type Timeline struct { + ID int64 + active bool + offset float64 + speed float64 + fadeIn *Interval + fadeOut *Interval + freezingTime float64 + next ITimeline + group *TimelineGroup + onStep func(*float64) ITimeline + onActive func(bool) +} + +func (t *Timeline) Init() *Timeline { + idSeedM.Lock() + defer idSeedM.Unlock() + t.ID = idSeed + idSeed++ + if debugTimeline { + log.Printf("Timeline %X", t.ID) + } + t.speed = 1.0 + return t +} + +func (t *Timeline) GetTimeline() *Timeline { + return t +} + +func (t *Timeline) SetActive(on bool) { + if t.active == on { + return + } + if debugTimeline { + log.Println("SetActive", on, t.ID) + } + t.active = on + if t.onActive != nil { + t.onActive(on) + } +} + +func (t *Timeline) Step(time *float64) ITimeline { + if *time < 0.0 { + *time = 0.0 + } + + var running ITimeline = t + var loopLimit int = 10000 + for running != nil && *time > EPSILON && loopLimit > 0 { + loopLimit-- + var realStep, scaledTime, oldScaledTime float64 + var oldRunning ITimeline + r := running.GetTimeline() + + // step until time <= 0 || offset <= 0 + var min float64 = *time // minimum step in consideration of time, offset, fadeout.end, freezingTime + if r.offset > EPSILON { + if min > r.offset { + min = r.offset + } + *time -= min + r.offset -= min + continue + } + + r.SetActive(true) + + if r.fadeOut != nil { + end := r.fadeOut.End() + if end < 0.0 { + end = 0.0 + } + if min > end { + min = end + } + } + + // step until time <= 0 || fadeOut.end <= 0 || freezingTime <= 0 + if r.freezingTime > EPSILON { + if min > r.freezingTime { + min = r.freezingTime + } + *time -= min + r.freezingTime -= min + if r.fadeIn != nil { + r.fadeIn.Step(min) + } + if r.fadeOut != nil { + r.fadeOut.Step(min) + } + + goto CHECK_FADE_OUT + } + + // step until time <= 0 || fadeOut.end <= 0 || runOut.end <= 0 + scaledTime = min * r.speed + oldScaledTime = scaledTime + oldRunning = running + running = r.onStep(&scaledTime) + if r.speed > EPSILON { + realStep = (oldScaledTime - scaledTime) / r.speed + } else { + realStep = min + } + *time -= realStep + if r.fadeIn != nil { + r.fadeIn.Step(realStep) + } + if r.fadeOut != nil { + r.fadeOut.Step(realStep) + } + + if running != oldRunning { + if running != nil { + continue + } + + // running == null, means the timeline has run out, should check whether fade out + if r.fadeOut == nil || r.fadeOut.End() <= EPSILON { + oldRunning.SetActive(false) + running = r.next + continue + } + + // step until time <= 0 || fadeOut.end <= 0 + end2 := r.fadeOut.End() + if end2 < 0.0 { + end2 = 0.0 + } + min2 := *time + if min2 > end2 { + min2 = end2 + } + *time -= min2 + if r.fadeIn != nil { + r.fadeIn.Step(min2) + } + if r.fadeOut != nil { + r.fadeOut.Step(min2) + } + running = oldRunning + } + + CHECK_FADE_OUT: + t2 := running.GetTimeline() + if t2.fadeOut != nil && t2.fadeOut.End() <= EPSILON { + t2.SetActive(false) + running = t2.next + } + } + + if loopLimit <= 0 { + panic("LOOP LIMIT") + } + return running +} + +func (t *Timeline) String() string { + var nID int64 = 0 + if t.next != nil { + nID = t.next.GetTimeline().ID + } + var gID int64 = 0 + if t.group != nil { + gID = t.group.ID + } + return fmt.Sprintf("{id:%x,active:%t,offset:%.3f,speed:%.3f,in:%s,out:%s,frz:%.3f,next:%x,group:%x}", + t.ID, t.active, t.offset, t.speed, t.fadeIn, t.fadeOut, t.freezingTime, nID, gID) +} diff --git a/internal/timeline/timeline_group.go b/internal/timeline/timeline_group.go new file mode 100644 index 00000000..7b20cb1c --- /dev/null +++ b/internal/timeline/timeline_group.go @@ -0,0 +1,16 @@ +package timeline + +type TimelineGroup struct { + Timeline + Tracks []ITimeline +} + +func (tg *TimelineGroup) Step(time *float64) ITimeline { + tg.Timeline.Step(time) + return nil +} + +// AddTrack 方法 +func (tg *TimelineGroup) AddTrack(track ITimeline) { + tg.Tracks = append(tg.Tracks, track) +} diff --git a/internal/timeline/tweener.go b/internal/timeline/tweener.go new file mode 100644 index 00000000..d0674a7c --- /dev/null +++ b/internal/timeline/tweener.go @@ -0,0 +1,308 @@ +package timeline + +import ( + "log" + "math" + + "github.com/goplus/spx/internal/math32" +) + +var debugTweener bool = false + +type TweenerMode int + +const ( + CUR_TO_END TweenerMode = iota + START_TO_END + START_TO_CUR +) + +type ITweener interface { + ITimeline + GetTweener() *Tweener + // Rewind() +} + +type Tweener struct { + Timeline + mode TweenerMode + getter func() interface{} + setter func(interface{}) + startVal interface{} + curVal interface{} // only for START_TO_END mode + endVal interface{} + duration float64 +} + +func (t *Tweener) GetTweener() *Tweener { + return t +} + +func (t *Tweener) Init__0(getter func() interface{}, setter func(interface{}), endVal interface{}, duration float64) *Tweener { + t.mode = CUR_TO_END + t.getter = getter + t.setter = setter + t.endVal = endVal + if duration < 0.0 { + duration = 0.0 + } + t.duration = duration + t.Timeline.Init() + t.Timeline.onActive = t.onActive + t.Timeline.onStep = func(time *float64) ITimeline { + return t.onStep(time) + } + if debugTweener { + log.Printf("Tweener.Init__0 %X", t.ID) + } + return t +} + +func (t *Tweener) Init__1(setter func(interface{}), startVal interface{}, endVal interface{}, duration float64) *Tweener { + t.mode = START_TO_END + t.setter = setter + t.startVal = startVal + t.curVal = startVal + t.endVal = endVal + if duration < 0.0 { + duration = 0.0 + } + t.duration = duration + t.Timeline.Init() + t.Timeline.onActive = t.onActive + t.Timeline.onStep = func(time *float64) ITimeline { + return t.onStep(time) + } + if debugTweener { + log.Printf("Tweener.Init__1 %X", t.ID) + } + return t +} + +func (t *Tweener) Init__2(getter func() interface{}, setter func(interface{}), startVal interface{}, duration float64) *Tweener { + t.mode = START_TO_CUR + t.getter = getter + t.setter = setter + t.startVal = startVal + t.duration = duration + t.Timeline.Init() + t.Timeline.onActive = t.onActive + t.Timeline.onStep = func(time *float64) ITimeline { + return t.onStep(time) + } + if debugTweener { + log.Printf("Tweener.Init__2 %X", t.ID) + } + return t +} + +func (t *Tweener) GetTimeline() *Timeline { + return &t.Timeline +} + +func (t *Tweener) onActive(active bool) { + if debugTweener { + log.Printf("Tweener.onActive %t %X", active, t.ID) + } + + switch t.mode { + case CUR_TO_END: + if active { + t.startVal = t.getter() + } else { + t.setter(t.endVal) + } + case START_TO_CUR: + if active { + t.endVal = t.getter() + t.setter(t.startVal) + } else { + t.setter(t.endVal) + } + case START_TO_END: + if active { + t.setter(t.startVal) + } else { + t.setter(t.endVal) + } + } +} + +func (t *Tweener) Step(time *float64) ITimeline { + timeline := &t.Timeline + running := timeline.Step(time) + if running == timeline { + return t + } else { + return running + } +} + +func (t *Tweener) onStep(time *float64) ITimeline { + switch t.startVal.(type) { + case float64: + return t.onStepFloat64(time) + case math32.Vector2: + return t.onStepV2(time) + case *math32.Vector2: + panic("should pass Vector2, not *Vector2, check types of tweener.getter return and startVal and endVal") + } + return t +} + +func (t *Tweener) onStepFloat64(time *float64) ITimeline { + var start float64 + var ok bool + if start, ok = t.startVal.(float64); !ok { + return nil + } + var end float64 + if end, ok = t.endVal.(float64); !ok { + return nil + } + + switch t.mode { + case CUR_TO_END, START_TO_CUR: + delta := end - start + if math.Abs(delta) < EPSILON { + return nil + } + if t.duration < EPSILON { + return nil + } + if t.getter == nil || t.setter == nil { + return nil + } + cur, ok2 := t.getter().(float64) + if !ok2 { + return nil + } + + speed := delta / t.duration + need := math.Abs((end - cur) / speed) + if *time < need { + t.setter(cur + *time*speed) + *time = 0.0 + return t + } else { + *time -= need + t.setter(t.endVal) + return nil + } + case START_TO_END: + delta := end - start + if math.Abs(delta) < EPSILON { + return nil + } + if t.duration < EPSILON { + return nil + } + if t.setter == nil { + return nil + } + cur, ok2 := t.curVal.(float64) + if !ok2 { + return nil + } + + speed := delta / t.duration + need := math.Abs((end - cur) / speed) + if *time < need { + t.curVal = cur + *time*speed + t.setter(t.curVal) + *time = 0.0 + return t + } else { + *time -= need + t.curVal = t.endVal + t.setter(t.endVal) + return nil + } + } + return nil +} + +func (t *Tweener) onStepV2(time *float64) ITimeline { + var start math32.Vector2 + var ok bool + if start, ok = t.startVal.(math32.Vector2); !ok { + return nil + } + var end math32.Vector2 + if end, ok = t.endVal.(math32.Vector2); !ok { + return nil + } + + switch t.mode { + case CUR_TO_END, START_TO_CUR: + delta := (&end).Sub(&start) + if delta.LengthSquared() < EPSILON { + return nil + } + if t.duration < EPSILON { + return nil + } + if t.getter == nil || t.setter == nil { + return nil + } + cur, ok2 := t.getter().(math32.Vector2) + if !ok2 { + return nil + } + speed := delta.Scale(1.0 / t.duration) + need := math.Abs((&end).Sub(&cur).Length() / speed.Length()) + if *time < need { + v2 := (&cur).Add((speed.Scale(*time))) + t.setter(*v2) + *time = 0.0 + return t + } else { + *time -= need + t.setter(t.endVal) + return nil + } + case START_TO_END: + delta := (&end).Sub(&start) + if delta.LengthSquared() < EPSILON { + return nil + } + if t.duration < EPSILON { + return nil + } + if t.setter == nil { + return nil + } + cur, ok2 := t.curVal.(math32.Vector2) + if !ok2 { + return nil + } + + speed := delta.Scale(1.0 / t.duration) + need := math.Abs((&end).Sub(&cur).Length() / speed.Length()) + if *time < need { + t.curVal = *((&cur).Add(speed.Scale(*time))) + t.setter(t.curVal) + *time = 0.0 + return t + } else { + *time -= need + t.curVal = t.endVal + t.setter(t.endVal) + return nil + } + } + return nil +} + +func (t *Tweener) From() ITweener { + newT := &Tweener{} + switch t.mode { + case CUR_TO_END: + newT.Init__2(t.getter, t.setter, t.endVal, t.duration) + case START_TO_CUR: + newT.Init__1(t.setter, t.endVal, t.startVal, t.duration) + case START_TO_END: + newT.Init__0(t.getter, t.setter, t.startVal, t.duration) + } + return newT +} diff --git a/internal/timeline/tweener_test.go b/internal/timeline/tweener_test.go new file mode 100644 index 00000000..b96bf1e9 --- /dev/null +++ b/internal/timeline/tweener_test.go @@ -0,0 +1,280 @@ +package timeline + +import ( + "fmt" + "math" + "sync" + "testing" + + "github.com/goplus/spx/internal/math32" + "github.com/qiniu/x/log" +) + +func Assert(condition bool, args ...string) { + if !condition { + for _, arg := range args { + log.Error("assert fail" + arg) + } + panic("assert fail") + } +} + +func AssertApprox(f1 float64, f2 float64, args ...string) { + equal := math.Abs(float64(f1)-float64(f2)) < 0.0001 + if !equal { + for _, arg := range args { + log.Error(arg) + } + panic(fmt.Sprintf("\n%f != %f\n", f1, f2)) + } +} + +type testSpriteImpl struct { + angle float64 + x, y float64 +} + +func TestTweener1(t *testing.T) { + fmt.Println("TestTweener1") + s := &testSpriteImpl{} + tn := &Tweener{} + getter := func() interface{} { + return s.angle + } + setter := func(a interface{}) { + s.angle = a.(float64) + } + + tn.Init__0(getter, setter, 100.0, 1.0) + var time float64 = 0.0 + running := tn.Step(&time) + Assert(running == tn) + time = 0.5 + running = tn.Step(&time) + Assert(running == tn) + AssertApprox(s.angle, 50) + time = 0.5 + running = tn.Step(&time) + Assert(running == nil) + AssertApprox(s.angle, 100) +} + +func TestTweener2(t *testing.T) { + s := &testSpriteImpl{} + tn := &Tweener{} + setter := func(a interface{}) { + s.angle = a.(float64) + } + tn.Init__1(setter, 0.0, 100.0, 1.0) + var time float64 = 0.0 + running := tn.Step(&time) + Assert(running == tn) + time = 0.5 + running = tn.Step(&time) + Assert(running == tn) + AssertApprox(s.angle, 50) + time = 0.5 + running = tn.Step(&time) + Assert(running == nil) + AssertApprox(s.angle, 100) +} + +func TestTweener3(t *testing.T) { + s := &testSpriteImpl{} + s.angle = 100.0 + tn := &Tweener{} + getter := func() interface{} { + return s.angle + } + setter := func(a interface{}) { + s.angle = a.(float64) + } + tn.Init__2(getter, setter, 0.0, 1.0) + var time float64 = 0.0 + running := tn.Step(&time) + Assert(running == tn) + time = 0.5 + running = tn.Step(&time) + Assert(running == tn) + AssertApprox(s.angle, 50) + time = 0.5 + running = tn.Step(&time) + Assert(running == nil) + AssertApprox(s.angle, 100) +} + +func TestFrom(t *testing.T) { + s := &testSpriteImpl{} + s.angle = 0.0 + to := &Tweener{} + getter := func() interface{} { + return s.angle + } + setter := func(a interface{}) { + s.angle = a.(float64) + } + to.Init__0(getter, setter, 100.0, 1.0) + + from := to.From() + AssertApprox(s.angle, 0.0) + var time float64 = 0.0 + running := from.Step(&time) + Assert(running == from) + time = 0.5 + running = from.Step(&time) + Assert(running == from) + AssertApprox(s.angle, 50.0) + AssertApprox(float64(time), 0.0) + time = 0.5 + running = from.Step(&time) + AssertApprox(float64(time), 0.0) + Assert(running == nil) + AssertApprox(s.angle, 0.0) +} + +func TestSyncMap(t *testing.T) { + var m sync.Map + for i := 1; i < 100; i++ { + s := &testSpriteImpl{} + s.angle = 0.0 + to := &Tweener{} + getter := func() interface{} { + return s.angle + } + setter := func(a interface{}) { + s.angle = a.(float64) + } + to.Init__0(getter, setter, 100.0, 1.0) + m.Store(to.ID, to) + } +} + +func TestTweenerV2(t *testing.T) { + fmt.Println("TestTweenerV2") + s := &testSpriteImpl{} + tn := &Tweener{} + getter := func() interface{} { + return *math32.NewVector2(s.x, s.y) + } + setter := func(obj interface{}) { + if v, ok := obj.(math32.Vector2); ok { + s.x, s.y = v.X, v.Y + } + } + tn.Init__0(getter, setter, *math32.NewVector2(100.0, 100.0), 1.0) + var time float64 = 0.0 + running := tn.Step(&time) + Assert(running == tn) + time = 0.1 + running = tn.Step(&time) + Assert(running == tn) + AssertApprox(s.x, 10.0) + AssertApprox(s.y, 10.0) + time = 0.4 + running = tn.Step(&time) + AssertApprox(float64(time), 0.0) + Assert(running == tn) + AssertApprox(s.x, 50.0) + AssertApprox(s.y, 50.0) + time = 0.5 + running = tn.Step(&time) + AssertApprox(float64(time), 0.0) + Assert(running == nil) + AssertApprox(s.x, 100.0) + AssertApprox(s.y, 100.0) +} + +func TestTweenerV2_2(t *testing.T) { + fmt.Println("TestTweenerV2") + s := &testSpriteImpl{} + tn := &Tweener{} + setter := func(obj interface{}) { + if v, ok := obj.(math32.Vector2); ok { + s.x, s.y = v.X, v.Y + } + } + tn.Init__1(setter, *math32.NewVector2(0.0, 0.0), *math32.NewVector2(100.0, 100.0), 1.0) + var time float64 = 0.0 + running := tn.Step(&time) + Assert(running == tn) + time = 0.1 + running = tn.Step(&time) + Assert(running == tn) + AssertApprox(s.x, 10.0) + AssertApprox(s.y, 10.0) + time = 0.4 + running = tn.Step(&time) + AssertApprox(float64(time), 0.0) + Assert(running == tn) + AssertApprox(s.x, 50.0) + AssertApprox(s.y, 50.0) + time = 0.5 + running = tn.Step(&time) + AssertApprox(float64(time), 0.0) + Assert(running == nil) + AssertApprox(s.x, 100.0) + AssertApprox(s.y, 100.0) +} + +func TestTweenerV2_3(t *testing.T) { + fmt.Println("TestTweenerV2") + s := &testSpriteImpl{} + s.x = 100.0 + s.y = 100.0 + tn := &Tweener{} + getter := func() interface{} { + return *math32.NewVector2(s.x, s.y) + } + setter := func(obj interface{}) { + if v, ok := obj.(math32.Vector2); ok { + s.x, s.y = v.X, v.Y + } + } + tn.Init__2(getter, setter, *math32.NewVector2(0.0, 0.0), 1.0) + var time float64 = 0.0 + running := tn.Step(&time) + Assert(running == tn) + time = 0.1 + running = tn.Step(&time) + Assert(running == tn) + AssertApprox(s.x, 10.0) + AssertApprox(s.y, 10.0) + time = 0.4 + running = tn.Step(&time) + AssertApprox(float64(time), 0.0) + Assert(running == tn) + AssertApprox(s.x, 50.0) + AssertApprox(s.y, 50.0) + time = 0.5 + running = tn.Step(&time) + AssertApprox(float64(time), 0.0) + Assert(running == nil) + AssertApprox(s.x, 100.0) + AssertApprox(s.y, 100.0) +} + +/* +func (p *SpriteImpl) TestTweenScale(targetScale float64, secs float64) { + fmt.Println("TestTweenScale") + tn := &timeline.Tweener{} + tn.Init1(func() interface{} { + return p.scale + }, func(obj interface{}) { + p.scale = obj.(float64) + }, targetScale, timeline.float64(secs)) + p.g.timelines.AddTimeline(tn) +} + +func (p *SpriteImpl) TestTweenV2(x, y float64, secs float64) { + fmt.Println("TestTweenV2") + tn := &timeline.Tweener{} + tn.Init1(func() interface{} { + return *math32.NewVector2(p.x, p.y) + }, func(obj interface{}) { + if v2, ok := obj.(math32.Vector2); ok { + p.x, p.y = v2.X, v2.Y + } + }, *math32.NewVector2(x, y), timeline.float64(secs)) + p.g.timelines.AddTimeline(tn) +} +*/ diff --git a/timeline_mgr.go b/timeline_mgr.go new file mode 100644 index 00000000..3d6f4b7d --- /dev/null +++ b/timeline_mgr.go @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package spx + +import ( + "fmt" + "sync" + + "github.com/goplus/spx/internal/timeline" +) + +type TimelineMgr struct { + timelines sync.Map // map: ID => ITimeline +} + +func (p *TimelineMgr) AddTimeline(t timeline.ITimeline) { + if t != nil { + p.timelines.Store(t.GetTimeline().ID, t) + } +} + +func (p *TimelineMgr) Update(deltaTime float64) { + timelines := &p.timelines + remove := []int64{} + timelines.Range(func(id, v interface{}) bool { + if it, ok := v.(timeline.ITimeline); ok { + t := deltaTime + running := it.Step(&t) + if running == nil { + remove = append(remove, it.GetTimeline().ID) + } + } + return true + }) + for _, id := range remove { + fmt.Printf("TimelineMgr.Update del %X\n", id) + timelines.Delete(id) + } +}