Skip to content

Commit b048129

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 b048129

File tree

3 files changed

+93
-155
lines changed

3 files changed

+93
-155
lines changed

lib/animation.js

+15-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
}

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

+48-72
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,71 +207,29 @@ 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 = {
252-
stop: this.sandbox.spy(),
232+
stop: this.sandbox.spy()
253233
};
254234
this.progress = 1;
255235
this.clock = this.sandbox.useFakeTimers();
@@ -259,6 +239,7 @@ exports["Animation - Looping"] = {
259239
this.calculateProgress = this.sandbox.stub(this.animation, "calculateProgress", function() {
260240
this.animation.progress = this.progress;
261241
this.animation.loopback = this.progress;
242+
this.animation.normalizedKeyFrames = [[0,1]];
262243
return this.progress;
263244
}.bind(this));
264245
this.findIndices = this.sandbox.stub(this.animation, "findIndices").returns({ left: 2, right: 3});
@@ -308,7 +289,6 @@ exports["Animation - Looping"] = {
308289
test.equal(this.findIndices.callCount, 5);
309290
test.equal(this.tweenedValue.callCount, 5);
310291

311-
312292
this.animation.loop = true;
313293
this.animation.progress = 1;
314294
this.animation.loopback = 1;
@@ -362,22 +342,18 @@ exports["Animation - Looping"] = {
362342
this.animation.loopback = 1;
363343
this.animation.metronomic = false;
364344
this.animation.reverse = false;
365-
this.animation.segments.push(1, 2, 3);
366345
this.animation.loopFunction({ calledAt: 1 });
367346

368347
test.equal(this.stop.callCount, 1);
369-
test.equal(this.next.callCount, 1);
370348
test.equal(this.calculateProgress.callCount, 10);
371349
test.equal(this.findIndices.callCount, 10);
372350
test.equal(this.tweenedValue.callCount, 10);
373351

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

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

383359
};

0 commit comments

Comments
 (0)