Skip to content

Commit fcb3005

Browse files
committed
Fix sycnchronous animation queues
rwaldron#1376 only fixed the problem when animation segments were enqueued in seperate passes through the event loop. When two animation segments were enqueued on the same pass through the even loop we were starting both segments because the isRunning state was not being set until nextTick. This should fix all that. I've updated some of the tests which I believe were written in a non-intuitive manner because of this bug. Instead of really understanding the problem I had just written the tests in a way that made them pass. There's probably a name for this sort of behavior. If not, we should invent a name.
1 parent f36463c commit fcb3005

File tree

3 files changed

+96
-154
lines changed

3 files changed

+96
-154
lines changed

lib/animation.js

+20-9
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ function Animation(target) {
9191

9292
Animation.Segment.call(this);
9393

94-
this.defaultTarget = target;
94+
this.defaultTarget = target || {};
9595
}
9696

9797
util.inherits(Animation, Emitter);
@@ -152,8 +152,7 @@ Animation.prototype.enqueue = function(opts) {
152152
opts.target = this.defaultTarget;
153153
}
154154

155-
this.segments.push(new Animation.Segment(opts));
156-
155+
this.segments.push(opts);
157156

158157
/* istanbul ignore if */
159158
if (!this.paused && !this.isRunning) {
@@ -170,10 +169,16 @@ Animation.prototype.enqueue = function(opts) {
170169
*/
171170
Animation.prototype.next = function() {
172171

173-
if (this.segments.length > 0) {
174-
175-
Object.assign(this, this.segments.shift());
172+
if (this.isRunning) {
173+
return this;
174+
} else {
175+
this.isRunning = true;
176+
}
176177

178+
if (this.segments.length > 0) {
179+
var segment = new Animation.Segment(this.segments.shift());
180+
181+
Object.assign(this, segment);
177182
this.paused = this.currentSpeed === 0 ? true : false;
178183

179184
if (this.onstart) {
@@ -281,7 +286,6 @@ Animation.prototype.loopFunction = function(loop) {
281286
// Find the current timeline progress
282287
var progress = this.calculateProgress(loop.calledAt);
283288

284-
285289
// Find the left and right cuePoints/keyFrames;
286290
var indices = this.findIndices(progress);
287291

@@ -320,14 +324,16 @@ Animation.prototype.loopFunction = function(loop) {
320324
this.endTime = this.startTime + this.scaledDuration;
321325
} else {
322326

323-
this.stop();
327+
this.isRunning = false;
324328

325329
if (this.oncomplete) {
326330
process.nextTick(this.oncomplete.bind(this));
327331
}
328332

329333
if (this.segments.length > 0) {
330-
this.next();
334+
process.nextTick(() => { this.next(); });
335+
} else {
336+
this.stop();
331337
}
332338
}
333339
}
@@ -413,6 +419,11 @@ Animation.prototype.tweenedValue = function(indices, progress) {
413419
progress: null
414420
};
415421

422+
// This can happen if the clock ticks over to a new frame before a segment is ready run
423+
if (typeof this.normalizedKeyFrames !== "object") {
424+
this.normalizeKeyframes();
425+
}
426+
416427
var result = this.normalizedKeyFrames.map(function(keyFrame) {
417428
// Note: "this" is bound to the animation object
418429

test/animation.js

+30-74
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,33 @@ exports["Animation -- Servo"] = {
360360
);
361361

362362
test.done();
363-
}
363+
},
364+
365+
enqueueOnSameTick: function(test) {
366+
367+
test.expect(7);
368+
369+
this.animation = new Animation(this.a);
370+
371+
this.normalizeKeyframes = this.sandbox.stub(this.animation, "normalizeKeyframes");
372+
373+
test.equal(this.animation.isRunning, false);
374+
test.equal(this.animation.segments.length, 0);
375+
376+
// This is the first segment so it should be immediately shifted off the queue
377+
test.equal(this.animation.enqueue(this.segment.single), this.animation);
378+
test.equal(this.animation.segments.length, 0);
379+
test.equal(this.animation.isRunning, true);
380+
381+
// This is the second segment so it should stay in the queue
382+
test.equal(this.animation.enqueue(this.segment.single), this.animation);
383+
test.equal(this.animation.segments.length, 1);
384+
385+
process.nextTick(() => {
386+
this.normalizeKeyframes.restore();
387+
test.done();
388+
});
389+
}
364390

365391
};
366392

@@ -436,6 +462,7 @@ exports["Animation"] = {
436462
test.equal(this.animation.segments.length, 1);
437463
test.done();
438464
},
465+
439466
next: function(test) {
440467
test.expect(12);
441468

@@ -687,78 +714,6 @@ exports["Animation"] = {
687714
test.done();
688715
},
689716

690-
loopFunctiononcomplete: function(test) {
691-
test.expect(2);
692-
693-
this.clock = this.sandbox.useFakeTimers();
694-
this.tfb = this.sandbox.stub(Animation, "TemporalFallback", function() {});
695-
696-
var startTime = Date.now();
697-
var loop = {
698-
calledAt: startTime + 1000
699-
};
700-
701-
this.animation.startTime = startTime;
702-
this.animation.fallBackTime = 500;
703-
this.animation.speed(1);
704-
705-
706-
this.animation.normalizedKeyFrames = [];
707-
this.animation.target = {};
708-
this.animation.target[Animation.render] = function() {};
709-
this.animation.playLoop = null;
710-
this.animation.onloop = this.sandbox.stub();
711-
this.animation.next = this.sandbox.stub();
712-
713-
var animation = this.animation;
714-
715-
this.sandbox.stub(Animation.prototype, "stop");
716-
this.animation.oncomplete = function() {
717-
test.equal(Animation.TemporalFallback.callCount, 1);
718-
test.equal(this, animation);
719-
720-
animation.oncomplete = null;
721-
test.done();
722-
};
723-
724-
this.animation.loopFunction(loop);
725-
},
726-
727-
loopFunctionNOoncompleteSegmentsRemaining: function(test) {
728-
test.expect(1);
729-
730-
this.clock = this.sandbox.useFakeTimers();
731-
this.tfb = this.sandbox.stub(Animation, "TemporalFallback", function() {});
732-
this.stop = this.sandbox.stub(Animation.prototype, "stop");
733-
734-
var startTime = Date.now();
735-
var loop = {
736-
calledAt: startTime + 1000
737-
};
738-
739-
this.animation.startTime = startTime;
740-
this.animation.fallBackTime = 500;
741-
this.animation.speed(1);
742-
743-
744-
this.animation.normalizedKeyFrames = [];
745-
this.animation.target = {};
746-
this.animation.target[Animation.render] = function() {};
747-
this.animation.playLoop = null;
748-
this.animation.onloop = this.sandbox.stub();
749-
this.animation.next = this.sandbox.stub();
750-
751-
this.nextTick = this.sandbox.stub(process, "nextTick");
752-
753-
this.animation.next = function() {
754-
test.equal(this.stop.callCount, 1);
755-
test.done();
756-
}.bind(this);
757-
758-
this.animation.segments = [1, 2, 3, 4];
759-
this.animation.loopFunction(loop);
760-
},
761-
762717
loopFunctionfallBackTimeWithPlayLoop: function(test) {
763718
test.expect(1);
764719

@@ -1021,8 +976,9 @@ exports["Animation.Segment"] = {
1021976
},
1022977

1023978
instanceof: function(test) {
1024-
test.expect(1);
979+
test.expect(2);
1025980
test.equal(Animation({}) instanceof Animation, true);
981+
test.equal(Animation() instanceof Animation, true);
1026982
test.done();
1027983
},
1028984

test/extended/animation.js

+46-71
Original file line numberDiff line numberDiff line change
@@ -141,39 +141,61 @@ exports["Animation"] = {
141141
this.animation.enqueue(this.segment.long);
142142

143143
setTimeout(function() {
144+
// calledAt is a property on temporal tasks
144145
test.ok(this.animation.playLoop.calledAt);
145146
}.bind(this), 3000);
146147

147148
setTimeout(function() {
149+
// interval is the timer on our fallback
148150
test.ok(this.animation.playLoop.interval);
149151
this.animation.stop();
150152
test.done();
151153
}.bind(this), 6000);
152154

153155
},
154156

155-
synchronousSegments: function(test) {
157+
synchronousNextOnTemporal: function(test) {
156158

159+
var startTime = Date.now();
157160
this.animation = new Animation(this.servos);
161+
test.expect(2);
158162

159-
test.expect(3);
160-
161-
this.animation.enqueue(this.segment.long);
162-
this.animation.enqueue(this.segment.short);
163+
var segment = Object.assign({}, this.segment.short);
164+
segment.fps = 200;
165+
segment.oncomplete = () => {
166+
test.equal(Math.abs(Date.now() - startTime - 500) <= 10, true);
167+
};
168+
this.animation.enqueue(segment);
163169

164-
setTimeout(function() {
165-
test.ok(Math.abs(this.animation.progress - 0.5) < 0.05);
166-
}.bind(this), 3500);
170+
segment.oncomplete = () => {
171+
test.equal(Math.abs(Date.now() - startTime - 1000) <= 10, true);
172+
test.done();
173+
};
174+
this.animation.enqueue(segment);
167175

168-
setTimeout(function() {
169-
test.ok(Math.abs(this.animation.progress - 0.5) < 0.51);
170-
}.bind(this), 7350);
176+
},
177+
178+
synchronousNextOnFallback: function(test) {
171179

172-
setTimeout(function() {
173-
test.ok(Math.abs(this.animation.progress) === 1);
174-
test.done();
175-
}.bind(this), 7700);
180+
var startTime = Date.now();
181+
this.animation = new Animation(this.servos);
182+
test.expect(2);
176183

184+
var segment = Object.assign({}, this.segment.long);
185+
segment.fps = 200;
186+
segment.oncomplete = () => {
187+
test.equal(Math.abs(Date.now() - startTime - 7000) <= 10, true);
188+
};
189+
this.animation.enqueue(segment);
190+
191+
segment = Object.assign({}, this.segment.short);
192+
segment.fps = 200;
193+
segment.oncomplete = () => {
194+
test.equal(Math.abs(Date.now() - startTime - 7500) <= 10, true);
195+
test.done();
196+
};
197+
this.animation.enqueue(segment);
198+
177199
},
178200

179201
/*
@@ -185,69 +207,27 @@ exports["Animation"] = {
185207
*/
186208
roundedPi: function(test) {
187209
this.animation = new Animation(this.servos);
188-
test.expect(2);
210+
test.expect(1);
189211

190-
var complete = false;
191212
var tempSegment = this.segment.short;
192213

193214
tempSegment.easing = "inSine";
194215
tempSegment.progress = 0.5;
195216

196-
tempSegment.oncomplete = function() {
197-
complete = true;
198-
};
199-
200-
this.animation.enqueue(tempSegment);
201-
202-
setTimeout(function() {
217+
tempSegment.oncomplete = () => {
203218
test.ok(this.animation.progress === 1);
204-
test.ok(complete === true);
205219
test.done();
206-
}.bind(this), 300);
207-
208-
}
209-
210-
};
211-
212-
213-
exports["Animation - Looping"] = {
214-
setUp: function(done) {
215-
this.sandbox = sinon.sandbox.create();
216-
217-
this.play = this.sandbox.stub(Animation.prototype, "play");
218-
219-
this.animation = new Animation({});
220-
this.chain = {
221-
result: [],
222-
"@@render": function(args) {
223-
this.result = this.result.concat(args);
224-
},
225-
"@@normalize": function(keyFrames) {
226-
var last = [50, 0, -20];
227-
228-
// If user passes null as the first element in keyFrames use current position
229-
if (keyFrames[0] === null) {
230-
keyFrames[0] = {
231-
position: last
232-
};
233-
}
234-
235-
return keyFrames;
236-
}
237220
};
238221

239-
done();
240-
},
222+
this.animation.enqueue(tempSegment);
241223

242-
tearDown: function(done) {
243-
this.sandbox.restore();
244-
done();
245224
},
246225

247226
loopFunction: function(test) {
248-
test.expect(34);
227+
test.expect(33);
249228

250-
this.animation = new Animation(this.chain);
229+
this.animation = new Animation(this.servos);
230+
251231
this.animation.playLoop = {
252232
stop: this.sandbox.spy(),
253233
};
@@ -308,7 +288,6 @@ exports["Animation - Looping"] = {
308288
test.equal(this.findIndices.callCount, 5);
309289
test.equal(this.tweenedValue.callCount, 5);
310290

311-
312291
this.animation.loop = true;
313292
this.animation.progress = 1;
314293
this.animation.loopback = 1;
@@ -362,22 +341,18 @@ exports["Animation - Looping"] = {
362341
this.animation.loopback = 1;
363342
this.animation.metronomic = false;
364343
this.animation.reverse = false;
365-
this.animation.segments.push(1, 2, 3);
366344
this.animation.loopFunction({ calledAt: 1 });
367345

368346
test.equal(this.stop.callCount, 1);
369-
test.equal(this.next.callCount, 1);
370347
test.equal(this.calculateProgress.callCount, 10);
371348
test.equal(this.findIndices.callCount, 10);
372349
test.equal(this.tweenedValue.callCount, 10);
373350

374-
this.animation.oncomplete = function() {
375-
test.done();
376-
};
377-
378351
this.animation.loopFunction({ calledAt: 1 });
379352

380353
test.equal(this.animation.target["@@render"].callCount, 11);
381-
},
354+
355+
test.done();
356+
}
382357

383358
};

0 commit comments

Comments
 (0)