Source

DuIO_api.jsxinc

// ==================== |-----| ====================
// ==================== | api | ====================
// ==================== |-----| ====================

// Include all Duik methods and DuAEF Extension

/**
 * <h3>A collection of tools for exporting and importing stuff in After Effects</h3>
 * <p>DuIO requires <i>DuAEF</i>, the <i>Duduf After Effects Framework</i>. Two builds of the <i>DuIO API</i> are available:<br />
 * <ul><li><code>DuIO_api.jsxinc</code> does not include <i>DuAEF</i>, and can be used to compine multiple <i>Duduf APIs</i> with a single copy of <i>DuAEF</i>.<br />
 * Be careful to grab the right version of <i>DuAEF</i> in this case.</li>
 * <li><code>DuAEF_DuIO_api.jsxinc</code> includes all dependencies, with <i>DuAEF</i>, and is easier to include in your scripts.</li></ul></p>
 * @example
 * // Encapsulate everything to avoid global variables!
 * // The parameter is either undefined (stand alone script) or the panel containing the ui (ScriptUI)
 * (function(thisObj)
 * {
 *      // If you only need Duik, just include DuAEF_Duik_api
 *      #include "DuAEF_DuIO_api.jsxinc";
 *      
 *      // Running the init() method of DuAEF is required to setup everything properly.
 *      DuAEF.init( "YourScriptName", "1.0.0", "YourCompanyName" );
 *      
 *      // These info can be used by the framework to improve UX, but they're optional
 *      DuESF.chatURL = 'http://chat.rxlab.info'; // A link to a live-chat server like Discord or Slack...
 *      DuESF.bugReportURL = 'https://github.com/RxLaboratory/DuAEF_Dugr/issues/new/choose'; // A link to a bug report form
 *      DuESF.featureRequestURL = 'https://github.com/RxLaboratory/DuAEF_Dugr/issues/new/choose'; // A link to a feature request form
 *      DuESF.aboutURL = 'http://rxlaboratory.org/tools/dugr'; // A link to the webpage about your script
 *      DuESF.docURL = 'http://dugr.rxlab.guide'; // A link to the documentation of the script
 *      DuESF.scriptAbout = 'Duduf Groups: group After Effects layers!'; // A short string describing your script
 *      DuESF.companyURL = 'https://rxlaboratory.org'; // A link to your company's website
 *      DuESF.rxVersionURL = 'http://version.rxlab.io' // A link to an RxVersion server to check for updates
 *      
 *      // Build your UI here, declare your methods, etc.
 * 
 *      // This will be our main panel
 *      var ui = DuScriptUI.scriptPanel( thisObj, true, true, new File($.fileName) );
 *      ui.addCommonSettings(); // Automatically adds the language settings, location of the settings file, etc
 *
 *      DuScriptUI.staticText( ui.settingsGroup, "Hello world of settings!" ); // Adds a static text to the settings panel
 *      DuScriptUI.staticText( ui.mainGroup, "Hello worlds!" ); // Adds a static text to the main panel
 *      
 *      // When you're ready to display everything
 *      DuScriptUI.showUI(ui);
 *
 *      // Note that if you don't have a UI or if you don't use DuScriptUI to show it,
 *      // you HAVE TO run this method before running any other function:
 *      // DuAEF.enterRunTime();
 *  
 * })(this);
 * @example
 * // Encapsulate everything to avoid global variables!
 * // The parameter is either undefined (stand alone script) or the panel containing the ui (ScriptUI)
 * (function(thisObj)
 * {
 *      // If you need to combine Duik and other APIs like DuIO or DuGR
 *      // Include DuAEF first, and then stand-alone APIs
 *      #include "DuAEF.jsxinc";
 *      #include "DuGR_api.jsxinc";
 *      #include "Duik_api.jsxinc";
 *      #include "DuIO_api.jsxinc";
 *      
 *      // Running the init() method of DuAEF is required to setup everything properly.
 *      DuAEF.init( "YourScriptName", "1.0.0", "YourCompanyName" );
 *      
 *      // These info can be used by the framework to improve UX, but they're optional
 *      DuESF.chatURL = 'http://chat.rxlab.info'; // A link to a live-chat server like Discord or Slack...
 *      DuESF.bugReportURL = 'https://github.com/RxLaboratory/DuAEF_Dugr/issues/new/choose'; // A link to a bug report form
 *      DuESF.featureRequestURL = 'https://github.com/RxLaboratory/DuAEF_Dugr/issues/new/choose'; // A link to a feature request form
 *      DuESF.aboutURL = 'http://rxlaboratory.org/tools/dugr'; // A link to the webpage about your script
 *      DuESF.docURL = 'http://dugr.rxlab.guide'; // A link to the documentation of the script
 *      DuESF.scriptAbout = 'Duduf Groups: group After Effects layers!'; // A short string describing your script
 *      DuESF.companyURL = 'https://rxlaboratory.org'; // A link to your company's website
 *      DuESF.rxVersionURL = 'http://version.rxlab.io' // A link to an RxVersion server to check for updates
 *      
 *      // Build your UI here, declare your methods, etc.
 * 
 *      // This will be our main panel
 *      var ui = DuScriptUI.scriptPanel( thisObj, true, true, new File($.fileName) );
 *      ui.addCommonSettings(); // Automatically adds the language settings, location of the settings file, etc
 *
 *      DuScriptUI.staticText( ui.settingsGroup, "Hello world of settings!" ); // Adds a static text to the settings panel
 *      DuScriptUI.staticText( ui.mainGroup, "Hello worlds!" ); // Adds a static text to the main panel
 *      
 *      // When you're ready to display everything
 *      DuScriptUI.showUI(ui);
 *
 *      // Note that if you don't have a UI or if you don't use DuScriptUI to show it,
 *      // you HAVE TO run this method before running any other function:
 *      // DuAEF.enterRunTime();
 *  
 * })(this);
 * @namespace
 * @author Nicolas Dufresne and contributors
 * @copyright 2008 - 2021 Nicolas Dufresne, RxLaboratory
 * @version 3.0.0
 * @requires DuAEF>=1.0.0
 * @category DuIO
 * @license GPL-3.0 <br />
 * DuIO is free software: you can redistribute it and/or modify<br />
 * it under the terms of the GNU General Public License as published by<br />
 * the Free Software Foundation, either version 3 of the License, or<br />
 * (at your option) any later version.<br />
 *<br />
 * DuIO is distributed in the hope that it will be useful,<br />
 * but WITHOUT ANY WARRANTY; without even the implied warranty of<br />
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the<br />
 * GNU General Public License for more details.<br />
 *<br />
 * You should have received a copy of the GNU General Public License<br />
 * along with DuIO. If not, see {@link http://www.gnu.org/licenses/}.
 */
