Offsetting all keyframes - please take a look at this script

Find out why the . goes before the /

Moderator: Paul Tuersley

Post Reply
Redsandro
Posts: 108
Joined: June 25th, 2008, 4:55 pm

We are trying to offset all keyframes by x frames and we came up with this script, but it will make AE CS3 hang in an infinite loop.
Basically it finds keyframes, sets new ones at the offset time and deletes the old ones. Sounds pretty easy but obviously there's something wrong with the script.
Can you see what the problem is?

There seems to be an infinite loop where for (p = 1; p <= pGroup.numProperties; p++) keeps getting an infinite number of "Time Remap" and "Motion Trackers". I don't know why.

Code: Select all

/**********
 *	Offset Keyframes
 **********
 *	Filename:		Rs - Offset Keyframes.jsx
 *	Written by:		Redsandro - http://www.rednet.nl/en/
 *	Date:			2010/01/11
 *	Last updated:	2010/01/11
 **********
 *
 *	Description:
 *
 *	Offset all keyframes of a layer for fixing different-length replacement
 *
 **********/



clearOutput();

var myComp = app.project.activeItem;
var errors = new Array();

if (myComp == null || !(myComp instanceof CompItem)) {
	alert("No composition selected.");
}
else if (myComp.selectedLayers.length == 0) {
	alert("Select one or more layers to offset keyframes of.");
}
else {
	// Vars
	var selLays = myComp.selectedLayers
	var fDuration = myComp.frameDuration;
	var propsCount = 0;
	var offsetCount = 25;
	offsetCount = parseInt(prompt("Offset keys how many frames? (New footage less handle (1 minute), keys go backwards (-1500).)", offsetCount));
	var offset = offsetCount * fDuration;
	if (offset == 0) exit;

	app.beginUndoGroup("Rs - Offset Keyframes");

	// Walk layers 
	for (var l = 0; l < selLays.length; l++) { // Layers Zero based!
		// Vars
		var lay = selLays[l];
		alert(lay.numProperties);

		// Walk props
		propOffset(lay, offset)
	}

	app.endUndoGroup();
}

function propOffset(pGroup, offset) {
	for (p = 1; p <= pGroup.numProperties; p++) { // Properties NON-Zerobased
		var prop = pGroup.property(p);
		if (prop instanceof PropertyGroup)
			propOffset(prop, offset);
		else if (prop instanceof Property) {
			if (prop.canVaryOverTime && prop.numKeys > 0) {
				// walk keyframes
				for (k = 1; k <= prop.numKeys; k++) { // NON-Zerobased
					// Walk forewards or backwards
					if (offset > 0)	// Start with last key if keys are moving forward
						k = prop.numKeys - k + 1;
					//var key = prop(k);
					// clone key
					var time = prop.keyTime(k);
					prop.setValueAtTime(time+offset, prop.valueAtTime(time, false));
					prop.removeKey(k);
				}
			}
		}
		else if (p.name = "Marker") {
			//SELFNOTE: Something with markers.. I hardly use them.
			// Scripting guide p. ~99.
			// PseudoCode:
			// foreach something:
			// p.setValueAtTime(OldTime+offset, OldMarkerValue);
			// p.removeMarker(oldTime);
		}
	}
}
avi + m2t -> Vdub + DGIndex -> AE CS3 -> x264 -> Hell On Earth :mrgreen:
Redsandro
Posts: 108
Joined: June 25th, 2008, 4:55 pm

I got it.

for (p = 1; p <= pGroup.numProperties; p++)
should be
for (var p = 1; p <= pGroup.numProperties; p++)

Otherwise (I didn't know this until my headache discovered there was no other explanation) p is declared global, making each subcall of the function reference the old value of p, thus infinite loop.

But it still doesn't work. Now it does nothing. (It does all the walking and stuff but it doesn't see my mask path keyframes for example).
avi + m2t -> Vdub + DGIndex -> AE CS3 -> x264 -> Hell On Earth :mrgreen:
Redsandro
Posts: 108
Joined: June 25th, 2008, 4:55 pm

Oh I get it.. I was testing on mask path keyframes, but other keyframes get detected. Mask paths keys don't.

Any idea why?
avi + m2t -> Vdub + DGIndex -> AE CS3 -> x264 -> Hell On Earth :mrgreen:
Redsandro
Posts: 108
Joined: June 25th, 2008, 4:55 pm

Even then, it only moves the last keyframe.
I give up. :cry:
avi + m2t -> Vdub + DGIndex -> AE CS3 -> x264 -> Hell On Earth :mrgreen:
Paul Tuersley
Posts: 704
Joined: June 5th, 2004, 7:59 am
Location: London, UK

I didn't dig through to find out exactly why it wasn't working, but a couple of issues I spotted are:
1. You're doing a loop through prop.numKeys using k, but when moving keyframes forward you're changing the k value in a way that looks like it would potentially mess up your loop.
2. I've been told by one of the AE team that using valueAtTime to set lots of keyframes is not great when used in conjunction with undoGroup, because AE stores an "undo" copy of the whole property every time you set a value, even a single key. This can slow things down, and potentially cause AE to run out of memory due to the size of the undoGroup cache. So it's better to put all the times and values into two arrays, then set all the keys for a property in one go using setValuesAtTimes(timesArray, valuesArray).

