-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathtween.lua
More file actions
395 lines (363 loc) · 12.5 KB
/
tween.lua
File metadata and controls
395 lines (363 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
---@alias Zenitha.Tween.Tag string
local preAnimSet={} ---@type Set<Zenitha.Tween> new Animation created during _update will be added here first, then moved to updAnimSet
local updAnimSet={} ---@type Set<Zenitha.Tween>
local tagAnimSet={} ---@type Set<Zenitha.Tween>
local unqAnimMap={} ---@type Map<Zenitha.Tween>
local min,floor=math.min,math.floor
local sin,cos=math.sin,math.cos
local clamp=MATH.clamp
---@enum (key) Zenitha.Tween.basicCurve
local curves={
linear=function(t) return t end,
inSin=function(t) return 1-cos(t*1.5707963267948966) end,
outSin=function(t) return sin(t*1.5707963267948966) end,
inQuad=function(t) return t^2 end,
outQuad=function(t) return 1-(1-t)^2 end,
inCubic=function(t) return t^3 end,
outCubic=function(t) return 1-(1-t)^3 end,
inQuart=function(t) return t^4 end,
outQuart=function(t) return 1-(1-t)^4 end,
inQuint=function(t) return t^5 end,
outQuint=function(t) return 1-(1-t)^5 end,
inExp=function(t) return 2^(10*(t-1)) end,
outExp=function(t) return 1-2^(-10*t) end,
inCirc=function(t) return 1-(1-t^2)^.5 end,
outCirc=function(t) return (1-(t-1)^2)^.5 end,
inBack=function(t) return t^2*(2.70158*t-1.70158) end,
inElastic=function(t) return -2^(10*(t-1))*sin((10*t-10.75)*2.0943951023931953) end,
outBack=nil,
outElastic=nil,
}
curves.outBack=function(t) return 1-curves.inBack(1-t) end
curves.outElastic=function(t) return 1-curves.inElastic(1-t) end
---@enum (key) Zenitha.Tween.easeTemplate
local easeTemplates={
Linear={'linear'},
InSin={'inSin'},
OutSin={'outSin'},
InOutSin={'inSin','outSin'},
OutInSin={'outSin','inSin'},
InQuad={'inQuad'},
OutQuad={'outQuad'},
InOutQuad={'inQuad','outQuad'},
OutInQuad={'outQuad','inQuad'},
InCubic={'inCubic'},
OutCubic={'outCubic'},
InOutCubic={'inCubic','outCubic'},
OutInCubic={'outCubic','inCubic'},
InQuart={'inQuart'},
OutQuart={'outQuart'},
InOutQuart={'inQuart','outQuart'},
OutInQuart={'outQuart','inQuart'},
InQuint={'inQuint'},
OutQuint={'outQuint'},
InOutQuint={'inQuint','outQuint'},
OutInQuint={'outQuint','inQuint'},
InExp={'inExp'},
OutExp={'outExp'},
InOutExp={'inExp','outExp'},
OutInExp={'outExp','inExp'},
InCirc={'inCirc'},
OutCirc={'outCirc'},
InOutCirc={'inCirc','outCirc'},
OutInCirc={'outCirc','inCirc'},
InBack={'inBack'},
OutBack={'outBack'},
InOutBack={'inBack','outBack'},
OutInBack={'outBack','inBack'},
InElastic={'inElastic'},
OutElastic={'outElastic'},
InOutElastic={'inElastic','outElastic'},
OutInElastic={'outElastic','inElastic'},
}
--------------------------------------------------------------
-- Tween Class
---@class Zenitha.Tween
---@field running boolean
---@field duration number default to 1
---@field time number used when no timeFunc
---@field loop false | 'repeat' | 'yoyo'
---@field loopCount number current loop number (start from 1)
---@field totalLoop number the total number of times to loop
---@field flipMode boolean true when loop is `'yoyo'`, making time flow back and forth
---@field ease Zenitha.Tween.basicCurve[]
---@field tags Set<Zenitha.Tween.Tag>
---@field unqTag Zenitha.Tween.Tag
---@field private doFunc fun(t:number, loopNo:number)
---@field private timeFunc? fun(): number custom how time goes
---@field private onRepeat fun(loopNo:number)
---@field private onFinish function
---@field private onKill function
local Tween={}
Tween.__index=Tween
local duringUpdate=false -- During update, new [tween]:run() will be added to preAnimSet first to prevent undefined behavior of table iterating
---Set doFunc (generally unnecessary, already set when creating)
---@param doFunc fun(t:number)
---@return Zenitha.Tween
function Tween:setDo(doFunc)
assert(type(doFunc)=='function',"[tween]:setDo(doFunc): Need function")
self.doFunc=doFunc
return self
end
---Set onRepeat callback function `onRepeat(finishedLoopCount)`
---@param func fun(loopCount:number)
---@return Zenitha.Tween
function Tween:setOnRepeat(func)
assert(type(func)=='function',"[tween]:setOnRepeat(onRepeat): Need function")
-- assert(not self.running,"[tween]:setOnRepeat(func): Can't set OnRepeat when running")
self.onRepeat=func
return self
end
---Set onFinish callback function
---@param func function
---@return Zenitha.Tween
function Tween:setOnFinish(func)
assert(type(func)=='function',"[tween]:setOnFinish(onFinish): Need function")
-- assert(not self.running,"[tween]:setOnFinish(func): Can't set OnFinish when running")
self.onFinish=func
return self
end
---Set onKill callback function
---@param func function
---@return Zenitha.Tween
function Tween:setOnKill(func)
assert(type(func)=='function',"[tween]:setOnKill(onKill): Need function")
-- assert(not self.running,"[tween]:setOnKill(func): Can't set OnKill when running")
self.onKill=func
return self
end
---Set easing mode
---@param ease? Zenitha.Tween.easeTemplate | Zenitha.Tween.basicCurve[] default to 'InOutSin'
---@return Zenitha.Tween
function Tween:setEase(ease)
-- assert(not self.running,"[tween]:setEase(ease): Can't set ease when running")
if type(ease)=='string' then
assertf(easeTemplates[ease],"[tween]:setEase(ease): Invalid ease name '%s'",ease)
self.ease=easeTemplates[ease]
elseif type(ease)=='table' then
for i=1,#ease do
assertf(curves[ease[i]],"[tween]:setEase(ease): Invalid ease curve name '%s'",ease[i])
end
self.ease=ease
else
error("[tween]:setEase(ease): Need string|table")
end
return self
end
---Set duration
---@param duration? number
---@return Zenitha.Tween
function Tween:setDuration(duration)
assert(type(duration)=='number' and duration>=0,"[tween]:setDuration(duration): Need >=0")
-- assert(not self.running,"[tween]:setDuration(duration): Can't set duration when running")
self.duration=duration
return self
end
---Set Looping
---@param loopMode false | 'repeat' | 'yoyo'
---@param totalLoop? number default to Infinity
---@return Zenitha.Tween
function Tween:setLoop(loopMode,totalLoop)
assert(not self.timeFunc,"[tween]:setLoop(loopMode): Looping and timeFunc can't exist together")
assert(not loopMode or loopMode=='repeat' or loopMode=='yoyo',"[tween]:setLoop(loopMode): Need false|'repeat'|'yoyo'")
assert(not totalLoop or type(totalLoop)=='number' and totalLoop>=0,"[tween]:setLoop(loopMode,totalLoop): totalLoop need >=0")
-- assert(not self.running,"[tween]:setLoop(loopMode): Can't set loop when running")
self.loop=loopMode
self.totalLoop=totalLoop or 1e99
-- self.loopCount=1 -- will be set on :run()
-- self.flipMode=false
return self
end
---Set tag for batch actions
---@param tag Zenitha.Tween.Tag
---@return Zenitha.Tween
function Tween:setTag(tag)
assert(type(tag)=='string',"[tween]:setTag(tag): Need string")
tagAnimSet[self]=true
self.tags[tag]=true
return self
end
---Set uniqueID (when start running, other active animations with same uniqueID will be killed)
---@param uniqueTag Zenitha.Tween.Tag
---@return Zenitha.Tween
function Tween:setUnique(uniqueTag)
assert(type(uniqueTag)=='string',"[tween]:setUnique(uniqueTag): Need string")
self.unqTag=uniqueTag
return self
end
---Copy an animation ojbect (idk what this is for)
---@return Zenitha.Tween
function Tween:copy()
local anim=TWEEN.new()
TABLE.update(anim,self,2)
return anim
end
---Start the animation animate with time (again), or custom timeFunc
---
---**Warning:** you still have full access to animation after [tween]:run(), but don't touch it unless you know what you're doing
---@param timeFunc? fun(): number Custom the timeFunc (return a number in duration)
---@return Zenitha.Tween
function Tween:run(timeFunc)
if self.running then return self end
assert(timeFunc==nil or type(timeFunc)=='function',"[tween]:run(timeFunc): Need function if exists")
assert(not (self.loop and timeFunc),"[tween]:run(timeFunc): Looping and timeFunc can't exist together")
if self.unqTag then
if unqAnimMap[self.unqTag] then
local a=unqAnimMap[self.unqTag]
a:kill()
end
unqAnimMap[self.unqTag]=self
end
if timeFunc then
self.timeFunc=timeFunc
else
self.time=0
end
if self.loop then
self.loopCount=1
self.flipMode=false
end
self:update(0)
if self.running then
(duringUpdate and preAnimSet or updAnimSet)[self]=true
end
return self
end
---Finish instantly (cannot apply to animation with timeFunc)
---@param simBound? boolean simulate all bound case for animation with loop
---@return Zenitha.Tween
function Tween:skip(simBound)
assert(not self.timeFunc,"[tween]:skip(): Can't skip an animation with timeFunc")
if not self.loop then
self.time=self.duration
self:update(0)
else
if simBound then
assert(self.totalLoop<1e99,"[tween]:skip(): Can't simulate an infinite animation")
repeat
self.time=self.duration
self:update(0)
until not self.running
else
self.time=self.duration
self.loopCount=self.totalLoop
if self.loop=='repeat' then
-- Do nothing
elseif self.loop=='yoyo' then
self.flipMode=self.totalLoop%2==1==self.flipMode
end
self:update(0)
end
end
return self
end
---Release animation from auto updating list and tag list
function Tween:kill()
preAnimSet[self]=nil
updAnimSet[self]=nil
tagAnimSet[self]=nil
if self.unqTag then unqAnimMap[self.unqTag]=nil end
self.onKill()
self.running=false
end
---@param t number
---@param ease function[]
---@return number
local function curveValue(t,ease)
local step=#ease
local n=min(floor(t*step),step-1)
local base=n/step
local curve=curves[ease[n+1]]
return base+curve((t-base)*step)/step
end
---Update the animation
function Tween:update(dt)
self.running=true
if self.timeFunc then
local t=self.timeFunc()
if t then
self.doFunc(curveValue(clamp(self.flipMode and 1-t or t,0,1),self.ease),self.loopCount)
else
self.onFinish()
self:kill()
end
else
self.time=self.time+dt
local t=self.duration<=0 and 1 or min(self.time/self.duration,1)
self.doFunc(curveValue(self.flipMode and 1-t or t,self.ease),self.loopCount)
if t>=1 then
if self.loop and self.loopCount<self.totalLoop then
self.time=0
self.onRepeat(self.loopCount)
self.loopCount=self.loopCount+1
if self.loop=='yoyo' then
self.flipMode=not self.flipMode
end
else
self.onFinish()
self:kill()
end
end
end
end
--------------------------------------------------------------
-- Module
local TWEEN={}
---Create a new tween animation
---@param doFunc? fun(t:number) | fun(t:number, loopNo:number)
---@return Zenitha.Tween
function TWEEN.new(doFunc)
assert(doFunc==nil or type(doFunc)=='function',"TWEEN.new(doFunc): Need function")
local anim=setmetatable({
running=false,
duration=1,
doFunc=doFunc or NULL,
ease=easeTemplates.InOutQuad,
tags={},
unqTag=nil,
onRepeat=NULL,
onFinish=NULL,
onKill=NULL,
},Tween)
return anim
end
---Update all autoAnims (called by Zenitha)
---@param dt number
function TWEEN._update(dt)
duringUpdate=true
for anim in next,updAnimSet do
anim:update(dt)
end
for anim in next,preAnimSet do
preAnimSet[anim]=nil
updAnimSet[anim]=true
end
duringUpdate=false
end
---@param tag Zenitha.Tween.Tag
---@param method 'setEase' | 'setTime' | 'pause' | 'continue' | 'skip' | 'kill' | 'update'
local function tagAction(tag,method,...)
assert(type(tag)=='string',"TWEEN.tag_"..method..": tag need string")
for anim in next,tagAnimSet do
if anim.tags[tag] then
Tween[method](anim,...)
end
end
end
---Finish tagged animations instantly
---@param tag Zenitha.Tween.Tag
function TWEEN.tag_skip(tag)
tagAction(tag,'skip')
end
---Kill tagged animations
---@param tag Zenitha.Tween.Tag
function TWEEN.tag_kill(tag)
tagAction(tag,'kill')
end
---Update tagged animations
---@param tag Zenitha.Tween.Tag
---@param dt number
function TWEEN.tag_update(tag,dt)
tagAction(tag,'update',dt)
end
return TWEEN