• Global community
    • Language:
      • Deutsch
      • English
      • Español
      • Français
      • Português
  • 日本語コミュニティ
    Dedicated community for Japanese speakers
  • 한국 커뮤니티
    Dedicated community for Korean speakers
Exit
0

Set/Get value at frame - impossible?

Community Beginner ,
Dec 15, 2020 Dec 15, 2020

Copy link to clipboard

Copied

I'm trying to set/get the value of a property at a given frame, but I haven't found a solution that works for all frame-rates/durations/comp start times. The keyframes often end up between frames (or not exactly on the frame).

 

Below is sample code that I expect to work, but it doesn't. It always fails when I change the frame rate and/or compStartTime to something else than a standard 24/25fps. Same thing applies for both set/get value. In this example I set up a comp with a frame rate of 29.97 and a start-frame of 1001.

 

function frameToTime(comp, frame){
return frame*comp.frameDuration;
}

var comp = app.project.activeItem;
var layer = comp.layer(1);

var frame = 1041;

// Calculate the time and the offset from the display start time.
var time = frameToTime(comp, frame) - comp.displayStartTime

layer.rotation.setValueAtTime(time, 100);

 

I've found a number of links that talk about percision errors with frame to time calculations, but not really a solution. Is there one?

 

Any suggestion?

 

TOPICS
Expressions , Scripting

Views

1.4K

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
LEGEND ,
Dec 15, 2020 Dec 15, 2020

Copy link to clipboard

Copied

If you're working with crooked framerates you need to acount for that drop-/non-drop frame thing which I believe the the timecode functions already do, but not the normal time. So instead of using actual time, you may need to use timecode functions and retireve the time value they spit out.

 

Mylenium

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Beginner ,
Dec 15, 2020 Dec 15, 2020

Copy link to clipboard

Copied

Thanks for the reply Mylenium.

Yes, the drop frames might be an issue, but it also happens with "normal" frame-rates.

For example, I created a comp with a frame rate of 24 and a start frame of 801 and duration of 100 frames. I then executed the code snippet above with a frame value of 831. The resulting keyframe is not set correctly, it's created between 831 and 832.

 

I've tried with a number of "rounding" fixes, adding/subtracting 0.0001 etc, but nothing has worked so far in all cases.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Valorous Hero ,
Dec 15, 2020 Dec 15, 2020

Copy link to clipboard

Copied

Have you tried using a Marker instead of a KF?

Motion Graphics Brand Guidelines & Motion Graphics Responsive Design Toolkits

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Enthusiast ,
Dec 16, 2020 Dec 16, 2020

Copy link to clipboard

Copied

I can confirm that this imprecise keyframe placement thing is an issue. I've had to tackle it in many of my scripts. One solution I use is to create all the keyframes then go back through and check their timings and if they're off I remove them and replace them with one with something like a -0.0001 offset. At least that way the result of the keyframe value is being displayed on the desired frame, and if the user changes the value it will add a new keyframe at the correct time, fractionally after the scripted one, so it will corrected replace that value.

 

Don't think I've tried this but one idea would be to do something else like having the script trim the layer then call the Convert Expression To Keyframes function to create a correct keyframe placement you could then alter using setValueAtKey. Horribly laborious but then I've done worse to get around AE's scripting foibles!

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Beginner ,
Dec 16, 2020 Dec 16, 2020

Copy link to clipboard

Copied