var DuIO = {};

// ==================== |---------| ====================
// ==================== | strings | ====================
// ==================== |---------| ====================

DuScriptUI.String.IMPORT_ANIMATION = "Import animation";// ==================== |-----------| ====================
// ==================== | animation | ====================
// ==================== |-----------| ====================

/**
 * Animation IO
 * @namespace
 * @category DuIO
 */
DuIO.Animation = {};


/**
 * Pastes the animation previously copied by {@link Duik.Animation.copy} to the selected properties.
 * @param {DuAELayerAnimation[]} data The animation data to paste
 * @param {CompItem} [comp] The composition.
 * @param {Boolean} [replace=false] - Whether to completely erase and replace the current animation
 * @param {Boolean} [offset=false] - Whether to offset the animation from the current value
 * @param {Boolean} [reverse=false] - Whether to reverse the animation
 * @param {string[]} [matchNames=[]] A filter to apply only on specific property types
 * @param {Boolean} [keyframesOnly=true] - If false, the value of properties without keyframes will be set too.
 * @param {Boolean} [expressions=false] - If true, the expression is set too.
 * @param {Boolean} [dontMoveAncestors=false] - When set to true, the transform (position, rotation) values for ancestor layers (the ones without parent) will be offset to 0 before applying the animation.
 */
DuIO.Animation.paste = function ( layers, data, replace, offset, reverse, matchNames, keyframesOnly, expressions, dontMoveAncestors )
{
	var comp = DuAEProject.getActiveComp();
	if (!comp) return;

    if (typeof layers === 'undefined')
	{
		layers = DuAEComp.getSelectedLayers();
		if (layers.length == 0) layers = comp.layers;
	}
	if (layers.length == 0) return;

    //options
    var time = comp.time;
	if (data.firstKeyFrameTime) time -= data.firstKeyFrameTime;
    replace = def( replace, false );
    offset = def( offset, false );
    reverse = def( reverse, false );
	matchNames = def(matchNames, []);
	keyframesOnly = def( keyframesOnly, true);
	expressions = def( expressions, !keyframesOnly);
	dontMoveAncestors = def(dontMoveAncestors, false);

    DuAE.beginUndoGroup(DuScriptUI.String.PASTE_KEYFRAMES, false);
    DuAEProject.setProgressMode(true);

    var remaining = DuAELayer.setAnims( layers, data, time, undefined, expressions, keyframesOnly, replace, matchNames, offset, reverse, dontMoveAncestors );

    if (remaining.length > 0)
    {
        var ui_pasteLayerPicker = DuScriptUI.layerPickerDialog("Missing layers");

        for (var i = 0, num = remaining.length; i < num; i++)
        {
            ui_pasteLayerPicker.addSelector(remaining[i]._index + ' | ' + remaining[i]._name);
            //try to preselect by name
            var ok = false;
            for (var j = 1, numLayers = comp.numLayers; j <= numLayers; j++)
            {
                var l = comp.layer(j);
                if (l.name.toLowerCase() == remaining[i]._name.toLowerCase())
                {
                    ui_pasteLayerPicker.layerPicker.selectors[i].setCurrentIndex(j);
                    ok = true;
                    break;
                }
            }
            if (!ok && remaining[i]._index > 0 && remaining[i]._index <= comp.numLayers) ui_pasteLayerPicker.layerPicker.selectors[i].setCurrentIndex(remaining[i]._index);
        }

        ui_pasteLayerPicker.onAccept = function () {
            DuAE.beginUndoGroup(DuScriptUI.String.PASTE_KEYFRAMES, false);
            DuAEProject.setProgressMode(true);
            DuAELayer.setAllAnims(
                ui_pasteLayerPicker.getLayers(),
                remaining,
                time,
                undefined,
                expressions,
                keyframesOnly,
                replace,
                matchNames,
                offset,
                reverse
            );
            DuAEProject.setProgressMode(false);
            DuAE.endUndoGroup(DuScriptUI.String.PASTE_KEYFRAMES);
        };

        DuAEProject.setProgressMode(false);
        DuScriptUI.showUI(ui_pasteLayerPicker);
    }

    DuAEProject.setProgressMode(false);
    DuAE.endUndoGroup(DuScriptUI.String.PASTE_KEYFRAMES);

}

/**
 * Copies all the animations on selected layers, and saves them to a Json file.
 * @param {File} file The file to save the data
 * @param {Layer[]|DuList<Layer>|LayerCollection} [layers] An array of Layers or LayerCollection with the animation. Selected layers from the current comp if omitted.
 * @param {Boolean} [selectedKeysOnly] Wether to copy only selected keys or not. If omitted, will be true if there are selected keyframes, false otherwise.
 * @return {DuAELayerAnimation[]} The animations
 */
DuIO.Animation.toJson = function( file, layers, selectedKeysOnly)
{
    layers = def( layers, DuAEComp.getSelectedLayers() );
    if (layers.length == 0) return null;
    var comp = layers[0].containingComp;

    //wether to store only selected keys if there are any
    selectedKeysOnly = def( selectedKeysOnly, DuAELayer.haveSelectedKeys(layers) );
    
    //end time of the animation to store
    var endTime = comp.workAreaDuration + comp.workAreaStart;
    //start time of the animation to store
    var startTime;
    // If there are selected keys, look for the time of the first one
    if (selectedKeysOnly) startTime = DuAELayer.firstKeyFrameTime(layers,true);
    else startTime = comp.workAreaStart;

    var data = {};
	data.duio = {};
	data.duio.version = DuESF.scriptVersion.fullVersion;
	data.duio.animation = true;
	data.duio.rig = false;

	//copy the animation
	var anims  = DuAELayer.getAnims( layers, selectedKeysOnly, [startTime, endTime] );
	//clean data
	data.layers = DuIO.Animation.cleanExportData(anims);

	//save data
	DuFile.saveJSON(data,file);

    return anims;
}

/**
 * Loads an animation file and applies it on the layers
 * @param {File} jsonFile The file containing the animation
 * @param {Layer[]|DuList<Layer>|LayerCollection} [layers] The layers. If omitted, will get the selected layers from the current comp, or all layers if none are selected.
 * @param {Boolean} [keyframesOnly=false] If true, will load only keyframes, but not static values or expressions.
 * @param {string[]} [matchNames=[]] A filter for the properties to load. If empty, will load all properties.
 * @param {Boolean} [offset=false] Whether to offset the current values or use absolute values.
 * @param {Boolean} [reverse=false] Whether to reverse the keyframes in time.
 */
DuIO.Animation.fromJson = function( jsonFile, layers, keyframesOnly, matchNames, offset, reverse )
{
	if (!jsonFile.exists) return;
	
	keyframesOnly = def( keyframesOnly, false );

	DuAE.beginUndoGroup( DuScriptUI.String.IMPORT_ANIMATION, false );
	DuAEProject.setProgressMode(true);

	var data = DuFile.parseJSON( jsonFile );
	//TODO version check
	var anim = data.layers;
	//clean data
	anim  = DuIO.Animation.cleanImportData(anim);

	DuIO.Animation.paste( layers, anim, false, offset, reverse, matchNames, keyframesOnly, undefined, true );

	DuAEProject.setProgressMode(false);
	DuAE.endUndoGroup( DuScriptUI.String.IMPORT_ANIMATION );
}

/**
 * Cleans data from an After Effects animation before exporting it.<br />
 * This is a low-level function which you may need only if building your own export formats.<br />
 * It cleans the data returned by the "getAnim" functions  (see {@link DuAELayer.getAnim}, {@link DuAELayer.getAnims}, {@link DuAEProperty.getAnim}) to be able to store it in a text file.
 * @param {object}	data	The animation
 * @return {object}	The data cleaned
 */
DuIO.Animation.cleanExportData = function (data)
{
	var newData = data;
	if (newData.keys)
	{
		for (var k = 0, num =  newData.keys.length ; k < num; k++)
		{
			newData.keys[k]._inInterpolationType = DuIO.Animation.keyframeInterpolationTypeToName(newData.keys[k]._inInterpolationType);
			newData.keys[k]._outInterpolationType = DuIO.Animation.keyframeInterpolationTypeToName(newData.keys[k]._outInterpolationType);
		}
	}
	if (newData.anims)
	{
		for (var a = 0, num =  newData.anims.length ; a < num; a++)
		{
			newData.anims[a] = DuIO.Animation.cleanExportData(newData.anims[a]);
		}
	}
	if (newData instanceof Array)
	{
		for (var a = 0, num =  newData.length ; a < num; a++)
		{
			newData[a] = DuIO.Animation.cleanExportData(newData[a]);
		}
	}

	return newData;
}

/**
 * Cleans data from a JSON before loading it.<br />
 * This is a low-level function which you may need only if building your own import formats.<br />
 * It cleans the data stored after having used {@link DuAEF.Interchange.animation.cleanAnimExportData}<br />
 * to be able to set the animations using the "setAnim" functions (see {@link DuAEF.DuAE.Layer.setAnim}, {@link DuAEF.DuAE.Property.setAnim}).
 * @param {object}	data	The animation
 * @return {object}	The data cleaned
 */
DuIO.Animation.cleanImportData = function (data)
{
	var newData = data;
	if (newData.keys)
	{
		for (var k = 0, num =  newData.keys.length ; k < num; k++)
		{
			newData.keys[k] = DuIO.Animation.cleanKeyframeImportData(newData.keys[k]);
		}
	}
	if (newData.anims)
	{
		for (var a = 0, num =  newData.anims.length ; a < num; a++)
		{
			newData.anims[a] = DuIO.Animation.cleanImportData(newData.anims[a]);
		}
	}
	if (newData instanceof Array)
	{
		for (var a = 0, num =  newData.length ; a < num; a++)
		{
			newData[a] = DuIO.Animation.cleanImportData(newData[a]);
		}
	}

	return newData;
}

