Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
Timeline |
|
| 0.0;0 | ||||
Timeline$1 |
|
| 0.0;0 | ||||
Timeline$1TimingTargetAdapter$anon1 |
|
| 0.0;0 | ||||
Timeline$2 |
|
| 0.0;0 | ||||
Timeline$Intf |
|
| 0.0;0 | ||||
Timeline$KFPair |
|
| 0.0;0 | ||||
Timeline$KFPair$Intf |
|
| 0.0;0 | ||||
Timeline$KFPairList |
|
| 0.0;0 | ||||
Timeline$KFPairList$Intf |
|
| 0.0;0 | ||||
Timeline$SubTimeline |
|
| 0.0;0 | ||||
Timeline$SubTimeline$Intf |
|
| 0.0;0 |
1 | /* | |
2 | * Copyright 2008 Sun Microsystems, Inc. All Rights Reserved. | |
3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
4 | * | |
5 | * This code is free software; you can redistribute it and/or modify it | |
6 | * under the terms of the GNU General Public License version 2 only, as | |
7 | * published by the Free Software Foundation. Sun designates this | |
8 | * particular file as subject to the "Classpath" exception as provided | |
9 | * by Sun in the LICENSE file that accompanied this code. | |
10 | * | |
11 | * This code is distributed in the hope that it will be useful, but WITHOUT | |
12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
14 | * version 2 for more details (a copy is included in the LICENSE file that | |
15 | * accompanied this code). | |
16 | * | |
17 | * You should have received a copy of the GNU General Public License version | |
18 | * 2 along with this work; if not, write to the Free Software Foundation, | |
19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
20 | * | |
21 | * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, | |
22 | * CA 95054 USA or visit www.sun.com if you need additional information or | |
23 | * have any questions. | |
24 | */ | |
25 | ||
26 | package javafx.animation; | |
27 | ||
28 | import com.sun.javafx.runtime.Pointer; | |
29 | import com.sun.scenario.animation.Clip; | |
30 | import com.sun.scenario.animation.Interpolators; | |
31 | import com.sun.scenario.animation.TimingTarget; | |
32 | import com.sun.scenario.animation.TimingTargetAdapter; | |
33 | import javafx.lang.Duration; | |
34 | import javafx.lang.Sequences; | |
35 | import java.lang.Object; | |
36 | import java.lang.System; | |
37 | import java.util.ArrayList; | |
38 | import java.lang.System; | |
39 | ||
40 | /** | |
41 | * Represents an animation, defined by one or more {@code KeyFrame}s. | |
42 | */ | |
43 | 201 | public class Timeline { |
44 | ||
45 | /** | |
46 | * Used to specify an animation that repeats indefinitely (until | |
47 | * the {@code stop()} method is called). | |
48 | */ | |
49 | 4 | public static attribute INDEFINITE = -1; |
50 | ||
51 | /** | |
52 | * Defines the number of cycles in this animation. | |
53 | * The {@code repeatCount} may be {@code INDEFINITE} | |
54 | * for animations that repeat indefinitely, but must otherwise be >= 0. | |
55 | * The default value is 1. | |
56 | */ | |
57 | 59 | public attribute repeatCount: Number = 1.0; |
58 | ||
59 | /** | |
60 | * Defines whether this animation reverses direction on alternating | |
61 | * cycles. | |
62 | * If {@code true}, the animation will proceed forward on | |
63 | * the first cycle, then reverses on the second cycle, and so on. | |
64 | * The default value is {@code false}, indicating that the | |
65 | * animation will loop such that each cycle proceeds | |
66 | * forward from the initial {@code KeyFrame}. | |
67 | */ | |
68 | 61 | public attribute autoReverse: Boolean = false; |
69 | ||
70 | /** | |
71 | * Defines whether this animation reverses direction in place | |
72 | * each time {@code start()} is called. | |
73 | * If {@code true}, the animation will initially proceed forward, | |
74 | * then restarts in place except heading in opposite direction. | |
75 | * The default value is {@code false}, indicating that the | |
76 | * animation will restart from the initial {@code KeyFrame} | |
77 | * each time {@code start()} is called. | |
78 | */ | |
79 | 13 | public attribute toggle: Boolean = false on replace { |
80 | 8 | isReverse = true; |
81 | }; | |
82 | ||
83 | /** | |
84 | * Defines the sequence of {@code KeyFrame}s in this animation. | |
85 | * If a {@code KeyFrame} is not provided for the {@code time==0s} | |
86 | * instant, one will be synthesized using the target values | |
87 | * that are current at the time {@code start()} is called. | |
88 | */ | |
89 | 10 | public attribute keyFrames: KeyFrame[] on replace { |
90 | 5 | invalidate(); |
91 | }; | |
92 | ||
93 | /** | |
94 | * Read-only attribute that indicates whether the animation is | |
95 | * currently running. | |
96 | * <p> | |
97 | * This value is initially {@code false}. | |
98 | * It will become {@code true} after {@code start()} has been called, | |
99 | * and then becomes {@code false} again after the animation ends | |
100 | * naturally, or after an explicit call to {@code stop()}. | |
101 | * <p> | |
102 | * Note that {@code running} will remain {@code true} even when | |
103 | * {@code paused==true}. | |
104 | */ | |
105 | 44 | public /*controlled*/ attribute running: Boolean = false; |
106 | ||
107 | /** | |
108 | * Read-only attribute that indicates whether the animation is | |
109 | * currently paused. | |
110 | * <p> | |
111 | * This value is initially {@code false}. | |
112 | * It will become {@code true} after {@code pause()} has been called | |
113 | * on a running animation, and then becomes {@code false} again after | |
114 | * an explicit call to {@code resume()} or {@code stop()}. | |
115 | * <p> | |
116 | * Note that {@code running} will remain {@code true} even when | |
117 | * {@code paused==true}. | |
118 | */ | |
119 | 38 | public /*controlled*/ attribute paused: Boolean = false; |
120 | ||
121 | // if false, indicates that the internal (optimized) data structure | |
122 | // needs to be rebuilt | |
123 | 39 | private attribute valid = false; |
124 | function invalidate() { | |
125 | 5 | valid = false; |
126 | } | |
127 | ||
128 | // duration is inferred from time of last key frame and durations | |
129 | // of any sub-timelines in rebuildTargets() | |
130 | 218 | private attribute duration: Number = -1; |
131 | ||
132 | function getTotalDur():Number { | |
133 | if (not valid) { | |
134 | 19 | rebuildTargets(); |
135 | } | |
136 | if (duration < 0 or repeatCount < 0) { | |
137 | 19 | return -1; |
138 | } | |
139 | 19 | return duration * repeatCount; |
140 | } | |
141 | ||
142 | /** | |
143 | * Starts (or restarts) the animation. | |
144 | * <p> | |
145 | * If {@code toggle==false} and the animation is currently running, | |
146 | * the animation will be restarted from its initial position. | |
147 | * <p> | |
148 | * If {@code toggle==true} and the animation is currently running, | |
149 | * the animation will immediately change direction in place and | |
150 | * continue on in that new direction. When the animation finishes | |
151 | * in one direction, calling {@code start()} again will restart the | |
152 | * animation in the opposite direction. | |
153 | */ | |
154 | public function start() { | |
155 | if (toggle) { | |
156 | // change direction in place | |
157 | if (clip == null) { | |
158 | 4 | buildClip(); |
159 | } | |
160 | 4 | isReverse = not isReverse; |
161 | 4 | offsetValid = false; |
162 | 4 | frameIndex = keyFrames.size() - frameIndex; |
163 | if (not clip.isRunning()) { | |
164 | 4 | clip.start(); |
165 | } | |
166 | 18 | } else { |
167 | // stop current clip and restart from beginning | |
168 | 5 | buildClip(); |
169 | 5 | clip.start(); |
170 | } | |
171 | } | |
172 | ||
173 | /** | |
174 | * Stops the animation. If the animation is not currently running, | |
175 | * this method has no effect. | |
176 | */ | |
177 | public function stop() { | |
178 | 0 | clip.stop(); |
179 | } | |
180 | ||
181 | /** | |
182 | * Pauses the animation. If the animation is not currently running, | |
183 | * this method has no effect. | |
184 | */ | |
185 | public function pause() { | |
186 | 0 | clip.pause(); |
187 | } | |
188 | ||
189 | /** | |
190 | * Resumes the animation from a paused state. If the animation is | |
191 | * not currently running or not currently paused, this method has | |
192 | * no effect. | |
193 | */ | |
194 | public function resume() { | |
195 | 0 | clip.resume(); |
196 | } | |
197 | ||
198 | 5 | private function buildClip() { |
199 | if (clip <> null and clip.isRunning()) { | |
200 | 5 | clip.stop(); |
201 | } | |
202 | 5 | clip = Clip.create(Clip.INDEFINITE, adapter); |
203 | 5 | clip.setInterpolator(Interpolators.getLinearInstance()); |
204 | } | |
205 | ||
206 | 100 | private attribute clip: Clip; |
207 | 189 | private attribute sortedFrames: KeyFrame[]; |
208 | 194 | private attribute targets: ArrayList = new ArrayList(); |
209 | 64 | private attribute subtimelines: ArrayList = new ArrayList(); |
210 | 25 | private attribute adapter: TimingTarget = createAdapter(); |
211 | ||
212 | 104 | private attribute cycleIndex: Integer = 0; |
213 | 260 | private attribute frameIndex: Integer = 0; |
214 | ||
215 | 66 | private attribute isReverse: Boolean = true; |
216 | 41 | private attribute offsetT: Number = 0; |
217 | 48 | private attribute lastElapsed: Number = 0; |
218 | 45 | private attribute offsetValid: Boolean = false; |
219 | ||
220 | // | |
221 | // Need to revalidate everything (call rebuildTargets() again) if | |
222 | // any of the following change after construction: | |
223 | // - Timeline.keyFrames (insert, delete, or replace) | |
224 | // - KeyFrame.time (any) | |
225 | // - KeyValue.target (any) | |
226 | // | |
227 | // The following should be safe to change at any time: | |
228 | // - Timeline.repeatCount | |
229 | // - Timeline.autoReverse | |
230 | // - Timeline.toggle | |
231 | // - KeyValue.value | |
232 | // - KeyValue.interpolate | |
233 | // | |
234 | 5 | private function rebuildTargets():Void { |
235 | 5 | targets.clear(); |
236 | 5 | subtimelines.clear(); |
237 | 5 | duration = 0; |
238 | if (sizeof keyFrames == 0) { | |
239 | 5 | return; |
240 | } | |
241 | ||
242 | 5 | sortedFrames = Sequences.sort(keyFrames) as KeyFrame[]; |
243 | ||
244 | 10 | var zeroFrame:KeyFrame; |
245 | 5 | if (sortedFrames[0].time == 0s) { |
246 | 3 | zeroFrame = sortedFrames[0]; |
247 | } else { | |
248 | 7 | zeroFrame = KeyFrame { time: 0s }; |
249 | } | |
250 | ||
251 | 5 | for (keyFrame in keyFrames) { |
252 | if (duration >= 0) { | |
253 | 30 | duration = java.lang.Math.max(duration, keyFrame.time.millis); |
254 | } | |
255 | ||
256 | if (keyFrame.timelines <> null) { | |
257 | 30 | for (timeline in keyFrame.timelines) { |
258 | 2 | var subDur = timeline.getTotalDur(); |
259 | if (duration >= 0 and subDur >= 0) { | |
260 | 1 | duration = java.lang.Math.max(duration, keyFrame.time.millis + subDur); |
261 | } else { | |
262 | 1 | duration = -1; |
263 | } | |
264 | 3 | var sub = SubTimeline { |
265 | 1 | startTime: keyFrame.time |
266 | 1 | timeline: timeline |
267 | } | |
268 | 1 | subtimelines.add(sub); |
269 | } | |
270 | } | |
271 | ||
272 | 30 | for (keyValue in keyFrame.values) { |
273 | // TODO: targets should really be Map<Pointer,List<KFPair>> | |
274 | 54 | var pairlist: KFPairList; |
275 | 27 | for (i in [0..<targets.size()]) { |
276 | 24 | var pl = targets.get(i) as KFPairList; |
277 | 24 | if (pl.target == keyValue.target) { |
278 | // already have a KFPairList for this target | |
279 | 24 | pairlist = pl; |
280 | 24 | break; |
281 | } | |
282 | } | |
283 | 27 | if (pairlist == null) { |
284 | 6 | pairlist = KFPairList { |
285 | 3 | target: keyValue.target |
286 | } | |
287 | 3 | if (keyFrame.time <> 0s) { |
288 | // get current value and attach it to zero frame | |
289 | 3 | var kv = KeyValue { |
290 | 1 | target: keyValue.target; |
291 | 1 | value: keyValue.target.get(); |
292 | } | |
293 | 3 | var kfp = KFPair { |
294 | 1 | value: kv |
295 | 1 | frame: zeroFrame |
296 | } | |
297 | 1 | pairlist.add(kfp); |
298 | } | |
299 | 3 | targets.add(pairlist); |
300 | } | |
301 | 81 | var kfpair = KFPair { |
302 | 27 | frame: keyFrame |
303 | 27 | value: keyValue |
304 | } | |
305 | 27 | pairlist.add(kfpair); |
306 | } | |
307 | } | |
308 | ||
309 | 5 | valid = true; |
310 | } | |
311 | ||
312 | 15 | function process(totalElapsed:Number):Void { |
313 | // 1. calculate totalDur | |
314 | // 2. modify totalElapsed depending on direction | |
315 | // 3. clamp totalElapsed and set needsStop if necessary | |
316 | // 4. calculate curT and cycle based on totalElapsed | |
317 | // 5. decide whether to increment or decrement cycle/frame index, depending on direction | |
318 | // 6. visit key frames | |
319 | // 7. do interpolation between active key frames | |
320 | // 8. visit subtimelines | |
321 | // 9. stop clip if needsStop | |
322 | ||
323 | 30 | var needsStop = false; |
324 | 30 | var totalDur = getTotalDur(); |
325 | ||
326 | 15 | if (totalDur >= 0) { |
327 | 15 | if (toggle) { |
328 | 8 | if (not offsetValid) { |
329 | if (isReverse) { | |
330 | 2 | offsetT = totalElapsed + lastElapsed; |
331 | } else { | |
332 | 6 | offsetT = totalElapsed - lastElapsed; |
333 | } | |
334 | 4 | offsetValid = true; |
335 | } | |
336 | ||
337 | // adjust totalElapsed to account for direction (the | |
338 | // incoming totalElapsed value will continue to increase | |
339 | // monotonically regardless of how many times the direction | |
340 | // has been reversed, so here we just massage it back into | |
341 | // the range [0,totalDur] so that other calculations below | |
342 | // will work as usual) | |
343 | if (isReverse) { | |
344 | 4 | totalElapsed = offsetT - totalElapsed; |
345 | } else { | |
346 | 12 | totalElapsed = totalElapsed - offsetT; |
347 | } | |
348 | } | |
349 | ||
350 | // process one last pulse to ensure targets reach their end values | |
351 | if (toggle and isReverse) { | |
352 | 4 | if (totalElapsed <= 0) { |
353 | 2 | totalElapsed = 0; |
354 | 2 | needsStop = true; |
355 | } | |
356 | } else { | |
357 | 26 | if (totalElapsed >= totalDur) { |
358 | 8 | totalElapsed = totalDur; |
359 | 8 | needsStop = true; |
360 | } | |
361 | } | |
362 | ||
363 | // capture last adjusted totalElapsed value (used in toggle case) | |
364 | 15 | lastElapsed = totalElapsed; |
365 | } | |
366 | ||
367 | 30 | var curT:Number; |
368 | 30 | var cycle:Integer; |
369 | 30 | var backward = false; |
370 | if (duration < 0) { | |
371 | // indefinite duration (e.g. will occur when a sub-timeline | |
372 | // has indefinite repeatCount); always stay on zero cycle | |
373 | 0 | curT = totalElapsed; |
374 | 0 | cycle = 0; |
375 | 15 | } else { |
376 | 15 | curT = totalElapsed % duration; |
377 | 15 | cycle = totalElapsed / duration as Integer; |
378 | 15 | if (curT == 0 and totalElapsed <> 0) { |
379 | // we're at the end, or exactly on a cycle boundary; | |
380 | // treat this as the "1.0" case of the previous cycle | |
381 | // instead of the "0.0" case of the current cycle | |
382 | // TODO: there's probably a better way to deal with this... | |
383 | 10 | curT = duration; |
384 | 10 | cycle -= 1; |
385 | } | |
386 | if (autoReverse) { | |
387 | 15 | if (cycle % 2 == 1) { |
388 | 0 | curT = duration - curT; |
389 | 0 | backward = true; |
390 | } | |
391 | } | |
392 | } | |
393 | ||
394 | // look through each KeyFrame and see if we need to visit its | |
395 | // key values and its action function | |
396 | if (toggle and isReverse) { | |
397 | 4 | backward = not backward; |
398 | 8 | while (cycleIndex > cycle) { |
399 | // we're on a new cycle; visit any key frames that we may | |
400 | // have missed along the way | |
401 | 4 | visitCycle(cycleIndex, cycleIndex > cycle+1); |
402 | 4 | cycleIndex--; |
403 | } | |
404 | } else { | |
405 | 34 | while (cycleIndex < cycle) { |
406 | // we're on a new cycle; visit any key frames that we may | |
407 | // have missed along the way | |
408 | 8 | visitCycle(cycleIndex, cycleIndex < cycle-1); |
409 | 8 | cycleIndex++; |
410 | } | |
411 | } | |
412 | 15 | visitFrames(curT, backward, false); |
413 | ||
414 | // now handle the active interval for each target | |
415 | 15 | for (i in [0..<targets.size()]) { |
416 | 26 | var pairlist = targets.get(i) as KFPairList; |
417 | 26 | var kfpair1 = pairlist.get(0); |
418 | 26 | var leftT = kfpair1.frame.time.millis; |
419 | ||
420 | if (curT < leftT) { | |
421 | // haven't yet reached the first key frame | |
422 | // for this target | |
423 | 13 | continue; |
424 | } | |
425 | ||
426 | 26 | var v1:KeyValue; |
427 | 26 | var v2:KeyValue; |
428 | 26 | var segT = 0.0; |
429 | ||
430 | 13 | for (j in [1..<pairlist.size()]) { |
431 | // find keyframes on either side of the curT value | |
432 | 90 | var kfpair2 = pairlist.get(j); |
433 | 90 | var rightT = kfpair2.frame.time.millis; |
434 | 45 | if (curT <= rightT) { |
435 | 13 | v1 = kfpair1.value; |
436 | 13 | v2 = kfpair2.value; |
437 | 13 | segT = (curT - leftT) / (rightT - leftT); |
438 | 13 | break; |
439 | } | |
440 | ||
441 | 32 | kfpair1 = kfpair2; |
442 | 32 | leftT = kfpair1.frame.time.millis; |
443 | } | |
444 | ||
445 | if (v1 <> null and v2 <> null) { | |
446 | 13 | pairlist.target.set(v2.interpolate.interpolate(v1.value, v2.value, segT)); |
447 | } | |
448 | } | |
449 | ||
450 | // look through all sub-timelines and recursively call process() | |
451 | // on any active SubTimeline objects | |
452 | 15 | for (i in [0..<subtimelines.size()]) { |
453 | 1 | var sub = subtimelines.get(i) as SubTimeline; |
454 | 1 | if (curT >= sub.startTime.millis) { |
455 | 1 | var subDur = sub.timeline.getTotalDur(); |
456 | if (subDur < 0 or curT <= sub.startTime.millis + subDur) { | |
457 | 1 | sub.timeline.process(curT - sub.startTime.millis); |
458 | } | |
459 | } | |
460 | } | |
461 | ||
462 | if (needsStop and clip <> null) { | |
463 | 15 | clip.stop(); |
464 | } | |
465 | } | |
466 | ||
467 | private function visitCycle(cycle:Integer, catchingUp:Boolean) { | |
468 | 24 | var cycleBackward = false; |
469 | if (autoReverse) { | |
470 | if (cycle % 2 == 1) { | |
471 | 12 | cycleBackward = true; |
472 | } | |
473 | } | |
474 | if (toggle and isReverse) { | |
475 | 12 | cycleBackward = not cycleBackward; |
476 | } | |
477 | 24 | var cycleT = if (cycleBackward) 0 else duration; |
478 | 12 | visitFrames(cycleT, cycleBackward, catchingUp); |
479 | // avoid repeated visits to terminals in autoReverse case | |
480 | 12 | frameIndex = if (autoReverse) 1 else 0; |
481 | } | |
482 | ||
483 | private function visitFrames(curT:Number, backward:Boolean, catchingUp:Boolean) { | |
484 | if (backward) { | |
485 | 18 | var i1 = sortedFrames.size()-1-frameIndex; |
486 | 18 | var i2 = 0; |
487 | 9 | for (fi in [i1..i2 step -1]) { |
488 | 27 | var kf = sortedFrames[fi]; |
489 | if (curT <= kf.time.millis) { | |
490 | if (not (catchingUp and kf.canSkip)) { | |
491 | 25 | kf.visit(); |
492 | } | |
493 | 25 | frameIndex++; |
494 | } else { | |
495 | 52 | break; |
496 | } | |
497 | } | |
498 | 54 | } else { |
499 | 36 | var i1 = frameIndex; |
500 | 36 | var i2 = sortedFrames.size()-1; |
501 | 18 | for (fi in [i1..i2]) { |
502 | 70 | var kf = sortedFrames[fi]; |
503 | if (curT >= kf.time.millis) { | |
504 | if (not (catchingUp and kf.canSkip)) { | |
505 | 67 | kf.visit(); |
506 | } | |
507 | 67 | frameIndex++; |
508 | } else { | |
509 | 137 | break; |
510 | } | |
511 | } | |
512 | } | |
513 | } | |
514 | ||
515 | 0 | private function createAdapter():TimingTarget { |
516 | 127 | TimingTargetAdapter { |
517 | 32 | public function begin() : Void { |
518 | 9 | running = true; |
519 | 9 | paused = false; |
520 | ||
521 | if (toggle and isReverse) { | |
522 | 2 | cycleIndex = (repeatCount-1) as Integer; |
523 | 2 | lastElapsed = getTotalDur(); |
524 | 9 | } else { |
525 | 7 | cycleIndex = 0; |
526 | 7 | lastElapsed = 0; |
527 | } | |
528 | 9 | frameIndex = 0; |
529 | 9 | offsetT = 0; |
530 | 9 | offsetValid = false; |
531 | } | |
532 | ||
533 | 14 | public function timingEvent(fraction, totalElapsed) : Void { |
534 | 14 | process(totalElapsed as Number); |
535 | } | |
536 | ||
537 | 0 | public function pause() : Void { |
538 | 0 | paused = true; |
539 | } | |
540 | ||
541 | 0 | public function resume() : Void { |
542 | 0 | paused = false; |
543 | } | |
544 | ||
545 | 22 | public function end() : Void { |
546 | 9 | running = false; |
547 | 9 | paused = false; |
548 | } | |
549 | } | |
550 | } | |
551 | } | |
552 | ||
553 | 112 | class KFPair { |
554 | 1396 | attribute frame:KeyFrame; |
555 | 110 | attribute value:KeyValue; |
556 | } | |
557 | ||
558 | 352 | class KFPairList { |
559 | 59 | attribute target:Pointer; |
560 | 742 | private attribute pairs:ArrayList = new ArrayList(); |
561 | ||
562 | function size(): Integer { | |
563 | 13 | return pairs.size(); |
564 | } | |
565 | ||
566 | 28 | function add(pair:KFPair): Void { |
567 | // keep list sorted chronologically | |
568 | 28 | for (i in [0..<pairs.size()]) { |
569 | 238 | var listval = get(i); |
570 | 238 | if (pair.frame.time < listval.frame.time) { |
571 | 0 | pairs.add(i, pair); |
572 | 0 | return; |
573 | } | |
574 | } | |
575 | 28 | pairs.add(pair); |
576 | } | |
577 | ||
578 | function get(i:Integer): KFPair { | |
579 | 296 | return pairs.get(i) as KFPair; |
580 | } | |
581 | } | |
582 | ||
583 | 4 | class SubTimeline { |
584 | 6 | attribute startTime:Duration; |
585 | 7 | attribute timeline:Timeline; |
586 | } |