Here's a version of the script that seems to work ok. Try uncommenting out the $.writeln lines to see all the properties the script is looping through in the ESTK console. You can't use setValuesAtTimes for Markers, so I've used setValueAtTime for them.

Code: Select all

/**********
*   Offset Keyframes
**********
*   Filename:      Rs - Offset Keyframes.jsx
*   Written by:      Redsandro - http://www.rednet.nl/en/
*   Date:         2010/01/11
*   Last updated:   2010/01/11
**********
*
*   Description:
*
*   Offset all keyframes of a layer for fixing different-length replacement
*
**********/



clearOutput();

var myComp = app.project.activeItem;
var errors = new Array();

if (myComp == null || !(myComp instanceof CompItem)) {
   alert("No composition selected.");
}
else if (myComp.selectedLayers.length == 0) {
   alert("Select one or more layers to offset keyframes of.");
}
else {
   // Vars
   var selLays = myComp.selectedLayers
   var fDuration = myComp.frameDuration;
   var propsCount = 0;
   var offsetCount = 25;
   offsetCount = parseInt(prompt("Offset keys how many frames? (New footage less handle (1 minute), keys go backwards (-1500).)", offsetCount));
   var offset = offsetCount * fDuration;
   if (offset == 0) exit;

   app.beginUndoGroup("Rs - Offset Keyframes");

   // Walk layers 
   for (var l = 0; l < selLays.length; l++) { // Layers Zero based!
      // Vars
      var lay = selLays[l];
      alert(lay.numProperties);

      // Walk props
      propOffset(lay)
   }

   app.endUndoGroup();
}

function propOffset(pGroup) {
	//$.writeln("in propOffset, pGroup = " + pGroup.name);
	var valuesArray = new Array();
	var timesArray = new Array();
	for (var p = 1; p <= pGroup.numProperties; p++) { // Properties NON-Zerobased
		var prop = pGroup.property(p);
		//$.writeln("prop = " + prop.name);
		if (prop instanceof PropertyGroup) propOffset(prop);
		
		else if (prop instanceof Property) {
			
			if (prop.canVaryOverTime && prop.numKeys > 0) {
				// walk keyframes
				while (prop.numKeys > 0) {
					valuesArray.push(prop.keyValue(prop.numKeys));
					timesArray.push(prop.keyTime(prop.numKeys) + offset);
					prop.removeKey(prop.numKeys);
				}
			
				if (prop.name == "Marker") {
					for (var x = 0; x < valuesArray.length; x++) {
						prop.setValueAtTime(timesArray[x], valuesArray[x]);
					}
				} else {
					prop.setValuesAtTimes(timesArray, valuesArray);
				}
				valuesArray.length = 0;
				timesArray.length = 0;
			}
		}
	}
}
Paul Tuersley
Posts: 704
Joined: June 5th, 2004, 7:59 am
Location: London, UK

After posting the script I reread the thread and saw your comment about Mask Paths, which didn't work in my first attempt either. Here's a new version that seems to work ok for mask keyframes. I left in but commented out the $.writeln code I used to help figure that out.

Code: Select all

/**********
*   Offset Keyframes
**********
*   Filename:      Rs - Offset Keyframes.jsx
*   Written by:      Redsandro - http://www.rednet.nl/en/
*   Date:         2010/01/11
*   Last updated:   2010/01/11
**********
*
*   Description:
*
*   Offset all keyframes of a layer for fixing different-length replacement
*
**********/



clearOutput();

var myComp = app.project.activeItem;
var errors = new Array();

if (myComp == null || !(myComp instanceof CompItem)) {
   alert("No composition selected.");
}
else if (myComp.selectedLayers.length == 0) {
   alert("Select one or more layers to offset keyframes of.");
}
else {
   // Vars
   var selLays = myComp.selectedLayers
   var fDuration = myComp.frameDuration;
   var propsCount = 0;
   var offsetCount = 25;
   offsetCount = parseInt(prompt("Offset keys how many frames? (New footage less handle (1 minute), keys go backwards (-1500).)", offsetCount));
   var offset = offsetCount * fDuration;
   if (offset == 0) exit;

   app.beginUndoGroup("Rs - Offset Keyframes");

   // Walk layers 
   for (var l = 0; l < selLays.length; l++) { // Layers Zero based!
      // Vars
      var lay = selLays[l];
      alert(lay.numProperties);

      // Walk props
      propOffset(lay)
   }

   app.endUndoGroup();
}