Thanks for confirming Paul. I actually made some tests yesterday with the same method you suggested. It actually seems to work quite well. Basically, I have the script create a null object with an exporession on some property then bake that to keyframes. I can then generate a list of times for all frames. Something like this:

 

	function generateTimeList(comp){				
		var helper = comp.layers.addNull(comp.duration);
		var prop = helper.rotation;
		prop.expression = 1;
		prop.selected = true;
		app.executeCommand(app.findMenuCommandId("Convert Expression to Keyframes"));
		
		var times = []	
		for(var i =1 ; i<=prop.numKeys;i++){
			// Get the time from index.
			t = prop.keyTime(i);
			times.push(t);		
		}
	
		helper.remove()
	
		return times

 

As long as there's a reliable way to get the startFrame of a comp, this should work. Then you can just get the time by calling:

var times = generateTimeList(comp)
var frame = 931
var startFrame = 500
var time = times[frame-startFrame]

I see that a comp.displayStartFrame was added in 17.1 so that should be safe (haven't tested though). In earlier version it's still needed to convert the displayStartTime to frames first. I'll need to do some more tests with that and see if there are issues. If there ares issues, I guess it's possible to reset the comp start time to 0 before running the script and then reset after.

 

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Beginner ,
Dec 16, 2020 Dec 16, 2020

Copy link to clipboard

Copied

So I did some more tests, and using the "helper" approach to create a layer, bake keyframes and then read the time values on those keyframes seems quite solid. I've tested with different frame rates, different start times etc and all seems to work. It's of course a bit slower, but in my use case it's better to be slow than to fail.

 

If anyone is intrested, here's a helper class with a few examples at the bottom.

 

function CompHelper(comp){
	// Helper class for working with frames (instead of time) in After Effects.
	// Simon Bjork
	// December 2020
	// bjork.simon@gmail.com
	
	// There are a few bugs in After Effects that prevents an easy frame>time conversion.
	// The biggest issue is that there are rounding errors in the internal calculations that make keyframes to be added between frames.
	
	if(!(comp instanceof CompItem)){
		alert("No CompItem")
		return false;
	}

	// Initialize the comp.	
	this.comp = comp
	getCompData();
	
	// Class variables.	
	this.update = update;	
	this.setStartFrame = setStartFrame;
	this.getStartFrame = getStartFrame;
	this.getFrameRange = getFrameRange;
	this.getTime = getTime;
	this.valueAtFrame = valueAtFrame
	this.valuesAtFrames = valuesAtFrames;
	this.setValueAtFrame = setValueAtFrame;
	this.setValuesAtFrames = setValuesAtFrames;
	this.setDurationFrames = setDurationFrames;
	
	function getCompData(){
		this.width = this.comp.width;
		this.height = this.comp.height;
		this.par = this.comp.pixelAspect;
		this.fps = this.comp.frameRate;
		this.duration = this.comp.duration;
		this.frameDuration = this.comp.frameDuration;
	
		this.frameRange = getFrameRange(this.comp)	
		this.first = this.frameRange[0];
		this.last = this.frameRange[1];
		
		this.startFrame = getStartFrame();
		this.times = generateTimeList()
	} // function getCompInfo(){
	
	function update(){
		// Better name for external use.
		getCompData()
	} // function update(){

	function generateTimeList(){
		// Generate a time value for each frame in the composition.	
		
		// Create helper and generate keyframes.
		var helper = this.comp.layers.addNull(this.duration);
		var prop = helper.rotation;
		prop.expression = 1;
		prop.selected = true;
		app.executeCommand(app.findMenuCommandId("Convert Expression to Keyframes"));
		
		// Loop over the keyframes and collect the time values.		
		var times = []	
		for(var i =1 ; i<=prop.numKeys;i++){
			// Get the time from index.
			t = prop.keyTime(i);
			times.push(t);		
		}			
		helper.remove()		
		return times
	} // function generateTimeList(frames){
	
	function timeToFrames(time){
		return Math.round(time/this.frameDuration);
	} // function timeToFrames(time){

	function framesToTime(frame){
		return frame*this.frameDuration;
	} // function framesToTime(frame){

	function getStartFrame(){
		// Alternative for AE 17.1: startFrame = (app.project.displayStartFrame) + (this.comp.displayStartTime/this.frameDuration)		
		var startSec = this.comp.displayStartTime		
		var startFrameCalc = (startSec / this.frameDuration) + app.project.displayStartFrame;
		var startFrame = Math.floor(startFrameCalc);		
		return startFrame;		
	} // function getStartFrame(){

	function getFrameRange(comp){
		// Get the first/last frame of the comp.
		var first = getStartFrame();
		var last = first + timeToFrames(comp.duration, comp) -1;
		return [Math.floor(first), Math.floor(last)];
	} // function getFrameRange(comp){
	
	function setStartFrame(frame){
		if(app.project.displayStartFrame == 1){
			var frame = frame-1
		}
		var startFrameSec = framesToTime(frame, comp) + 0.00001;
		if(startFrameSec < 0){
			var startFrameSec = 0;
		}
		this.comp.displayStartTime = startFrameSec;
	} // function setStartFrame(comp, frame){

	function getTime(frame){
		var time = this.times[frame-this.startFrame]
		return time;
	} // function getTime(frame){
		
	function setValueAtFrame(property, frame, value){
		var t = getTime(frame);
		property.setValueAtTime(t, value);
	} // function setValueAtFrame(property, frame, value){

	function setValuesAtFrames(property, frames, values){
		// Set values at multiple frames at the same time.
		// Much faster than calling setValueAtFrame multiple times.		
		if(frames.length != values.length){
			alert("Frames and values are not the same length");
			return
		}		
		var times = []
		for(var i=0; i<frames.length;i++){
			var frame = frames[i];
			var t = getTime(frame);
			times.push(t);			
			}
		property.setValuesAtTimes(times, values);		
	} // function setValuesAtFrames(property, frames, values){

	function valueAtFrame(prop, frame){
		// Get value at frame.
		var t = getTime(frame);
		var val = prop.valueAtTime(time, false);		
		return val	
	} // function valueAtFrame(prop, frame){

	function valuesAtFrames(prop, frames){
		var values = []
		for(var i=0; i<frames.length;i++){
			var frame = frames[i];
			var t = getTime(frame);
			var value = prop.valueAtTime(t, false);
			values.push(value);			
			}
		return values
	} // function valuesAtFrames(prop, frames){

	function setDurationFrames(frames, reload){
		// Set duration to a comp in frames.
		this.comp.duration = framesToTime(frames)
		
		if(reload){
			update();
		}
	} // function setDuration(frame, doReload){

	// Return the whole thing.	
	return this	
	
	} // function CompHelper(comp){


// ----------------------------------------------------------------

// Example use cases.

var comp = app.project.activeItem;
var c = CompHelper(comp)
var layer = c.layer(1);

// Set start frame of a comp.
c.setStartFrame(36);

// Set a value at frame.
c.setValueAtFrame(layer.rotation, 1005, 20);

// Set values at frames.
c.setValuesAtFrames(layer.rotation, [1003, 1004], [10, 20]);

// And more.

 

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Enthusiast ,
Dec 16, 2020 Dec 16, 2020

Copy link to clipboard

Copied

Looks interesting. I've had issues though where despite using the time of an existing correctly timed keyframe it still ends up putting it in a slightly different position. You create a keyframe at a specific time, then read back that time from the keyframe and it's different to what you told it to be. This seems to happen on longer sequences.

 

As an example, create a comp duration 20000 frames, add a solid, set an opacity keyframe and default expression, use Convert Expression to Keyframes, then with the property selected run this:

var activeItem = app.project.activeItem;
var selectedProp = activeItem.selectedProperties[0];
var timeArray = new Array();
var valueArray = new Array();

for (var x = 1; x <= selectedProp.numKeys; x++) {
	timeArray.push(selectedProp.keyTime(x));
	valueArray.push(selectedProp.keyValue(x));
}
$.writeln("there are " + selectedProp.numKeys + " keyframes");
selectedProp.setValuesAtTimes(timeArray, valueArray);
$.writeln("there are " + selectedProp.numKeys + " keyframes");

	

 

It just reads all the existing keyframe values and times then sets them again. It works as expected up to somewhere between 15000 and 20000 frames at which point I get:

there are 20000 keyframes

there are 21470 keyframes

You'll then find plenty of later keyframes where if you drag them you'll find another sitting in almost the same position. It can be a real pain!

 

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Beginner ,
Dec 16, 2020 Dec 16, 2020

Copy link to clipboard

Copied

Ouch, that sounds like pain indeed. How could you even fix that in a secondary loop?

 

Anyway, I did a few tests with the code above and it does seem to set keyframes correctly. It's a bit slow though. Tested in both 2019 and 2020.

 

If you find the time, try creating a new comp and set duration to 30000 frames, add a solid and then run this (along with the class above).

 

var comp = app.project.activeItem;
var c = CompHelper(xx)

var layer = comp.layer(1);
c.setValuesAtFrames(layer.rotation, [8801, 12330, 22346, 27998], [10, 20, 30, 40]);

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Beginner ,
Dec 16, 2020 Dec 16, 2020

Copy link to clipboard

Copied

Also this could be intresting, to set the value at each frame.

 

var comp = app.project.activeItem;
var c = CompHelper(comp)

var frames = []
var vals = []

for(var i=c.first; i<=c.last;i++){
	frames.push(i)
	vals.push(i*0.0001);
	}
	
c.setValuesAtFrames(layer.rotation, frames, vals)

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Dec 17, 2020 Dec 17, 2020

Copy link to clipboard

Copied

I'm having a really hard time following the problem in this post. Keyframes are set at a time value, not a frame value. A keyframe is added at the position of the current-time indicator. If you set a keyframe at 1 second and change the framerate from 24 to 900, the keyframe will still be at 1 second.

 

If you have a comp that is 24 fps and you set a keyframe at frame 7 then the time will be 3.428571428571429 seconds. Change the frame rate to 30 fps and the keyframe will still be at 3.428571428571429 seconds but the frame number will be 9 because that's the closest that you can get to 3.428571428571429 seconds and have the Current Time Indicator at the start of a frame. The timing of the animation will be as accurate as it can be to the original timing given the change in frame rate. If you are animating position or rotation or anything else, the timing will be as accurate as it can be to the original timing given the start point of each frame, because nothing moves changes in a frame. 

 

If you edit the frame value of a keyframe that happens to be not right at the start of a frame, the new keyframe will move and be lined up with the current frame. Again, there will be no difference in the timing, it will be as close as the frame rate allows it to be to the desired time. Nothing in comp will be fouled up if the keyframes do not line up with the start of the current frame.

 

If you start with a low frame rate, like 24, and you up the frame rate to more than double that frame rate, then you will have the ability to put a glitch in animation because there is more than one frame between the original keyframe and the new one added at the current time marker.

 

It also sounds like there is some confusion about how drop-frame timecode works.  NO frames are ever dropped. Only frame numbers. When color was added to the television signal they had to add some additional scan lines to the signal so they had to drop the frame rate enough to include those extra scan lines. If the counter that counts frames did not skip a few numbers by the time you were up to one minute you would already be off by 2 frames. Check the time display at 29.97 fps and you will see that the clock skips from 0;00;59;29 to 0;00;01;02 so the time is as accurate to the second and only off by the frame number by a tiny fraction. One more time, no frames are missing, only frame numbers. There is no such thing as 0;00;01;00 when you have drop frame timecode. 

 

If you set your script to use time instead of frames you would avoid any timing problems and any animation would be as accurate as it could be based on the time. 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Enthusiast ,
Dec 17, 2020 Dec 17, 2020

Copy link to clipboard

Copied

LATEST

The biggest issues I've had are with text keyframes. You might use a script to edit existing text keyframes but, even though you're telling it to replace the keyframe at the time of the existing keyframe, it gets added slightly before. In that case the edited value never gets seen. The original keyframe on the exact keyframe boundary still takes precedent. Or it gets added slightly after, so you still see the unedited text on that once frame before the change kicks in on the next.

 

In the case of my subtitles scripts, you're adding keyframes to a fresh layer but the keyframe ends up just fractionally after a frame boundary.  If you jump to that keyframe it will actually jump to the correct keyframe boundary just before the key. Editing the text will create a new keyframe on the exact frame boundary but the original keyframe value (that still exists although there's no way to see that in the UI) will kick back in from the next frame.

 

This isn't related to changing frame rates. Simply that in some circumstances, seemingly with longer duration comps (as you might expect in subtitling jobs), AE's scripting flaws in being able to set a keyframe on an exact frame boundary can cause major problems. And workarounds (such as baking in all keyframes first then editing them one at a time) will introduce massive performance penalties compared to setting them all at once using valuesAtTimes, like increasing time taken from seconds to minutes.

 

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines