rhubarb-lip-sync/extras/AdobeAfterEffects/rhubarb.jsx

390 lines
16 KiB
React
Raw Normal View History

2017-06-12 10:23:00 +00:00
// Polyfill for Object.assign
"function"!=typeof Object.assign&&(Object.assign=function(a,b){"use strict";if(null==a)throw new TypeError("Cannot convert undefined or null to object");for(var c=Object(a),d=1;d<arguments.length;d++){var e=arguments[d];if(null!=e)for(var f in e)Object.prototype.hasOwnProperty.call(e,f)&&(c[f]=e[f])}return c});
// Polyfill for Array.isArray
Array.isArray||(Array.isArray=function(r){return"[object Array]"===Object.prototype.toString.call(r)});
// Polyfill for Array.prototype.map
Array.prototype.map||(Array.prototype.map=function(r){var t,n,o;if(null==this)throw new TypeError("this is null or not defined");var e=Object(this),i=e.length>>>0;if("function"!=typeof r)throw new TypeError(r+" is not a function");for(arguments.length>1&&(t=arguments[1]),n=new Array(i),o=0;o<i;){var a,p;o in e&&(a=e[o],p=r.call(t,a,o,e),n[o]=p),o++}return n});
// Polyfill for Array.prototype.every
Array.prototype.every||(Array.prototype.every=function(r,t){"use strict";var e,n;if(null==this)throw new TypeError("this is null or not defined");var o=Object(this),i=o.length>>>0;if("function"!=typeof r)throw new TypeError;for(arguments.length>1&&(e=t),n=0;n<i;){var y;if(n in o&&(y=o[n],!r.call(e,y,n,o)))return!1;n++}return!0});
// Polyfill for Array.prototype.find
Array.prototype.find||(Array.prototype.find=function(r){if(null===this)throw new TypeError("Array.prototype.find called on null or undefined");if("function"!=typeof r)throw new TypeError("callback must be a function");for(var n=Object(this),t=n.length>>>0,o=arguments[1],e=0;e<t;e++){var f=n[e];if(r.call(o,f,e,n))return f}});
// Polyfill for Array.prototype.filter
Array.prototype.filter||(Array.prototype.filter=function(r){"use strict";if(void 0===this||null===this)throw new TypeError;var t=Object(this),e=t.length>>>0;if("function"!=typeof r)throw new TypeError;for(var i=[],o=arguments.length>=2?arguments[1]:void 0,n=0;n<e;n++)if(n in t){var f=t[n];r.call(o,f,n,t)&&i.push(f)}return i});
// Polyfill for Array.prototype.forEach
Array.prototype.forEach||(Array.prototype.forEach=function(a,b){var c,d;if(null===this)throw new TypeError(" this is null or not defined");var e=Object(this),f=e.length>>>0;if("function"!=typeof a)throw new TypeError(a+" is not a function");for(arguments.length>1&&(c=b),d=0;d<f;){var g;d in e&&(g=e[d],a.call(c,g,d,e)),d++}});
// Polyfill for Array.prototype.includes
Array.prototype.includes||(Array.prototype.includes=function(r,t){if(null==this)throw new TypeError('"this" is null or not defined');var e=Object(this),n=e.length>>>0;if(0===n)return!1;for(var i=0|t,o=Math.max(i>=0?i:n-Math.abs(i),0);o<n;){if(function(r,t){return r===t||"number"==typeof r&&"number"==typeof t&&isNaN(r)&&isNaN(t)}(e[o],r))return!0;o++}return!1});
// Polyfill for JSON
"object"!=typeof JSON&&(JSON={}),function(){"use strict";function f(a){return a<10?"0"+a:a}function this_value(){return this.valueOf()}function quote(a){return rx_escapable.lastIndex=0,rx_escapable.test(a)?'"'+a.replace(rx_escapable,function(a){var b=meta[a];return"string"==typeof b?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function str(a,b){var c,d,e,f,h,g=gap,i=b[a];switch(i&&"object"==typeof i&&"function"==typeof i.toJSON&&(i=i.toJSON(a)),"function"==typeof rep&&(i=rep.call(b,a,i)),typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";if(gap+=indent,h=[],"[object Array]"===Object.prototype.toString.apply(i)){for(f=i.length,c=0;c<f;c+=1)h[c]=str(c,i)||"null";return e=0===h.length?"[]":gap?"[\n"+gap+h.join(",\n"+gap)+"\n"+g+"]":"["+h.join(",")+"]",gap=g,e}if(rep&&"object"==typeof rep)for(f=rep.length,c=0;c<f;c+=1)"string"==typeof rep[c]&&(d=rep[c],(e=str(d,i))&&h.push(quote(d)+(gap?": ":":")+e));else for(d in i)Object.prototype.hasOwnProperty.call(i,d)&&(e=str(d,i))&&h.push(quote(d)+(gap?": ":":")+e);return e=0===h.length?"{}":gap?"{\n"+gap+h.join(",\n"+gap)+"\n"+g+"}":"{"+h.join(",")+"}",gap=g,e}}var rx_one=/^[\],:{}\s]*$/,rx_two=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,rx_three=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,rx_four=/(?:^|:|,)(?:\s*\[)+/g,rx_escapable=/[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,rx_dangerous=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;"function"!=typeof Date.prototype.toJSON&&(Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},Boolean.prototype.toJSON=this_value,Number.prototype.toJSON=this_value,String.prototype.toJSON=this_value);var gap,indent,meta,rep;"function"!=typeof JSON.stringify&&(meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},JSON.stringify=function(a,b,c){var d;if(gap="",indent="","number"==typeof c)for(d=0;d<c;d+=1)indent+=" ";else"string"==typeof c&&(indent=c);if(rep=b,b&&"function"!=typeof b&&("object"!=typeof b||"number"!=typeof b.length))throw new Error("JSON.stringify");return str("",{"":a})}),"function"!=typeof JSON.parse&&(JSON.parse=function(text,reviver){function walk(a,b){var c,d,e=a[b];if(e&&"object"==typeof e)for(c in e)Object.prototype.hasOwnProperty.call(e,c)&&(d=walk(e,c),void 0!==d?e[c]=d:delete e[c]);return reviver.call(a,b,e)}var j;if(text=String(text),rx_dangerous.lastIndex=0,rx_dangerous.test(text)&&(text=text.replace(rx_dangerous,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})),rx_one.test(text.replace(rx_two,"@").replace(rx_three,"]").replace(rx_four,"")))return j=eval("("+text+")"),"function"==typeof reviver?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}();
function last(array) {
return array[array.length - 1];
}
function identity(x) { return x; }
function toArray(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
result.push(list[i]);
}
return result;
}
function toArrayBase1(list) {
var result = [];
for (var i = 1; i <= list.length; i++) {
result.push(list[i]);
}
return result;
}
2017-06-16 19:13:58 +00:00
// Checks whether scripts are allowed to write files by creating and deleting a dummy file
function canWriteFiles() {
try {
var file = new File();
file.open('w');
file.writeln('');
file.close();
file.remove();
return true;
} catch (e) {
return false;
}
}
// On Windows, this is C:\ProgramData
var settingsFilePath = Folder.appData.fullName + '/rhubarb-ae-settings.json';
function readSettingsFile() {
var file = new File(settingsFilePath);
try {
file.open('r');
return JSON.parse(file.read());
} catch (e) {
return {};
} finally {
file.close();
}
}
function writeSettingsFile(settings) {
try {
var file = new File(settingsFilePath);
file.open('w');
file.write(JSON.stringify(settings, null, 2));
} catch (e) {
alert('Error persisting settings. ' + e.message);
} finally {
file.close();
}
}
2017-06-12 10:23:00 +00:00
// ExtendScript's resource strings are a pain to write.
// This function allows them to be written in JSON notation, then converts them into the required
// format.
// For instance, this string: '{ "__type__": "StaticText", "text": "Hello world" }'
// is converted to this: 'StaticText { "text": "Hello world" }'.
// This code relies on the fact that, contrary to the language specification, all major JavaScript
// implementations keep object properties in insertion order.
function createResourceString(tree) {
var result = JSON.stringify(tree, null, 2);
result = result.replace(/(\{\s*)"__type__":\s*"(\w+)",?\s*/g, '$2 $1');
return result;
}
// Object containing functions to create control description trees.
// For instance, `controls.StaticText({ text: 'Hello world' })`
// returns `{ __type__: StaticText, text: 'Hello world' }`.
var controlFunctions = (function() {
var controlTypes = [
// Strangely, 'dialog' and 'palette' need to start with a lower-case character
['Dialog', 'dialog'], ['Palette', 'palette'],
'Panel', 'Group', 'TabbedPanel', 'Tab', 'Button', 'IconButton', 'Image', 'StaticText',
'EditText', 'Checkbox', 'RadioButton', 'Progressbar', 'Slider', 'Scrollbar', 'ListBox',
'DropDownList', 'TreeView', 'ListItem', 'FlashPlayer'
];
var result = {};
controlTypes.forEach(function(type){
var isArray = Array.isArray(type);
var key = isArray ? type[0] : type;
var value = isArray ? type[1] : type;
result[key] = function(options) {
return Object.assign({ __type__: value }, options);
};
});
return result;
})();
// Returns the path of a project item within the project
function getItemPath(item) {
if (item === app.project.rootFolder) {
return '/';
}
var result = item.name;
while (item.parentFolder !== app.project.rootFolder) {
result = item.parentFolder.name + ' / ' + result;
item = item.parentFolder;
}
return '/ ' + result;
}
// Selects the item within an item control whose text matches the specified text.
// If no such item exists, selects the first item, if present.
function selectByTextOrFirst(itemControl, text) {
var targetItem = toArray(itemControl.items).find(function(item) {
return item.text === text;
});
if (!targetItem && itemControl.items.length) {
targetItem = itemControl.items[0];
}
if (targetItem) {
itemControl.selection = targetItem;
}
}
function getWaveFileProjectItems() {
var result = toArrayBase1(app.project.items).filter(function(item) {
var isAudioFootage = item instanceof FootageItem && item.hasAudio && !item.hasVideo;
if (!isAudioFootage) return false;
var isWaveFile = item.file && item.file.exists && item.file.name.match(/\.wav$/i);
return isWaveFile;
});
return result;
}
2017-06-12 10:23:00 +00:00
function createDialogWindow() {
var resourceString;
with (controlFunctions) {
resourceString = createResourceString(
Dialog({
text: 'Rhubarb Lip-Sync',
settings: Group({
orientation: 'column',
alignChildren: ['left', 'top'],
audioFile: Group({
label: StaticText({
text: 'Audio file:',
// If I don't explicitly activate a control, After Effects has trouble
// with keyboard focus, so I can't type in the text edit field below.
active: true
}),
value: DropDownList()
}),
dialogText: Group({
label: StaticText({ text: 'Dialog text (optional):' }),
value: EditText({
properties: { multiline: true },
characters: 60,
minimumSize: [0, 100]
})
}),
mouthComp: Group({
label: StaticText({ text: 'Mouth composition:' }),
value: DropDownList({})
}),
extendedMouthShapes: Group({
label: StaticText({ text: 'Extended mouth shapes:' }),
g: Checkbox({ text: 'G' }),
h: Checkbox({ text: 'H' }),
x: Checkbox({ text: 'X' }),
}),
targetFolder: Group({
label: StaticText({ text: 'Target folder:' }),
value: DropDownList({})
}),
frameRate: Group({
label: StaticText({ text: 'Frame rate:' }),
value: EditText({ characters: 8 }),
auto: Checkbox({ text: 'From mouth composition' })
})
}),
separator: Group({ preferredSize: ['', 3] }),
buttons: Group({
alignment: 'right',
animate: Button({
properties: { name: 'ok' },
text: 'Animate'
}),
cancel: Button({
properties: { name: 'cancel' },
text: 'Cancel'
})
})
})
);
}
// Create window and child controls
var window = new Window(resourceString);
var controls = {
audioFile: window.settings.audioFile.value,
dialogText: window.settings.dialogText.value,
mouthComp: window.settings.mouthComp.value,
mouthShapeG: window.settings.extendedMouthShapes.g,
mouthShapeH: window.settings.extendedMouthShapes.h,
mouthShapeX: window.settings.extendedMouthShapes.x,
targetFolder: window.settings.targetFolder.value,
frameRate: window.settings.frameRate.value,
autoFrameRate: window.settings.frameRate.auto,
animateButton: window.buttons.animate,
cancelButton: window.buttons.cancel
};
// Add audio file options
getWaveFileProjectItems().forEach(function(projectItem) {
var listItem = controls.audioFile.add('item', getItemPath(projectItem));
listItem.projectItem = projectItem;
});
// Add mouth composition options
var comps = toArrayBase1(app.project.items).filter(function (item) {
return item instanceof CompItem;
});
comps.forEach(function(projectItem) {
var listItem = controls.mouthComp.add('item', getItemPath(projectItem));
listItem.projectItem = projectItem;
});
// Add target folder options
var projectFolders = toArrayBase1(app.project.items).filter(function (item) {
return item instanceof FolderItem;
});
projectFolders.unshift(app.project.rootFolder);
projectFolders.forEach(function(projectFolder) {
var listItem = controls.targetFolder.add('item', getItemPath(projectFolder));
listItem.projectItem = projectFolder;
});
2017-06-16 19:13:58 +00:00
// Load persisted settings
var settings = readSettingsFile();
selectByTextOrFirst(controls.audioFile, settings.audioFile);
controls.dialogText.text = settings.dialogText || '';
selectByTextOrFirst(controls.mouthComp, settings.mouthComp);
controls.mouthShapeG.value = settings.extendedMouthShapes.g;
controls.mouthShapeH.value = settings.extendedMouthShapes.h;
controls.mouthShapeX.value = settings.extendedMouthShapes.x;
selectByTextOrFirst(controls.targetFolder, settings.targetFolder);
controls.frameRate.text = settings.frameRate || '';
controls.autoFrameRate.value = settings.autoFrameRate;
2017-06-16 19:12:48 +00:00
// Align controls
window.onShow = function() {
// Give uniform width to all labels
var groups = toArray(window.settings.children);
var labelWidths = groups.map(function(group) { return group.children[0].size.width; });
var maxLabelWidth = Math.max.apply(Math, labelWidths);
groups.forEach(function (group) {
group.children[0].size.width = maxLabelWidth;
});
// Give uniform width to inputs
var valueWidths = groups.map(function(group) {
return last(group.children).bounds.right - group.children[1].bounds.left;
});
var maxValueWidth = Math.max.apply(Math, valueWidths);
groups.forEach(function (group) {
var multipleControls = group.children.length > 2;
if (!multipleControls) {
group.children[1].size.width = maxValueWidth;
}
});
window.layout.layout(true);
};
var updating = false;
function update() {
if (updating) return;
updating = true;
try {
// Handle auto frame rate
var autoFrameRate = controls.autoFrameRate.value;
controls.frameRate.enabled = !autoFrameRate;
if (autoFrameRate) {
// Take frame rate from mouth comp
var mouthComp = (controls.mouthComp.selection || {}).projectItem;
controls.frameRate.text = mouthComp ? mouthComp.frameRate : '';
} else {
// Sanitize frame rate
var sanitizedFrameRate = controls.frameRate.text.match(/\d*\.?\d*/)[0];
if (sanitizedFrameRate !== controls.frameRate.text) {
controls.frameRate.text = sanitizedFrameRate;
}
}
2017-06-16 19:13:58 +00:00
// Store settings
var settings = {
audioFile: (controls.audioFile.selection || {}).text,
dialogText: controls.dialogText.text,
mouthComp: (controls.mouthComp.selection || {}).text,
extendedMouthShapes: {
g: controls.mouthShapeG.value,
h: controls.mouthShapeH.value,
x: controls.mouthShapeX.value
},
targetFolder: (controls.targetFolder.selection || {}).text,
frameRate: Number(controls.frameRate.text),
autoFrameRate: controls.autoFrameRate.value
};
writeSettingsFile(settings);
} finally {
updating = false;
}
}
// Handle changes
update();
2017-06-16 19:13:58 +00:00
controls.audioFile.onChange = update;
controls.dialogText.onChanging = update;
controls.mouthComp.onChange = update;
2017-06-16 19:13:58 +00:00
controls.mouthShapeG.onClick = update;
controls.mouthShapeH.onClick = update;
controls.mouthShapeX.onClick = update;
controls.targetFolder.onChange = update;
controls.frameRate.onChanging = update;
controls.autoFrameRate.onClick = update;
2017-06-12 10:23:00 +00:00
// Handle animation
controls.animateButton.onClick = function() {
// TODO: validate
app.beginUndoGroup('Rhubarb Lip-Sync');
window.close();
// TODO: animate
app.endUndoGroup();
};
// Handle cancelation
controls.cancelButton.onClick = function() {
window.close();
};
return window;
}
2017-06-16 19:13:58 +00:00
function checkPreconditions() {
if (!canWriteFiles()) {
Window.alert('This script requires file system access.\n\n'
+ 'Please enable Preferences > General > Allow Scripts to Write Files and Access Network.',
'Rhubarb Lip-Sync', true);
return false;
}
return true;
}
if (checkPreconditions()) {
createDialogWindow().show();
}