/**
 * Cleans data from a JSON before loading it.<br />
 * This is a low-level function which you may need only if building your own import formats.<br />
 * It cleans the data stored after having used {@link DuAEF.Interchange.animation.cleanAnimExportData}<br />
 * to be able to set the animations using the "setAnim" functions (see {@link DuAEF.DuAE.Layer.setAnim}, {@link DuAEF.DuAE.Property.setAnim}).
 * @memberof DuAEF.Interchange.animation
 * @param {object}	keyframeData	The keyframe data which is being loaded
 * @return {object}	The data cleaned
 */
DuIO.Animation.cleanKeyframeImportData = function (keyframeData)
{
	var newData = keyframeData;
	newData._inInterpolationType = DuIO.Animation.keyframeInterpolationNameToType(newData._inInterpolationType);
	newData._outInterpolationType = DuIO.Animation.keyframeInterpolationNameToType(newData._outInterpolationType);
	for (var i = 0, num = newData.inEase.length; i < num; i++)
	{
		var inInfluence = newData.inEase[i].influence;
		if (inInfluence < 0.1) inInfluence = 0.1;
		if (inInfluence > 100) inInfluence = 100;
		var outInfluence = newData.outEase[i].influence;
		if (outInfluence < 0.1) outInfluence = 0.1;
		if (outInfluence > 100) outInfluence = 100;
		newData.inEase[i] = new KeyframeEase(newData.inEase[i].speed, inInfluence);
		newData.outEase[i] = new KeyframeEase(newData.outEase[i].speed, outInfluence);
	}

	// If the value is a path
	if (typeof newData.value.vertices !== 'undefined')
	{
		var s = new Shape();
		var v = newData.value;
		s.closed = v.closed;
		s.featherInterps = v.featherInterps;
		s.featherRadii = v.featherRadii;
		s.featherRelCornerAngles = v.featherRelCornerAngles;
		s.featherRelSegLocs = v.featherRelSegLocs;
		s.featherSegLocs = v.featherSegLocs;
		s.featherTensions = v.featherTensions;
		s.featherTypes = v.featherTypes;
		s.inTangents = v.inTangents;
		s.outTangents = v.outTangents;
		s.vertices = v.vertices;
		newData.value = s;
	}

	return newData;
}

/**
 * Gets the name of an After Effects interpolation type.<br />
 * This is a low-level function which you should not need.<br />
 * It used by {@link DuIO.Animation.cleanAnimExportData} to store interpolation with their names.
 * @param {KeyframeInterpolationType}	type	The After Effects interpolation type
 * @return {string}	The interpolation name or empty string if not found
 */
DuIO.Animation.keyframeInterpolationTypeToName = function (type)
{
	if (type == KeyframeInterpolationType.LINEAR) return 'linear';
	if (type == KeyframeInterpolationType.BEZIER) return 'bezier';
	if (type == KeyframeInterpolationType.HOLD) return 'hold';
	else return '';
}

/**
 * Gets the After Effects interpolation type with its name.<br />
 * This is a low-level function which you should not need.<br />
 * It used by {@link DuAEF.Interchange.cleanAnimImportData} to set interpolation from their names.
 * @memberof DuAEF.Interchange.animation
 * @param {string}	name	The interpolation name
 * @return {KeyframeInterpolationType}	The interpolation type or null if not found
 */
DuIO.Animation.keyframeInterpolationNameToType = function (name)
{
	if (!name) return KeyframeInterpolationType.LINEAR;
	name = name.toString();
	if (name.toLowerCase() == 'linear' || name == '6612') return KeyframeInterpolationType.LINEAR;
	else if (name.toLowerCase() == 'bezier' || name == '6613') return KeyframeInterpolationType.BEZIER;
	else if (name.toLowerCase() == 'hold' || name == '6614') return KeyframeInterpolationType.HOLD;
	else return null;
}