Jump to content
Search Community

.call being skipped when timeScale is very high

avancamp test
Moderator Tag

Recommended Posts

I am potentially close to finding a repro.

 

First question: is it intentional that gsap.globalTimeline.progress() can sometimes grow unbounded as demonstrated in this CodePen? (check the console output)

 

EDIT: On first run, the issue doesn't seem to happen? Try hitting Run again and it seems to happen then.

 

EDIT 2: The issue is only present if the JS Console is open when Run is pressed... I do not understand.

 

EDIT 3: This issue is... hardware dependent somehow? It doesn't happen on my laptop, but does on my desktop. I am so confused.

 

EDIT 4: It has happened on my laptop exactly once after a few dozen tries. It happens on my desktop every time consistently.

 

See the Pen WNvvRap?editors=0010 by Lange (@Lange) on CodePen

Link to comment
Share on other sites

Yeah, the playheads of all timelines/tweens are limited to movement within the bounds of 0 and their duration EXCEPT for the globalTimeline. By necessity, that must be able to progress endlessly. That's why progress() is returning odd values for you but I can add some clamping to that just so that progress()/totalProgress() can't ever be reported as beyond 1 even on the globalTimeline. However, that doesn't mean the playhead won't actually go beyond where the last child tween sits. 

 

Does that clear things up? 

  • Like 1
Link to comment
Share on other sites

1 hour ago, avancamp said:

It is possible that I am somehow using an incorrect build of 3.1.2 beta. If you could send an npm-formatted zip of the latest build, I can try that in my real-world app and see if its behavior changes.

Sure, here's a tarball file you can install: https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/gsap-beta.tgz

Link to comment
Share on other sites

I was indeed not on that exact build, but this new build seems strange in that the .parent property is being unset at unexpected times? Here's the minimal repro:

 

import { gsap } from '../node_modules/gsap/index.js';

const header = document.createElement('h1');
header.textContent = 'Hello world.';
document.body.appendChild(header);

class Foo {
	constructor() {
		this.timeline = gsap.timeline();
		console.log('parent immediately after construction', this.timeline.parent);
	}

	right() {
		this.timeline.to(header, {
			x: 300,
			duration: 1
		});
		return this.timeline;
	}

	left() {
		this.timeline.to(header, {
			x: 0,
			duration: 1
		});
		return this.timeline;
	}
}

const instance = new Foo();

// This call works.
instance.right();

// This delayed call does not.
// Of note is that the timeline no longer has a parent.
setTimeout(() => {
	const anim = instance.left();
	console.log('parent 1.5s later', anim.parent);
}, 1500);

I can't reproduce this in a CodePen using https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/gsap-latest-beta.min.js, maybe it's not the same build as that zip you linked.

  • Thanks 1
Link to comment
Share on other sites

Got it, here's an even more minimal repro. The issue seems to occur when a timeline is created, but doesn't have any children added to it for a while.

 

import { gsap } from './package/index.js';

const header = document.createElement('h1');
header.textContent = 'Hello world.';
document.body.appendChild(header);

const timeline = gsap.timeline();
console.log('parent immediately after construction', timeline.parent);

// This delayed anim never plays.
setTimeout(() => {
	timeline.to(header, {
		x: 300,
		duration: 1
	});
	console.log('parent 1.5s later', timeline.parent);
}, 1500);

 

  • Thanks 1
Link to comment
Share on other sites

It may be rare in other codebases, but it is used heavily in mine haha. For whatever reason, it's a pattern I've found myself using extensively. Thanks for updating the build, will report back once I test it.

 

EDIT: Well, not "for whatever reason", the reason is that GSAP timelines are a great tool for creating long-running sequential queues of things, so I often have these timelines that live for the lifespan of the page which get things added to them when various events fire.

Link to comment
Share on other sites

Okay, that latest beta fixed all the new issues, thanks for that. I am now back on my original issue: .call being skipped.

 

I believe I have found a minimal repro. The problem is indeed caused by pause/resume. I believe the issue is that when progress(1) and/or timeScale(99) are used, increasingly large parts of the timeline are considered to be "under the playhead" in a given tick, and therefore more and more things get skipped when resume is invoked, due to it always setting suppressEvents = true.

 

This is a 3.1.1 CodePen, but the issue appears to be the same on 3.2.0-beta. Look at the console output to see if the test passed or failed:

 

EDIT: Also, if autoRemoveChildren: true is added to the timeline, it appears that onComplete gets called twice?

 

See the Pen xxGwOqY?editors=0010 by Lange (@Lange) on CodePen

 

Edited by avancamp
Discovered additional bug interaction with autoRemoveChildren
  • Thanks 1
Link to comment
Share on other sites

That's quite an interesting edge case - let me explain what was happening...

 