function propOffset(pGroup) {
	//$.writeln("in propOffset, pGroup = " + pGroup.name);
	var valuesArray = new Array();
	var timesArray = new Array();
	for (var p = 1; p <= pGroup.numProperties; p++) { // Properties NON-Zerobased
		var prop = pGroup.property(p);
		
		//$.writeln("prop = " + prop.name);
		//$.writeln(prop.propertyType == PropertyType.INDEXED_GROUP);
		//$.writeln(prop.propertyType == PropertyType.NAMED_GROUP);
		//if (prop.name == "Mask 1") {
		//	$.writeln(prop.numProperties);
		//	$.writeln(prop instanceof Property);
		//}
		if (prop.propertyType == PropertyType.INDEXED_GROUP || prop.propertyType == PropertyType.NAMED_GROUP) propOffset(prop);
		//if (prop instanceof PropertyGroup) propOffset(prop);
		
		else if (prop instanceof Property) {
			//$.writeln(prop.name  + " is a property");
			//$.writeln("can vary =" + prop.canVaryOverTime + " numkeys = " + prop.numKeys);
			
			if (prop.canVaryOverTime && prop.numKeys > 0) {
				// walk keyframes
				while (prop.numKeys > 0) {
					valuesArray.push(prop.keyValue(prop.numKeys));
					timesArray.push(prop.keyTime(prop.numKeys) + offset);
					prop.removeKey(prop.numKeys);
				}
			
				if (prop.name == "Marker") {
					for (var x = 0; x < valuesArray.length; x++) {
						prop.setValueAtTime(timesArray[x], valuesArray[x]);
					}
				} else {
					prop.setValuesAtTimes(timesArray, valuesArray);
				}
				valuesArray.length = 0;
				timesArray.length = 0;
			}
		}
	}
}
Redsandro
Posts: 108
Joined: June 25th, 2008, 4:55 pm

Wow...
This could have saved me a lot of work a few weeks back, which I unfortunately had to do manually because I couldn't figure it out. But it's awesome that you fixed it! I will definitely store this for my next possible need of moving them. And thanks for the advice on arrays, it explains why some of my other scripts went crazy slow.

I haven't seen the $.writeln before in anything other than jQuery, which is awesome but totally unrelated. Not heard about ESTK console either. Is it new? Sounds like it makes debugging easier.

-edit-

Nicely done how you handle the keyframe copy loop!
Last edited by Redsandro on January 30th, 2010, 2:51 pm, edited 1 time in total.
avi + m2t -> Vdub + DGIndex -> AE CS3 -> x264 -> Hell On Earth :mrgreen:
Paul Tuersley
Posts: 704
Joined: June 5th, 2004, 7:59 am
Location: London, UK

It's one of the panels in the ExtendScript Toolkit. It means you can get far more information than is practical when using alerts dialogs.
Redsandro
Posts: 108
Joined: June 25th, 2008, 4:55 pm

I never checked it out because I never got the extend toolkit thing to open. (Automatically installed with the suite, right?).
But that was back in the XP days, I run Win7 now. New round new chances so I'll check it out during my next scripting.

(CS3 still. My money will skip CS4. I think the true improvement we can use is 64 bit (CS5) support)

-edit-

And thanks for fixing the marker thing. :D

-edit-

Last question in this regard, how come naming the types of property groups fixes the ability to get to the mask keys?

Code: Select all

if (prop.propertyType == PropertyType.INDEXED_GROUP || prop.propertyType == PropertyType.NAMED_GROUP)
vs

Code: Select all

if (prop instanceof PropertyGroup)
Just trying to understand why I don't get it. :)
avi + m2t -> Vdub + DGIndex -> AE CS3 -> x264 -> Hell On Earth :mrgreen:
Paul Tuersley
Posts: 704
Joined: June 5th, 2004, 7:59 am
Location: London, UK

I don't know. But it was coming up as false for the Mask 1 group being a PropertyGroup, so I tried something else.
kikourof
Posts: 9
Joined: August 27th, 2007, 12:47 pm

Hello guys,

I came to this thread because I'm looking for a way to copy all the keyframe information from a series of mask paths pertaining to a single layer and then paste all these keyframes as position property for another layer.

The trick is that I have a lot of mask paths to copy (more than 50) and doing it manually is a nightmare.

I was surprised to see that copying and pasting the mask path into the clipboard was not working, so I guess there are some unusual things about these keyframes that don't behave as other property's keyframes under after effects; so I was hoping expressions could come to rescue ;)

I saw that your script in that thread that seems to be able to pick the mask path keyframe information so I was wondering if you could lead me to a script that you might know of and that could do what I'm looking for.

I searched the entire internet without any luck so far.

Thanks for your help,

Arnaud
Redsandro
Posts: 108
Joined: June 25th, 2008, 4:55 pm

You've got the right topic for the main loop, but I'm guessing you're looking for some more advanced processing.
If you somehow want to copy mask information to position information, you'll have to figure out a way to convert the very complicated data of a mask keyframe to simple 2d or 3d position data. It's not like a mask has a position value.

So even though I'm not sure what exactly you want, as 'mask to position' is open for interpretation, I also wouldn't know how to make that step as I've never worked with the data inside a mask keyframe.

But I've seen some mask processing scripts. Although they do not do what you want, it looks like working with the data inside such a keyframe is possible and therefore making a custom conversion to however you want your position data is certainly possible.

Sorry I couldn't be of more help.
avi + m2t -> Vdub + DGIndex -> AE CS3 -> x264 -> Hell On Earth :mrgreen:
Post Reply