├── .gitignore ├── examples ├── getAllPropertyNames.jsx ├── getAllProperties.jsx ├── multipleLayers.jsx └── findStrokes.jsx ├── README.md └── src └── PropertyTreeLooper.jsx /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /examples/getAllPropertyNames.jsx: -------------------------------------------------------------------------------- 1 | // Get all property names of a selected layer (without the layer itself or property groups) 2 | 3 | $.evalFile(File("../src/PropertyTreeLooper.jsx").fsName); // include the PropertyTreeLooper class 4 | 5 | (function () { 6 | var comp = app.project ? app.project.activeItem : null; 7 | if (!(comp instanceof CompItem)) { 8 | alert("Please select a composition first."); 9 | return; 10 | } 11 | 12 | var firstSelectedLayer = comp.selectedLayers[0]; 13 | if (!firstSelectedLayer) { 14 | alert("Please select a layer first."); 15 | return; 16 | } 17 | 18 | 19 | var names = []; 20 | var looper = new PropertyTreeLooper(); // create a new instance of the PropertyTreeLooper class 21 | looper.onPropertyFound = function (prop) { 22 | names.push(prop.name); 23 | }; 24 | looper.loop(firstSelectedLayer); 25 | 26 | 27 | alert(names); 28 | 29 | 30 | })(); 31 | -------------------------------------------------------------------------------- /examples/getAllProperties.jsx: -------------------------------------------------------------------------------- 1 | // Get all properties of a selected layer (without the layer itself or property groups) 2 | 3 | $.evalFile(File("../src/PropertyTreeLooper.jsx").fsName); // include the PropertyTreeLooper class 4 | 5 | (function () { 6 | var comp = app.project ? app.project.activeItem : null; 7 | if (!(comp instanceof CompItem)) { 8 | alert("Please select a composition first."); 9 | return; 10 | } 11 | 12 | var firstSelectedLayer = comp.selectedLayers[0]; 13 | if (!firstSelectedLayer) { 14 | alert("Please select a layer first."); 15 | return; 16 | } 17 | 18 | 19 | var properties = []; 20 | var looper = new PropertyTreeLooper(); // create a new instance of the PropertyTreeLooper class 21 | looper.onPropertyFound = function (prop) { 22 | properties.push(prop); 23 | }; 24 | looper.loop(firstSelectedLayer); 25 | 26 | 27 | alert("Found " + properties.length + " properties."); 28 | 29 | 30 | })(); 31 | -------------------------------------------------------------------------------- /examples/multipleLayers.jsx: -------------------------------------------------------------------------------- 1 | // Scann all properties of selected layers and count them 2 | 3 | $.evalFile(File("../src/PropertyTreeLooper.jsx").fsName); // include the PropertyTreeLooper class 4 | 5 | (function () { 6 | var comp = app.project ? app.project.activeItem : null; 7 | if (!(comp instanceof CompItem)) { 8 | alert("Please select a composition first."); 9 | return; 10 | } 11 | 12 | var selectedLayers = comp.selectedLayers; 13 | if (!selectedLayers || selectedLayers.length === 0) { 14 | alert("Please select some layers first."); 15 | return; 16 | } 17 | 18 | 19 | var numPropertiesAcrossSelectedLayers = 0; 20 | var looper = new PropertyTreeLooper(); // create a new instance of the PropertyTreeLooper class 21 | looper.onPropertyFound = function (prop) { 22 | numPropertiesAcrossSelectedLayers++; 23 | }; 24 | 25 | for (var i = 0; i < selectedLayers.length; i++) { 26 | looper.loop(selectedLayers[i]); 27 | } 28 | 29 | 30 | alert("Number of properties across selected layers: " + numPropertiesAcrossSelectedLayers); 31 | 32 | 33 | })(); 34 | -------------------------------------------------------------------------------- /examples/findStrokes.jsx: -------------------------------------------------------------------------------- 1 | // Scann all selected layers for Stroke property groups 2 | 3 | $.evalFile(File("../src/PropertyTreeLooper.jsx").fsName); // include the PropertyTreeLooper class 4 | 5 | (function () { 6 | var comp = app.project ? app.project.activeItem : null; 7 | if (!(comp instanceof CompItem)) { 8 | alert("Please select a composition first."); 9 | return; 10 | } 11 | 12 | var selectedLayers = comp.selectedLayers; 13 | if (!selectedLayers || selectedLayers.length === 0) { 14 | alert("Please select some layers first."); 15 | return; 16 | } 17 | 18 | 19 | var strokePropertyGroups = []; 20 | var looper = new PropertyTreeLooper(); // create a new instance of the PropertyTreeLooper class 21 | looper.onGroupFound = function (prop) { 22 | if (prop.matchName === "ADBE Vector Graphic - Stroke") { 23 | strokePropertyGroups.push(prop); 24 | } 25 | }; 26 | 27 | for (var i = 0; i < selectedLayers.length; i++) { 28 | looper.loop(selectedLayers[i]); 29 | } 30 | 31 | if (strokePropertyGroups.length === 0) { 32 | alert("No Stroke property groups found."); 33 | return; 34 | } else { 35 | alert("Number of Stroke property groups found: " + strokePropertyGroups.length); 36 | } 37 | 38 | // Now do something with the strokePropertyGroups array.... 39 | 40 | })(); 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExtendScript Recursive Property Looper 2 | (*For After Effects*) 3 | 4 | When developing for After-Effects it is often the case that we want to loop through all sub properties of a layer or a group, perhaps to find a specific property, to check if it exists, or to gather all properties of a certain type. 5 | 6 | With this class ( `PropertyTreeLooper` ) you can do just that. 7 | 8 | 9 | 10 | ## Usage 11 | 12 | 1. Include the script in your project as a file and import it: 13 | ```jsx 14 | $.evalFile(File("../*PATH_TO*/PropertyTreeLooper.jsx").fsName); // include the PropertyTreeLooper class 15 | ``` 16 | or simply [copy and paste the class into your script](./src/PropertyTreeLooper.jsx) 17 | 18 | 19 | 2. Create a new instance of the class: 20 | ```jsx 21 | var looper = new PropertyTreeLooper(); 22 | ``` 23 | 24 | 3. set a callback for everytime a property of any kind is found: 25 | ```jsx 26 | looper.onAny = function(prop) { 27 | // do something with the property 28 | } 29 | ``` 30 | 31 | 4. Execute the loop with a layer or a group as the argument: 32 | ```jsx 33 | looper.loop(layer); 34 | ``` 35 | 36 | 37 | 38 | 39 | ### Callbacks 40 | 41 | In the example above we used the `onAny` callback, but there are also callbacks for more specific use cases: 42 | ```jsx 43 | looper.onAny 44 | // called on each iteration of the loop 45 | 46 | looper.onPropertyFound 47 | // called when a property is found (specifically a property, not a group or a layer) 48 | 49 | looper.onGroupFound 50 | // called when a property group is found 51 | 52 | looper.onEnd 53 | // called right before the search ends 54 | 55 | ``` 56 | 57 | See more examples in the [examples folder](./examples). 58 | 59 | 60 | 61 | ### Optimizations 62 | If you know that the property you are looking for has been changed by the user, you can set `looper.skipUnmodified` to `true`. Skipping such properties and groups has the potential to speed up the loop significantly, which is especially useful when looping through all properties of a large number of layers. 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/PropertyTreeLooper.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description class 3 | */ 4 | var PropertyTreeLooper = function () { 5 | var rootClass = this; 6 | 7 | /** 8 | * @description callbacks 9 | * @param {function} onAny - callback for each time a property, group or layer is found (regardless of type) 10 | * @param {function} onPropertyFound - callback for each time a property of type PropertyType.PROPERTY is found 11 | * @param {function} onGroupFound - callback for each time a property of type PropertyType.PROPERTY_GROUP is found 12 | * @param {function} onEnd - callback for when the loop is finished 13 | * @param {boolean} skipUnmodified - skip properties, groups or layers that have not been modified 14 | */ 15 | this.onAny = null; 16 | this.onPropertyFound = null; 17 | this.onGroupFound = null; 18 | this.onEnd = null; 19 | this.skipUnmodified = false; 20 | this.loopIndex = -1; 21 | 22 | 23 | this.loop = function (prop) { 24 | rootClass.loopIndex++; 25 | var thisLoopIndex = rootClass.loopIndex; // cache inside to access after recursive calls 26 | 27 | 28 | // prepare 29 | if (!prop) { 30 | return; 31 | } 32 | 33 | // ⤵skip unmodified properties 34 | if (rootClass.skipUnmodified && prop.isModified === false) { 35 | return; 36 | } 37 | 38 | // ⤵call onAny callback 39 | if (rootClass.onAny) { 40 | rootClass.onAny(prop); 41 | } 42 | 43 | // ⤵call onPropertyFound or onLayerFound callbacks 44 | var type = prop.propertyType; 45 | if (type == PropertyType.PROPERTY && rootClass.onPropertyFound) { 46 | rootClass.onPropertyFound(prop); 47 | } else if (type == PropertyType.NAMED_GROUP && prop.containingComp && rootClass.onLayerFound) { 48 | rootClass.onLayerFound(prop); 49 | } 50 | 51 | // call onGroupFound callback and go deeper 52 | if (prop.numProperties > 0) { 53 | if (rootClass.onGroupFound) { 54 | rootClass.onGroupFound(prop); 55 | } 56 | 57 | // ...loop through its properties 58 | for (var i = 1; i <= prop.numProperties; i++) { 59 | rootClass.loop(prop.property(i)); 60 | } 61 | }; 62 | 63 | // ⤵call onEnd callback 64 | if (thisLoopIndex === 0) { 65 | // reset the loop index of the looper to its initial value 66 | rootClass.loopIndex = -1; 67 | 68 | if (rootClass.onEnd) { 69 | rootClass.onEnd(prop); 70 | } 71 | } 72 | }; 73 | 74 | }; 75 | 76 | 77 | 78 | 79 | 80 | --------------------------------------------------------------------------------