You've got a callback embedded in your timeline that has a pause() command inside it, and AFTER that you've got various other tweens/callbacks. So when you force the playhead to the very end with progress(1), it renders things in the proper order but as soon as it hits that callback that triggers a pause() (on the very timeline that's in the process of rendering), it halts the rendering process which is precisely what it should do. So the other stuff doesn't render. But the playhead is now all the way at the END (and it can't go any further). There's code in the rendering loop that basically says "if I'm asked to render at the same time as where the playhead currently is, just skip it because that's a waste of resources to do all that rendering when the playhead hasn't moved!" 

 

So when you resume() and the playhead starts moving again, it doesn't actually move because it's already at the end! That's why those remaining tweens/callbacks didn't render. It's like "hey, render at a progress of 1" and the timeline is like "dude, my playhead is already there...so I ain't gotta render anything."

 

It's a very tricky scenario actually because technically the playhead's position is only partially accurate (some of the children rendered, some didn't, because you paused part-way through a render). See what I mean? The best solution I could come up with is to sense that condition and shift the totalTime playhead back by 0.00000001. I think it's logically impossible to accommodate every aspect of this perfectly (the playhead can only be in one position after all). In my 12+ years of doing this, I don't think I've ever seen this scenario in a real-world project :) Congratulations, you're special. 

 

I've implemented this fix in the latest beta. Feel free to kick the tires. 

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

@GreenSock Thanks, I'll give it a whirl tomorrow morning and report back.

 

Also, if this edge case is just too strange to accommodate, I would accept something that prints a warning to the console advising against this behavior if GSAP detects this sort of thing. I don't mind changing my code, as long as I can't accidentally walk into this footgun again.

 

And yeah, broadcast graphics are super weird and require all kinds of weird uses of tooling haha. There's so many things I do that must seem absurd when viewed from a normal web dev perspective.

 

For example, I have graphics that ideally need to run for 7 full days straight. That's a very long-lived timeline, which is constantly reacting to events and queuing up animations. I also need to have systems which can immediately advance any animation to its end state, so that an operator can preview what the graphic will look like before they air it live. Stuff like that.

  • Like 1
Link to comment
Share on other sites

The issue has changed, but I don't think it is fixed. Here's another repro which adds autoRemoveChildren and re-orders the tweens/calls, and gets the issue to happen again. Of note is that if the final tween is commented out, the test passes. Very strange: 

 

import { gsap } from './package/index.js';

const header = document.createElement('h1');
header.textContent = 'Hello world.';
document.body.appendChild(header);

let called = false;
const timeline = gsap.timeline({
	autoRemoveChildren: true,
	onComplete() {
		report();
	}
});

timeline.call(async () => {
	timeline.pause();
	await waitForImageToLoad();
	timeline.resume();
});

timeline.call(() => {
	called = true;
});

timeline.to(header, {
	x: 300,
	duration: 1
});

// When this tween is commented out, the test passes.
timeline.to(header, {
	y: 100,
	duration: 1
});

timeline.progress(1);

function waitForImageToLoad() {
	return new Promise(resolve => {
		setTimeout(() => {
			resolve();
		}, 300);
	});
}

function report() {
	if (called) {
		console.log('Success!');
	} else {
		console.log('Failed, callback skipped.');
	}
}

 

  • Thanks 1
Link to comment
Share on other sites

1 hour ago, GreenSock said:

Gosh, you're good at finding obscure edge cases! That's a good thing :)

 

Haha, it comes from having used GSAP for about 6 years and having written many tens of thousands of lines of animation code with it. At this point I have a massive library of really complex graphics I can fire up which, for better or worse, strain parts of GSAP in ways they are perhaps not intended to be strained.

 

I have unfortunately found another repro, this one doesn't necessarily involve progress or timeScale either:

 

EDIT: Gosh I'm a fool, this repro makes no sense and will always fail lmao. Sorry, let me go try again...

 

 

Link to comment
Share on other sites

Okay, here's an actual repro that makes sense and won't waste your time lol:

 

import { gsap } from './package/index.js';

const header = document.createElement('h1');
header.textContent = 'Hello world.';
document.body.appendChild(header);
let called = false;

// On a timer because I'm not sure that putting this in
// an onComplete does the right thing in this test case.
setTimeout(report, 1000);

const parent = gsap.timeline();

parent.add(makeChild());

// Yes, this being delayed is part of the repro.
requestAnimationFrame(() => {
	parent.progress(1);
});

function makeChild() {
	const tl = gsap.timeline();

	tl.call(async () => {
		tl.pause();
		await waitForImageToLoad();
		tl.resume();
	});

	console.log('Creating grandchild timeline and adding it to the child timeline...');
	// This entire grandchild timeline is skipped.
	tl.add(makeGrandChild());

	return tl;
}

function makeGrandChild() {
	const tl = gsap.timeline();

	tl.to(header, {
		duration: 0.2,
		rotation: 15,
		x: 100,
		y: 150
	});

	tl.call(() => {
		called = true;
	});

	return tl;
}

function waitForImageToLoad() {
	return new Promise(resolve => {
		setTimeout(() => {
			resolve();
		}, 300);
	});
}

function report() {
	if (called) {
		console.log('Success!');
	} else {
		console.log('Failed, callback skipped.');
	}
}

 

  • Thanks 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...