├── README.md ├── photoshop ├── README.txt └── Export QML.jsx └── after-effects ├── Prosessor.js └── export-comp-transition.jsx /README.md: -------------------------------------------------------------------------------- 1 | # CS2QML - Adobe Creative Suite to Qt QML 2 | 3 | [![Analytics](https://ga-beacon.appspot.com/UA-2643697-15/creative-suite-to-qml/index?flat)](https://github.com/igrigorik/ga-beacon) 4 | 5 | Scripts for exporting things from different Adobe Creative Suite programs to Qt Quick prototypes. 6 | 7 | * Adobe Creative Suite http://www.adobe.com/products/creativesuite.html 8 | * Qt Project http://qt.io/ 9 | 10 | ## How come? 11 | 12 | Pretty often designers tend to work with tools such as Photoshop, Illustrator, 13 | After Effects and other similar software while creating User Interfaces and User Interactions. 14 | 15 | Most of the time these would more properly communicated via working prototype that behaves 16 | like the final product. This is where QML comes along. 17 | 18 | The scripts collected to this repository are to be used in this work flow. 19 | 20 | ## Before getting started 21 | 22 | Please note that recent versions of Adobe tools, such as After Effects for example, 23 | require to change certain settings in order to allow scripts such as found in this project, 24 | to write any files. The given setting can be found from: 25 | 26 | ``` 27 | Preferences > General > Allow scripts to write to files and access network 28 | ``` 29 | 30 | ## Status 31 | 32 | The following scripts, JavaScript with `jsx` extension, are available: 33 | 34 | * After Effects - transitions 35 | * Photoshop (from http://qt.gitorious.org/qt-labs/photoshop-qmlexporter, where no changes since 16th Dec 2010) 36 | 37 | It would be nice to have the following: 38 | 39 | * Illustrator 40 | * Flash Catalyst 41 | * InDesign 42 | 43 | ## License 44 | 45 | Copyright (c) [Juga Paazmaya](https://paazmaya.fi) 46 | -------------------------------------------------------------------------------- /photoshop/README.txt: -------------------------------------------------------------------------------- 1 | INSTALLATION AND USAGE NOTES 2 | 3 | * Version 0.1: 4 | - Initial version 5 | 6 | * Version 0.3: 7 | - Added support for exporting layer groups 8 | - Made updating the QML file optional 9 | - Fixed a bug preventing files with groups from exporting 10 | - Set Export as default button 11 | - Fixed a problem with unique ID's 12 | 13 | * Version 0.4: 14 | - Added progressbar and cancel button 15 | 16 | Usage: 17 | 18 | "Element name" 19 | - This is the name of the output QML file 20 | 21 | "Output Folder" 22 | - The folder containing exported files. Files are replaced 23 | without warning. 24 | 25 | "Rasterize text" 26 | - Force text layers to be exported as images and not text 27 | elements 28 | 29 | "Group layers" 30 | - This will export each top-level group as a merged 31 | QML Image element. 32 | 33 | "Export hidden" 34 | - If checked, hidden layers are exported but their 35 | visible property is set to false 36 | 37 | "Export QML" 38 | - Uncheck this if you have modified your QML document by 39 | hand but still want to re-export graphical assets. 40 | 41 | 42 | Output: 43 | - Layers and groups are exported as image elements in 44 | the root.qml file and png images are dumped into the 45 | images subirectory. 46 | - Text items are exported as Text elements 47 | 48 | 49 | 50 | Notes: 51 | - Files are replaced without warning! 52 | - The script has only been tested on Photoshop CS5 and CS4 and 53 | may or may not work on previous versions. 54 | 55 | 56 | Known issues: 57 | - The font names are not really mapped accurately at the 58 | moment. 59 | 60 | 61 | 62 | Installation: 63 | 64 | Copy "Export QML.jsx" into your 65 | "Photoshop/Plugins/Presets/Scripts" 66 | directory. It should now show up under 67 | "File/Scripts/Export QML" 68 | -------------------------------------------------------------------------------- /after-effects/Prosessor.js: -------------------------------------------------------------------------------- 1 | 2 | var timeLineDuration = 0; 3 | 4 | // make sure the animation start running and only once. 5 | function timerTriggered() { 6 | var item = main.loader.item; 7 | var list = []; 8 | 9 | // In the case of QML children, the key is their index number, starting from 0 10 | for (var key in item.children) { 11 | 12 | var child = item.children[key]; 13 | list = list.concat(child.animList); 14 | } 15 | 16 | var time = main.currentTime; 17 | var len = list.length; 18 | 19 | //console.log("timerTriggered. len: " + len + ", time: " + time); 20 | 21 | for (var i = 0; i < len; i++) { 22 | var anim = list[i]; // SequentialAnimation 23 | if (time > anim.initTime && !anim.alreadyRunOnce) { 24 | anim.alreadyRunOnce = true; 25 | anim.start(); 26 | // should also start drawing to canvas 27 | } 28 | } 29 | 30 | // show where we are at in the timeLine 31 | var pos = main.width * (time / timeLineDuration); 32 | timeLineKey.x = pos; 33 | } 34 | 35 | 36 | function fillTimeLine() { 37 | var item = main.loader.item; 38 | 39 | var longest = 0; 40 | var data = []; 41 | var w = main.width; 42 | var key; 43 | var child; 44 | var len = item.children.length; 45 | 46 | for (var i = 0; i < len; i++) { 47 | child = item.children[i]; 48 | longest = Math.max(longest, child.outTime); 49 | } 50 | 51 | for (var j = 0; j < len; j++) { 52 | child = item.children[j]; 53 | var duration = (child.outTime - child.inTime) / longest * w; 54 | var inTime = child.inTime / longest * w; 55 | data.push({ color: child.color.toString(), duration: duration, inTime: inTime }); 56 | } 57 | console.log("fillTimeLine. longest: " + longest + ", data.length: " + data.length); 58 | 59 | timeLineDuration = longest; 60 | 61 | timeLine.model = data; 62 | } 63 | 64 | 65 | function showTooltip(xPos, yPos, layerName){ 66 | toolTip.visible = true; 67 | toolTip.text = layerName; 68 | toolTip.x = xPos; 69 | toolTip.y = yPos; 70 | } 71 | function hideTooltip(){ 72 | toolTip.visible = false; 73 | } 74 | -------------------------------------------------------------------------------- /photoshop/Export QML.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | Photoshop to QML Exporter 3 | 4 | Version: 0.4 5 | 6 | For information about Qt Quick itself: 7 | http://qt.nokia.com/products/qt-quick/ 8 | 9 | Author: Jens Bache-wiig 10 | contact: jens.bache-wiig@nokia.com 11 | 12 | Copyright (c) 2010, Nokia 13 | All rights reserved. 14 | 15 | Redistribution and use in source and binary forms, with or without 16 | modification, are permitted provided that the following conditions are met: 17 | 1. Redistributions of source code must retain the above copyright 18 | notice, this list of conditions and the following disclaimer. 19 | 2. Redistributions in binary form must reproduce the above copyright 20 | notice, this list of conditions and the following disclaimer in the 21 | documentation and/or other materials provided with the distribution. 22 | 3. All advertising materials mentioning features or use of this software 23 | must display the following acknowledgement: 24 | This product includes software developed by the . 25 | 4. Neither the name of the nor the 26 | names of its contributors may be used to endorse or promote products 27 | derived from this software without specific prior written permission. 28 | 29 | THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY 30 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 31 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 32 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 33 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 34 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 35 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 36 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 37 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 38 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 39 | */ 40 | 41 | #target photoshop 42 | 43 | var mainDialog 44 | var progressPanel 45 | var runButton = 1 46 | var cancelButton = 2 47 | var qmlfile 48 | var layerCount = 0 49 | var layerIndex = 0 50 | var cancelExport = 0 51 | 52 | // Setting keys 53 | var appString = "QML Exporter - 1" 54 | var outputNameKey = 0; 55 | var destinationKey = 1; 56 | var rasterizeKey = 2; 57 | var exportByGroupKey = 3; 58 | var exportHidden = 4; 59 | var exportQML = 5; 60 | 61 | 62 | Array.prototype.indexOf = function(elt /*, from*/) 63 | { 64 | var len = this.length; 65 | 66 | var from = Number(arguments[1]) || 0; 67 | from = (from < 0) 68 | ? Math.ceil(from) 69 | : Math.floor(from); 70 | if (from < 0) 71 | from += len; 72 | 73 | for (; from < len; from++) 74 | { 75 | if (from in this && 76 | this[from] === elt) 77 | return from; 78 | } 79 | return -1; 80 | }; 81 | 82 | main(); 83 | 84 | function hexValue(dec) 85 | { 86 | var result; 87 | switch (dec) { 88 | case 10: 89 | result = "a"; 90 | break; 91 | case 11: 92 | result = "b" 93 | break; 94 | case 12: 95 | result = "c"; 96 | break; 97 | case 13: 98 | result = "d"; 99 | break; 100 | case 14: 101 | result = "e" 102 | case 15: 103 | result = "f" 104 | break; 105 | default: 106 | result = dec 107 | break; 108 | } 109 | return result; 110 | } 111 | 112 | // Converts SolidColor to a QML color property 113 | function qtColor(color) { 114 | var r = Math.floor(color.rgb.red) 115 | var g = Math.floor(color.rgb.green); 116 | var b = Math.floor(color.rgb.blue) 117 | var a = Math.floor(color.rgb.alpha * 255) 118 | var v1 = hexValue(Math.floor(r / 16)); 119 | var v2 = hexValue(r % 16); 120 | var v3 = hexValue(Math.floor(g / 16)); 121 | var v4 = hexValue(g % 16); 122 | var v5 = hexValue(Math.floor(b / 16)); 123 | var v6 = hexValue(b % 16); 124 | if (a > 0) { 125 | var v7 = hexValue(Math.floor(a / 16)); 126 | var v8 = hexValue(a % 16); 127 | return "\"#" + v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8 + "\""; 128 | } 129 | return "\"#" + v1 + v2 + v3 + v4 + v5 + v6 + "\""; 130 | } 131 | 132 | function main() { 133 | 134 | var exportInfo = new Object(); 135 | 136 | if (cancelButton == setupDialog(exportInfo)) 137 | return 'cancel'; 138 | 139 | 140 | var myMaximumValue = 1.0; 141 | var myProgressBarWidth = 300; 142 | progressPanel = new Window('window', 'Exporting document to QML...'); 143 | progressPanel.myProgressBar = progressPanel.add('progressbar', [12, 12, myProgressBarWidth, 24], 0, myMaximumValue); 144 | progressPanel.buttonCancel = progressPanel .add("button", undefined, "Cancel"); 145 | 146 | progressPanel.buttonCancel.onClick = function () { 147 | cancelExport = true; 148 | progressPanel.hide(); 149 | } 150 | progressPanel.show(); 151 | 152 | app.preferences.rulerUnits = Units.PIXELS 153 | 154 | var documentName = app.activeDocument.name; 155 | app.activeDocument = app.documents[documentName]; 156 | var documentCopy = app.activeDocument.duplicate(); 157 | documentCopy.activeLayer = documentCopy.layers[documentCopy.layers.length - 1]; 158 | 159 | var elementName = mainDialog.outputName.text; 160 | if (elementName.indexOf(".qml") == -1) // Append .qml unless not explicitly set 161 | elementName += ".qml" 162 | 163 | var outputName = exportInfo.destination + "/" + elementName; 164 | 165 | var imagefolder = new Folder(exportInfo.destination + "/images/"); 166 | imagefolder.create(); 167 | 168 | app.activeDocument.suspendHistory("export QML history", ""); 169 | 170 | var exportQML = mainDialog.exportQML.value 171 | 172 | if (exportQML && !cancelExport) { 173 | qmlfile = new File(outputName); 174 | qmlfile.encoding = "UTF8"; 175 | qmlfile.open("w", "TEXT", ""); 176 | qmlfile.write("import Qt 4.7\n"); 177 | qmlfile.write("Item {\n"); 178 | qmlfile.write(" width:" + app.activeDocument.width.as("px") + "\n"); 179 | qmlfile.write(" height:" + app.activeDocument.height.as("px") + "\n"); 180 | } 181 | 182 | countLayers(documentCopy) // For progressBar 183 | exportChildren(documentCopy, app.documents[documentName], exportInfo, documentCopy, exportInfo.fileNamePrefix); 184 | 185 | if (exportQML) { 186 | documentCopy.close(SaveOptions.DONOTSAVECHANGES); 187 | qmlfile.write("}\n"); 188 | qmlfile.close(); 189 | } 190 | Panel.hide(); 191 | } 192 | 193 | 194 | function setupDialog(exportInfo) { 195 | mainDialog = new Window("dialog", "Export Document To QML"); 196 | var brush = mainDialog.graphics.newBrush(mainDialog.graphics.BrushType.THEME_COLOR, "appDialogBackground"); 197 | mainDialog.graphics.backgroundColor = brush; 198 | mainDialog.graphics.disabledBackgroundColor = mainDialog.graphics.backgroundColor; 199 | mainDialog.orientation = 'column'; 200 | mainDialog.alignChildren = 'left'; 201 | 202 | mainDialog.groupFirstLine = mainDialog.add("group"); 203 | mainDialog.groupFirstLine.orientation = 'row'; 204 | mainDialog.groupFirstLine.alignChildren = 'left'; 205 | mainDialog.groupFirstLine.alignment = 'fill'; 206 | 207 | mainDialog.groupSecondLine = mainDialog.add("group"); 208 | mainDialog.groupSecondLine.orientation = 'row'; 209 | mainDialog.groupSecondLine.alignChildren = 'left'; 210 | 211 | mainDialog.groupThirdLine = mainDialog.add("group"); 212 | mainDialog.groupThirdLine.orientation = 'row'; 213 | mainDialog.groupThirdLine.alignChildren = 'right'; 214 | mainDialog.groupThirdLine.alignment = 'right'; 215 | 216 | mainDialog.groupFourthLine = mainDialog.add("group"); 217 | mainDialog.groupFourthLine.orientation = 'row'; 218 | mainDialog.groupFourthLine.alignChildren = 'right'; 219 | mainDialog.groupFourthLine.alignment = 'right'; 220 | 221 | mainDialog.groupFirstLine.add("statictext", undefined, "Element Name:"); 222 | mainDialog.groupSecondLine.add("statictext", undefined, "Output Folder:"); 223 | 224 | mainDialog.outputName = mainDialog.groupFirstLine.add("edittext", undefined, "MyElement"); 225 | mainDialog.outputName.preferredSize.width = 220 226 | mainDialog.rasterizeText = mainDialog.groupThirdLine.add("checkbox", undefined, "Rasterize Text"); 227 | mainDialog.exportByGroup = mainDialog.groupThirdLine.add("checkbox", undefined, "Group layers"); 228 | mainDialog.exportHidden = mainDialog.groupThirdLine.add("checkbox", undefined, "Export hidden"); 229 | 230 | mainDialog.destinationFolder = mainDialog.groupSecondLine.add("edittext", undefined, ""); 231 | mainDialog.destinationFolder.preferredSize.width = 220; 232 | mainDialog.buttonBrowse = mainDialog.groupSecondLine.add("button", undefined, "Browse.."); 233 | 234 | mainDialog.exportQML = mainDialog.groupThirdLine.add("checkbox", undefined, "Export QML"); 235 | mainDialog.buttonRun = mainDialog.groupFourthLine .add("button", undefined, "Export"); 236 | mainDialog.defaultElement = mainDialog.buttonRun 237 | 238 | mainDialog.buttonBrowse.onClick = function () { 239 | var defaultFolder = defaultFolder = "~"; 240 | var selFolder = Folder.selectDialog("Select destination", defaultFolder); 241 | if (selFolder != null) 242 | mainDialog.destinationFolder.text = selFolder.fsName; 243 | } 244 | 245 | mainDialog.buttonRun.onClick = function () { 246 | var destination = mainDialog.destinationFolder.text; 247 | if (destination.length == 0) { 248 | alert("you must specify a destination directory."); 249 | return; 250 | } 251 | 252 | var testFolder = new Folder(destination); 253 | if (!testFolder.exists) { 254 | alert("The destination directory does not exist."); 255 | return; 256 | } 257 | exportInfo.destination = destination; 258 | mainDialog.close(runButton); 259 | } 260 | 261 | mainDialog.buttonCancel = mainDialog.groupFourthLine .add("button", undefined, "Cancel"); 262 | mainDialog.buttonCancel.onClick = function () { 263 | mainDialog.close(cancelButton); 264 | } 265 | 266 | try { 267 | // Try to read saved settings 268 | var desc = app.getCustomOptions(appString); 269 | mainDialog.outputName.text = desc.getString(outputNameKey); 270 | mainDialog.destinationFolder.text = desc.getString(destinationKey); 271 | mainDialog.rasterizeText.value = desc.getBoolean(rasterizeKey); 272 | mainDialog.exportByGroup.value = desc.getBoolean(exportByGroupKey); 273 | mainDialog.exportHidden.value = desc.getBoolean(exportHidden); 274 | mainDialog.exportQML.value = desc.getBoolean(exportQML); 275 | } 276 | catch(e) { 277 | // Default settings on first run 278 | mainDialog.exportByGroup.value = true; 279 | mainDialog.exportHidden.value = false; 280 | mainDialog.exportQML.value = true; 281 | } // Use defaults 282 | 283 | app.bringToFront(); 284 | mainDialog.defaultElement.active = true; 285 | mainDialog.center(); 286 | 287 | var result = mainDialog.show(); 288 | if (cancelButton != result) { 289 | var desc = new ActionDescriptor(); 290 | desc.putString(outputNameKey, mainDialog.outputName.text); 291 | desc.putString(destinationKey, mainDialog.destinationFolder.text); 292 | desc.putBoolean(rasterizeKey, mainDialog.rasterizeText.value); 293 | desc.putBoolean(exportByGroupKey, mainDialog.exportByGroup.value); 294 | desc.putBoolean(exportHidden, mainDialog.exportHidden.value); 295 | desc.putBoolean(exportQML, mainDialog.exportQML.value); 296 | app.putCustomOptions(appString, desc); 297 | } 298 | return result; 299 | } 300 | 301 | function countLayers(obj) { 302 | // Even when grouping layers, we export all layers at depth == 0 303 | for (var i = 0; i < obj.artLayers.length; i++) { 304 | layerCount++; 305 | } 306 | for (var i = 0; i < obj.layerSets.length; i++) { // Recursive 307 | if (!mainDialog.exportByGroup.value) 308 | countLayers(obj.layerSets[i]); 309 | layerCount++; 310 | } 311 | } 312 | 313 | function hideAll(obj) { 314 | for (var i = 0; i < obj.artLayers.length; i++) { 315 | obj.artLayers[i].allLocked = false; 316 | obj.artLayers[i].visible = false; 317 | } 318 | for (var i = 0; i < obj.layerSets.length; i++) { // Recursive 319 | hideAll(obj.layerSets[i]); 320 | } 321 | } 322 | 323 | function exportChildren(dupObj, orgObj, exportInfo, dupDocRef, fileNamePrefix) { 324 | var exportQML = mainDialog.exportQML.value 325 | 326 | // Track names to detect duplicates 327 | var names = new Array; 328 | var uniqueCounter = 1; 329 | 330 | if (!mainDialog.exportByGroup.value) 331 | hideAll(dupObj) 332 | 333 | for (var i = dupObj.layers.length - 1; i >= 0 && !cancelExport ; i--) { 334 | progressPanel.myProgressBar.value = (layerIndex++)/layerCount 335 | var currentLayer = dupObj.layers[i]; 336 | // Ensure unique layer names 337 | while (names[currentLayer.name]) { 338 | $.writeln("Warning: duplicate layer name: " + currentLayer.name); 339 | currentLayer.name = orgObj.layers[i].name+ "_#" + uniqueCounter++; 340 | } 341 | names[currentLayer.name] = true; 342 | 343 | // Skip hidden layers 344 | var visible = true; 345 | if (!orgObj.layers[i].visible) { 346 | visible = false 347 | } 348 | 349 | if (!mainDialog.exportByGroup.value) { 350 | // Ignore layer groups and only show one layer at once 351 | if (currentLayer.typename== "LayerSet") 352 | continue; 353 | dupObj.layers[i].visible = true 354 | } else { 355 | // Hide all but current layergroup 356 | for (var k = dupObj.layers.length - 1; k >= 0; k--) 357 | dupObj.layers[k].visible = (k==i) 358 | } 359 | 360 | if (!visible && !mainDialog.exportHidden.value) 361 | continue; 362 | 363 | // Since we already save opacity, we dont want it affecting the output image 364 | var opacity = currentLayer.opacity / 100.0; 365 | currentLayer.opacity = 100; 366 | 367 | var layerName = dupObj.layers[i].name; // store layer name before change doc 368 | var fileNameBody = layerName.toLowerCase(); 369 | 370 | // Ignore empty text layers 371 | if (currentLayer.kind == LayerKind.TEXT && currentLayer.textItem.contents == "") continue; 372 | var documentCopyTmp = dupDocRef.duplicate(); 373 | 374 | // Trim copied document to layer bounds 375 | if (activeDocument.activeLayer.isBackgroundLayer == false) { 376 | // app.activeDocument.trim(TrimType.TRANSPARENT); 377 | var bounds = currentLayer.bounds 378 | activeDocument.crop (bounds, 0, bounds.width, bounds.height) 379 | } 380 | 381 | fileNameBody = fileNameBody.replace(/[ :\/\\*\?\"\<\>\|#]/g, "_"); // '/\:*?"<>|' -> '_' 382 | if (fileNameBody.length > 120) { 383 | fileNameBody = fileNameBody.substring(0, 120); 384 | } 385 | 386 | var isText = (currentLayer.kind == LayerKind.TEXT && !(mainDialog.rasterizeText.value)) 387 | var filename = fileNameBody + ".png"; 388 | if (exportQML) { 389 | // Write QML properties 390 | if (isText) qmlfile.write(" Text {\n"); 391 | else qmlfile.write(" Image {\n"); 392 | qmlfile.write(" id: " + fileNameBody + "\n"); 393 | 394 | if (!visible) 395 | qmlfile.write(" visible: " + visible+ "\n"); 396 | 397 | var xoffset = currentLayer.bounds[0].as("px"); 398 | var yoffset = currentLayer.bounds[1].as("px"); 399 | 400 | if (isText) { 401 | var textItem = currentLayer.textItem; 402 | qmlfile.write(" text: \"" + textItem.contents + "\"\n"); 403 | 404 | // ### Temporary hack to set font positioning 405 | // Using pointsize doesnt work for some reason and we need to 406 | // figure out which metric we need to use ascending, perhaps? 407 | yoffset -= textItem.size.as("px") / 4; 408 | 409 | qmlfile.write(" font.pixelSize: " + Math.floor(textItem.size.as("px")) + "\n"); 410 | 411 | //var fontfamily = app.textFonts.getByName(textitem.font); 412 | qmlfile.write(" font.family: \"" + textItem.font + "\"\n"); 413 | 414 | if (textItem.font.indexOf("Bold") != -1) qmlfile.write(" font.bold: true\n"); 415 | 416 | if (textItem.font.indexOf("Italic") != -1) qmlfile.write(" font.italic: true\n"); 417 | qmlfile.write(" color: " + qtColor(currentLayer.textItem.color) + "\n"); 418 | qmlfile.write(" smooth: true\n"); 419 | } else { 420 | qmlfile.write(" source: \"images/" + filename + "\"\n"); 421 | } 422 | 423 | qmlfile.write(" x: " + xoffset + "\n"); 424 | qmlfile.write(" y: " + yoffset + "\n"); 425 | qmlfile.write(" opacity: " + opacity + "\n"); 426 | qmlfile.write(" }\n"); 427 | } 428 | // Save document 429 | if (!isText) { 430 | var saveFile = new File(exportInfo.destination + "/images/" + filename); 431 | pngSaveOptions = new PNGSaveOptions(); 432 | pngSaveOptions.interlaced = false; 433 | documentCopyTmp.saveAs(saveFile, pngSaveOptions, true, Extension.LOWERCASE); 434 | } 435 | 436 | // Close tempfile 437 | documentCopyTmp.close(SaveOptions.DONOTSAVECHANGES); 438 | 439 | if (!mainDialog.exportByGroup.value) 440 | dupObj.layers[i].visible = false 441 | } 442 | 443 | for (var i = 0; i < dupObj.layerSets.length; i++) { 444 | var fileNameBody = fileNamePrefix; 445 | fileNameBody += "_" + "s"; 446 | if (!mainDialog.exportByGroup.value) 447 | exportChildren(dupObj.layerSets[i], orgObj.layerSets[i], exportInfo, dupDocRef, fileNameBody); // recursive call 448 | } 449 | } 450 | 451 | function collectLayerSets (theParent) { 452 | if (!allLayerSets) { 453 | var allLayerSets = new Array 454 | } 455 | for (var m = theParent.layers.length - 1; m >= 0;m--) { 456 | var theLayer = theParent.layers[m]; 457 | // No need to process hidden layers 458 | if (theLayer.typename == "LayerSet") { 459 | allLayerSets = allLayerSets.concat(theLayer); 460 | allLayerSets = allLayerSets.concat(collectLayerSets(theLayer)) 461 | } 462 | } 463 | return allLayerSets 464 | }; 465 | -------------------------------------------------------------------------------- /after-effects/export-comp-transition.jsx: -------------------------------------------------------------------------------- 1 | /** * This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License. * http://creativecommons.org/licenses/by-sa/3.0/ * * Juga Paazmaya */ { function ExportCompTransition(thisObj) { var scriptName = "Export Transitions to QML"; var scriptVersion = "0.5"; var scriptDate = "2nd May 2012"; var dialog; var outputFolder; // string, such as Folder.path var compAll = true; // all compositions or just the selected? var idCounter = 0; var seqAnimList = []; // Not good enough check as OS X should have same with Unix... var lineFeed = (Folder.fs == "Windows" ? "\r\n" : (Folder.fs == "Unix" ? "\n" : ($.os.indexOf ("10") !== -1 ? "\n" : "\r"))); var dirSeparator = (Folder.fs == "Windows" ? "\\" : "/"); var qtQuickVersion = "2.0"; function BuildAndShowUI(thisObj) { dialog = new Window("dialog", scriptName, undefined, {resizeable: false}); dialog.orientation = "column"; dialog.alignment = ["fill", "top"]; dialog.alignChildren = ["fill", "top"]; dialog.margins = [10,10,10,10]; dialog.spacing = 10; var introText = dialog.add("statictext", undefined, "Start by choosing the output folder...\nBeware! The existing files in the selected folder will be overwritten.\nOnce exporting has finished, remember to copy the Prosessor.js file to the given folder.", { multiline: true }); // ---- output folder selection var folderGroup = dialog.add("group"); folderGroup.alignment = ["fill", "center"]; folderGroup.alignChildren = ["fill", "center"]; var folderBox = folderGroup.add("panel"); folderBox.alignChildren = ["fill", "center"]; folderBox.orientation = "row"; var folderText = folderBox.add("statictext", undefined, "Output folder"); folderText.alignment = ["left", "center"]; dialog.folderEdit = folderBox.add("edittext", undefined, outputFolder); dialog.folderEdit.alignment = ["fill", "center"]; dialog.folderEdit.helpTip = "The current output folder if any. Needs to exist in the file system. In case it has files with the same name as those that are exported, they are overwritten."; var folderButton = folderBox.add("button", undefined, "Choose folder"); folderButton.alignment = ["right", "center"]; folderButton.size = folderButton.maximumSize = folderButton.minimumSize = folderButton.preferredSize = [100, 25]; folderButton.onClick = chooseFolder; folderButton.helpTip = "Click to choose the output folder."; // ---- // ---- radio buttons var radioBox = dialog.add("panel"); radioBox .alignment = "fill"; radioBox .orientation = "column"; radioBox.alignChildren = ["fill", "center"]; var radioText = radioBox.add("statictext", undefined, "You may export either all of the Compositions or just the currently selected active Composition" ); var radioAll = radioBox.add("radiobutton", undefined, "All compositions"); radioAll.helpTip = "Export all Compositions if this option is selected"; radioAll.value = compAll; radioAll.onActivate = function () { compAll = true; }; var radioSel = radioBox.add("radiobutton", undefined, "Only selected composition"); radioSel.helpTip = "Export only the currently active Composition if this option is selected"; radioSel.value = !compAll; radioSel.onActivate = function () { compAll = false; }; // ---- // ---- Qt Quick version selector var versionBox = dialog.add("panel"); versionBox.alignment = "fill"; versionBox.orientation = "row"; versionBox.alignChildren = ["fill", "center"]; var versionQuick1 = versionBox.add("radiobutton", undefined, "Qt Quick 1.1 (Qt 4.x). Simple easing"); versionQuick1.helpTip = "Use Qt Quick 1.1 import command"; versionQuick1.onActivate = function () { qtQuickVersion = "1.1"; }; versionQuick1.value = (qtQuickVersion == "1.1" ? true : false); var versionQuick2 = versionBox.add("radiobutton", undefined, "Qt Quick 2.0 (Qt 5.x). Use bezier curve easing"); versionQuick2.helpTip = "Use Qt Quick 2.0 import command"; versionQuick2.onActivate = function () { qtQuickVersion = "2.0"; }; versionQuick2.value = (qtQuickVersion == "2.0" ? true : false); // ---- // ---- line ending selection and prosess button var centerGroup = dialog.add("group"); centerGroup.alignment = "fill"; centerGroup.orientation = "row"; centerGroup.alignChildren = ["fill", "center"]; // ---- line ending selection var lineEndBox = centerGroup.add("panel"); lineEndBox.alignment = ["fill", "fill"]; lineEndBox.alignChildren = ["fill", "center"]; var lineEndText = lineEndBox.add("statictext", undefined, "Line ending markers and file format"); var lineEndN = lineEndBox.add("radiobutton", undefined, "Unix and Mac OS X (\\n - LF)"); lineEndN.helpTip = "Linux, Unix and Mac OS X use this"; lineEndN.onActivate = function () { lineFeed = "\n"; }; lineEndN.value = (lineFeed == "\n" ? true : false); var lineEndR = lineEndBox.add("radiobutton", undefined, "Mac OS 9 and before (\\r - CR)"); lineEndR.helpTip = "Older Mac (9 and before) used this"; lineEndR.onActivate = function () { lineFeed = "\r"; }; lineEndR.value = (lineFeed == "\r" ? true : false); var lineEndRN = lineEndBox.add("radiobutton", undefined, "Windows (\\r\\n - CR+LF)"); lineEndRN.helpTip = "Windows uses this"; lineEndRN.onActivate = function () { lineFeed = "\r\n"; }; lineEndRN.value = (lineFeed == "\r\n" ? true : false); // ---- prosess button var buttonBox = centerGroup.add("panel"); buttonBox.alignment = ["fill", "fill"]; buttonBox.alignChildren = ["left", "center"]; buttonBox.margins = [30,30,30,30]; dialog.okButton = buttonBox.add("button", undefined, "Start Processing", {name: "ok"}); dialog.okButton.alignment = ["center", "center"]; dialog.okButton.size = dialog.okButton.maximumSize = dialog.okButton.minimumSize = dialog.okButton.preferredSize = [200, 40]; dialog.okButton.onClick = startProcessing; dialog.okButton.helpTip = "Click here to start prosessing the compositions and to save them as QML files."; // ---- // ---- progress bar. Color missing... var brush = dialog.graphics.newBrush(dialog.graphics.BrushType.SOLID_COLOR, [0.1, 0.7, 0.2]); var pen = dialog.graphics.newPen(dialog.graphics.PenType.SOLID_COLOR, [0.1, 0.7, 0.2], 4); dialog.barProgress = dialog.add("progressbar", undefined, 0, 100); dialog.barProgress.alignment = ["fill", "center"]; dialog.barProgress.helpTip = "Current status of the prosess."; dialog.barProgress.onDraw = function () { }; //dialog.barProgress.maxvalue = 100; // default 100 //dialog.barProgress.graphics.foregroundColor = pen; // ---- // ---- log window dialog.logText = dialog.add("edittext", undefined, "Assuming your operating system is " + File.fs + " (" + $.os + ")" + ".\n", { multiline: true } ); dialog.logText.alignment = ["fill", "fill"]; dialog.logText.helpTip = "Log output of this script is shown here"; dialog.logText.size = [600, 200]; // ---- var bottomGroup = dialog.add("group"); bottomGroup.alignment = ["fill", "fill"]; bottomGroup.alignChildren = ["left", "center"]; // ---- about button var aboutButton = bottomGroup.add("button", undefined, "About", {name: "help"}); aboutButton.alignment = ["left", "bottom"]; aboutButton.helpTip = "About this After Effects script"; aboutButton.size = aboutButton.maximumSize = aboutButton.minimumSize = aboutButton.preferredSize = [100, 25]; aboutButton.onClick = function () { alert(scriptName + " by PAAZMAYA.com" + "\n" + "This script was made to help design work flow in moving transitions from After Effects to QML." + "\n\n" + "Current version " + scriptVersion + " was made in " + scriptDate + " and is provided as is. No warranty.", scriptName + " by PAAZMAYA.com"); }; // ---- // ---- close button dialog.closeButton = bottomGroup.add("button", undefined, "Close", {name: "cancel"}); dialog.closeButton.alignment = ["right", "bottom"]; dialog.closeButton.helpTip = "Closes this dialog"; dialog.closeButton.size = dialog.closeButton.maximumSize = dialog.closeButton.minimumSize = dialog.closeButton.preferredSize = [100, 25]; dialog.closeButton.onClick = function () { dialog.close(); }; // ----- dialog.onResizing = dialog.onResize = function () { this.layout.resize(); }; dialog.layout.layout(); dialog.center(); dialog.show(); } // helpers function round2(num) { return Math.round(num * 100) / 100; } function uniqueId() { return ++idCounter; } function interpolationTypeString(key) { var msg = "not found"; switch (key) { case KeyframeInterpolationType.LINEAR: msg = "linear"; break; case KeyframeInterpolationType.BEZIER: msg = "bezier"; break; case KeyframeInterpolationType.HOLD: msg = "hold"; break; } return msg; } // it seems Adobe implementation of replace only triggers once per character... function replaceNoGoodName(name) { var notWanted = [" ", " ", "-", "/", "\\", ".", "?", "=", "%"]; var len = notWanted.length; for (var i = 0; i < len; i++) { var c = notWanted[i]; while (name.indexOf(c) !== -1) { name = name.replace(c, "_"); } } return name; } function objectValues(obj) { var list = []; if (typeof obj === "undefined" || !obj) { return list; } for (var key in obj) { if (obj.hasOwnProperty(key)) { var k = obj[key]; if (k != null) { //list.push(key + ": " + k.toString()); if (typeof k == "object") { list.push(key + ": [ " + objectValues(k) + " ] "); } else { list.push(key + ": " + k.toString()); } } } } return list.join(", "); } // filename is without path. function saveFile(filename, content) { var path = outputFolder + dirSeparator + filename; var f = new File(path); var os = (lineFeed == "\r\n" ? "Windows" : (lineFeed == "\n" ? "Unix" : "Macintosh")); f.lineFeed = os; f.open("w"); f.write(content); f.close(); dialog.logText.text += "Line feed for: " + filename + " is " + f.lineFeed + "\n"; } function chooseFolder() { var path = outputFolder ? outputFolder : app.project.file.path; var dir = new Folder(path); // Folder, File or null var answer = dir.selectDlg("Please choose a directory where to export the QML files..."); if (answer != null) { outputFolder = answer.absoluteURI; dialog.folderEdit.text = outputFolder; dialog.okButton.enabled = true; } dialog.logText.text += "Selected output directory: " + outputFolder + "\n"; } function startProcessing() { // which one is selected, all or just selected comp? dialog.okButton.enabled = false; dialog.closeButton.enabled = false; var compositions = []; var names = []; var pBar = dialog.barProgress; // get a list of compositions which are to be turned to QML files if (compAll) { compositions = listCompositions(app.project); } else { compositions.push(app.project.activeItem); } dialog.logText.text += "Found " + compositions.length + " compositions\n"; pBar.value = 6; // now parse and save the contents.... var len = compositions.length; var increment = 89 / len; // 95 - 6 = 89 for (var i = 0; i < len; i++) { // prosess.... var comp = compositions[i]; var stuff = "// Composition: " + comp.parentFolder.name + " > " + comp.name + lineFeed + lineFeed + loopLayers(comp.layers); var name = replaceNoGoodName(comp.parentFolder.name + "_" + comp.name); names.push("\"" + name + "\""); pBar.value += increment / 2; // save file... dialog.logText.text += "Saving file: " + name + ".qml\n"; var qml = renderCommonQML(stuff); saveFile(name + ".qml", qml); pBar.value += increment / 2; } // progressBar is now 95% dialog.logText.text += "Saved " + names.length + " compositions to QML files\n"; // create main QML file var mainName = replaceNoGoodName(app.project.file.displayName); // Sort names names.sort(); var qmlMain = [ "id: main", "", "property real speedRatio: 1.0", "property int currentTime: 0 // ms", "property int currentIndex: 0 // index of qmlFiles", "property variant qmlFiles: [", "\t" + names.join("," + lineFeed + "\t"), "]", "property alias loader: pageLoader", (qtQuickVersion != "2.0" ? "// " : "") + "property alias canvas: canvasList", "property alias toolTip: toolTipBox", "property variant runningAnimations: [] // currently running animations", "", "color: \"royalblue\"", "", "focus: true", "Keys.onPressed: {", "if (event.key == Qt.Key_Left) {", "}", "}", // Keys.onPressed "Keys.onSpacePressed: {", // pause animations.... "console.log(\"space pressed\");", "}", // Keys.onSpacePressed "", "Loader {", "id: pageLoader", "anchors.fill: parent", "anchors.margins: 80", "source: main.qmlFiles[main.currentIndex] + \".qml\"", "onLoaded: {", "pageTimer.restart();", "main.currentTime = 0;", "Prosessor.fillTimeLine();", "}", // onLoaded "}", // Loader "Timer {", "id: pageTimer", "interval: 10 // milliseconds", "repeat: true", "running: false", "onTriggered: {", "currentTime += pageTimer.interval * main.speedRatio;", "Prosessor.timerTriggered();", "}", // onTriggered "}", // Timer "Rectangle {", "id: pageMenu", "height: 30", "width: parent.width", "x: 0", "y: 0", "color: \"black\"", "Row {", "anchors.fill: parent", "Text {", "font.pixelSize: 18", "text: \"Prev\"", "color: \"white\"", "width: parent.width / 4", "height: parent.height", "horizontalAlignment: Text.AlignHCenter", "verticalAlignment: Text.AlignVCenter", "MouseArea {", "anchors.fill: parent", "onClicked: main.currentIndex > 0 ? main.currentIndex-- : main.currentIndex = main.qmlFiles.length - 1;", "}", // MouseArea "}", // Text "Text {", "id: labelCurrent", "font.pixelSize: 20", "text: \"[\" + main.currentIndex + \"] \" + main.qmlFiles[main.currentIndex]", "color: \"pink\"", "width: parent.width / 2", "height: parent.height", "horizontalAlignment: Text.AlignHCenter", "verticalAlignment: Text.AlignVCenter", "}", // Text "Text {", "font.pixelSize: 18", "text: \"Next\"", "color: \"white\"", "width: parent.width / 4", "height: parent.height", "horizontalAlignment: Text.AlignHCenter", "verticalAlignment: Text.AlignVCenter", "MouseArea {", "anchors.fill: parent", "onClicked: main.currentIndex < main.qmlFiles.length - 1 ? main.currentIndex++ : main.currentIndex = 0;", "}", // MouseArea "}", // Text "}", // Row "}", // Rectangle "Column {", "id: timeLineColumn", "spacing: 0", "width: parent.width", "x: 0", "y: parent.height - 60", "Repeater {", "id: timeLine", "", "Rectangle {", "color: modelData.color", "width: modelData.duration", "x: modelData.inTime", "height: 60 / timeLine.count", "}", // Rectangle "}", // Repeater "}", // Column "Rectangle {", "id: timeLineKey", "width: 2", "height: timeLineColumn.height + 4", "color: \"white\"", "border.color: \"black\"", "border.width: 1", "y: timeLineColumn.y - 2", "x: 0", "}", // Rectangle "", "Rectangle {", "id: toolTipBox", "property string text", "visible: false", "width: childrenRect.width + 4", "height: childrenRect.height + 4", "radius: height / 4", "color: \"white\"", "border.width: 1", "border.color: \"black\"", "opacity: 0.7", "Text {", "anchors.centerIn: parent", "text: toolTipBox.text", "}", // Text "}", // Rectangle "" ]; if (qtQuickVersion == "2.0") { qmlMain.push( "Canvas {", "id: canvasList", "anchors.top: parent.top", "anchors.topMargin: pageMenu.height", "anchors.bottom: parent.bottom", "anchors.right: parent.right", "width: 60", "}" // Canvas ); } dialog.logText.text += "Saving main file: " + mainName + ".qml\n"; saveFile(mainName + ".qml", renderCommonQML(qmlMain.join(lineFeed))); // Update the progress bar pBar.value = 98; // and finally create the QML project file... var qmlProject = [ "import QmlProject 1.1", "", "Project {", " mainFile: \"" + mainName + ".qml\"", // in Qt 5, the mainFile will be locked if used " QmlFiles {", " directory: \".\"", " }", " JavaScriptFiles {", " directory: \".\"", " }", " ImageFiles {", " directory: \".\"", " }", "}", "" ]; dialog.logText.text += "Saving project file: " + mainName + ".qmlproject\n"; saveFile(mainName + ".qmlproject", qmlProject.join(lineFeed)); pBar.value = 100; dialog.logText.text += "\nCompleted!\n"; // First line in alert is bold... alert("Exporting Complete" + "\n" + "Export to QML files with Qt Quick " + qtQuickVersion + " has been completed", "Exporting Complete"); $.writeln("Exporting Complete"); // Re enable buttons dialog.okButton.enabled = true; dialog.closeButton.enabled = true; } function listCompositions(folder) { var list = []; var len = folder.numItems; for (var i = 0; i < len; i++) { var item = folder.item(i + 1); // The index numbering of a collection starts with 1, not 0. // A user-readable name for the item type; for example, “Folder”, “Footage”, or “Composition”. if (item.typeName == 'Folder') { list.concat(listCompositions(item)); } else if (item.typeName == 'Composition') { // compItem is the only that has layers... according to CS3 docs. list.push(item); } } return list; } // loop thru layers that have transform function loopLayers(layers) { var qmlRect = []; // iterate from bottom to up var llen = layers.length; while (llen > 0) { var layer = layers[llen]; llen--; var layerTrans = layer.transform; var id = "lay_" + replaceNoGoodName(layer.name).toLowerCase(); id += "_" + uniqueId(); // for this it is needed to loop thru properties and check for isEffect var layerEffect = layer.property("Effects"); qmlRect.push( "// Effects: value " + layerEffect.value //objectValues(layerEffect) ); var layerPosition = layer.property("Position"); var layerScale = layer.property("Scale"); var layerOpacity = layer.property("Opacity"); var layerAnchor = layer.property("anchorPoint"); var anchorX = round2(layerAnchor.value[0]); var anchorY = round2(layerAnchor.value[1]); qmlRect.push( "// Position: " + layerPosition.value.toString(), "// Scale: " + layerScale.value.toString(), "// Opacity: " + layerOpacity.value.toString(), "// Anchor: " + layerAnchor.value.toString() ); // by default the layer.name is the source path qmlRect.push( "Rectangle {", "id: " + id, "", "property string layerName: \"" + layer.name + "\"", "property int startTime: " + Math.round(layer.startTime * 1000) + " // ms", "property int inTime: " + Math.round(layer.inPoint * 1000) + " // ms", "property int outTime: " + Math.round(layer.outPoint * 1000) + " // ms", "", "transformOrigin: Item.Center", "", "x: " + round2(layerPosition.value[0] - anchorX) + " // " + layerPosition.value[0] + " - " + anchorX, "y: " + round2(layerPosition.value[1] - anchorY) + " // " + layerPosition.value[1] + " - " + anchorY, "visible: (inTime < main.currentTime ? true : false)", // && main.currentTime < outTime "scale: " + round2(layerScale.value[0] / 100), "//opacity: " + round2(layerOpacity.value / 100), "", "opacity: main.currentTime > outTime ? 0.1 : 0.5 // for testing purposes only... after out time, 0.1", "" ); var w = Math.round(layer.width / 2); var h = Math.round(layer.height / 2); var c = "Qt.rgba(Math.random(), Math.random(), Math.random(), 1)"; // Special case for finger.... if (id.indexOf("finger_") !== -1) { w = h = 40; c = "\"darkorange\""; qmlRect.push( "radius: width / 2" ); } qmlRect.push( "width: " + w, "height: " + h, "color: " + c, ); if (layerTrans.matchName.indexOf("Transform") !== -1) { var trans = loopTransform(layerTrans, id, anchorX, anchorY); var list = "property variant animList: [" + lineFeed + "\t\t" + seqAnimList.join("," + lineFeed + "\t\t") + lineFeed + "\t]"; qmlRect.push( list, trans ); seqAnimList = []; // clear the temp list } // undefined.... //list.push("layerEffect.matchName: " + layerEffect.matchName); // Looking for tints /* if (layerEffect.matchName.indexOf("Effects") !== -1) { } */ qmlRect.push( "Text {", "anchors.fill: parent", "anchors.margins: 6", "text: parent.layerName", "wrapMode: Text.WordWrap", "font.pointSize: 14", "}" // Text ); qmlRect.push( "MouseArea {", "anchors.fill: parent", "acceptedButtons: Qt.LeftButton | Qt.RightButton", "hoverEnabled: true", "onEntered: {", "Prosessor.showTooltip(mouseX + " + id + ".x, mouseY + " + id + ".y, parent.layerName);", "}", // onEntered "onExited: {", "Prosessor.hideTooltip();", "}", // onExited "onClicked: {", "if (mouse.button == Qt.LeftButton) {", "//Prosessor.runSequence(parent)", "}", // if "else if (mouse.button == Qt.RightButton) {", "//" + id + ".visible = false", "}", // else if "}", // onClicked "}", // MouseArea "" ); qmlRect.push( "", "}" // Rectangle ); } return qmlRect.join(lineFeed + "\t"); } // position needs to know anchoring as in QML is always from top left corner function loopTransform(layerTrans, layerName, anchorX, anchorY) { var qmlItem = []; /* For AVLayer: • "Anchor Point" or "anchorPoint" • "Position" or "position" • "Scale" or "scale" • "Rotation" or "rotation" • "Z Rotation" or "zRotation" or "Rotation Z" or "rotationZ" • "Opacity" or "opacity" • "Marker" or "marker" */ // keys seem to be indexed starting from 1 var keysTest = [ "anchorPoint", // AnchorAnimation, AnchorChanges "position", // NumberAnimation, PropertyChanges "scale", // NumberAnimation "rotation", // RotationAnimation "opacity", // NumberAnimation "rotationZ" // NumberAnimation ]; // RotationAnimation { duration: 1000; direction: RotationAnimation.Counterclockwise } var kLen = keysTest.length; for (var k = 0; k < kLen; k++) { var key = keysTest[k]; if (layerTrans.hasOwnProperty(key) && layerTrans.property(key).numKeys > 0) { var num = layerTrans.property(key).numKeys; var initValue; var prevTime; var prevValue; // from: var id = replaceNoGoodName(layerName + "_" + key); // QML Animations var qmlAnim = [ //"AnimationController {", //"id: ac_" + id, "", //"// key: " + key, "SequentialAnimation {", "id: " + id, "property variant sTarget: " + layerName, "property bool alreadyRunOnce: false", "//running: main.currentTime > " + id + ".initTime", "onStarted: console.log(\"" + id + " just started, initTime: \" + initTime)", //"onCompleted: console.log(\"" + id + " just completed.\")", //"onCompleted: running = false // prevent looping", "" ]; // save the id list for launching seqAnimList.push(id); var qmlPropSet = []; // used only for position as it has x and y //dialog.logText.text += "Iterating transform for layer: " + layerName + ", k: " + k + "\n"; for (var index = 1; index <= num; index++) { var pgk = layerTrans.property(key); var qmlAnimSet = []; var comments = []; var time = pgk.keyTime(index); var value = pgk.keyValue(index); // In case this is the first item, use it as zero. if (index == 1) { initValue = value; prevTime = time; dialog.logText.text += "initValue for layer: " + layerName + " at key: " + key + " and index: " + index + " is " + initValue.toString() + "\n"; qmlAnim.push( "property int initTime: " + Math.round(time * 1000) + " // ms", "property variant initValue: [" + value.toString() + "]", "" ); continue; // jump to next iteration. } var parsedValue; var parsedTime; // Should check for the previous time // In QML duration is in milliseconds, AE seconds parsedTime = Math.round((time - prevTime) * 1000); if (key == "position") { qmlAnimSet.push( "ParallelAnimation {", "" ); } if (key == "anchorPoint") { qmlAnimSet.push( "AnchorAnimation {" ); } else if (key == "rotation") { qmlAnimSet.push( "RotationAnimation {" // shoud check for direction... ); } else { qmlAnimSet.push( "NumberAnimation {" ); } // position needs two, x and y var qmlProp = ""; switch (key) { case "position" : qmlProp = "x"; break; default : qmlProp = key; break; } // Took out the difference calculation as the actual values are used in QML var type = pgk.propertyValueType; // type can be // PropertyValueType.OneD // PropertyValueType.TwoD_SPATIAL // PropertyValueType.ThreeD_SPATIAL if (type == PropertyValueType.OneD) { parsedValue = round2(parseFloat(value)) // - parseFloat(initValue)); } else if (type == PropertyValueType.TwoD_SPATIAL || type == PropertyValueType.TwoD || type == PropertyValueType.ThreeD_SPATIAL || type == PropertyValueType.ThreeD) { parsedValue = [ round2(parseFloat(value[0])), // - parseFloat(initValue[0])), round2(parseFloat(value[1])) // - parseFloat(initValue[1])) ]; if (value.length > 2) { // Since mostly everything happens in two dimensions, there is no need for the third value which // in most cases would be zero or something else. But check for it, just in case. // For scale it would be 100 var third = parseFloat(value[2]) // - parseFloat(initValue[2]); if (third > 0) { parsedValue.push(third); } } } else { parsedValue = null; } // special cases, QML opacity is 0..1, while AE is 0..100 if (key == "opacity") { parsedValue = round2(parsedValue / 100); } else if (key == "scale") { parsedValue[0] = round2(parsedValue[0] / 100); parsedValue[1] = round2(parsedValue[1] / 100); } qmlAnimSet.push( "\t" + "id: " + id + "_" + index.toString(), "\t" + "duration: " + parsedTime.toString() + " // ms", "\t" + "property: \"" + qmlProp + "\"", "\t" + "target: " + id + ".sTarget" ); // These should be PropertyChanges..... maybe if (typeof parsedValue == "number") { qmlAnimSet.push( "to: " + parsedValue.toString() ); } else { // assume array qmlAnimSet.push( "to: " + round2(parsedValue[0] - (key == "position" ? anchorX : 0)), ); } comments.push("time: " + time.toString() + ", prevTime: " + prevTime.toString() + ", value: " + value.toString() + ", type: " + type); comments.push("parsedTime: " + parsedTime.toString() + ", parsedValue: " + parsedValue.toString() + ", initValue: " + initValue.toString()); // position seems to have spatial values, which are not always the same..., but most of the time just three zeroes // When true, the named property defines a spatial value. Examples are position and effect point controls. if (pgk.isSpatial) { // Returns the incoming spatial tangent for the specified keyframe, if the named property is spacial (that is, the value type is TwoD_SPATIAL or ThreeD_SPATIAL). var spaIn = pgk.keyInSpatialTangent(index); var spaOut = pgk.keyOutSpatialTangent(index); comments.push("spaIn: " + spaIn.toString() + ", spaOut: " + spaOut.toString()); // Returns true if the specified keyframe has spatial auto-Bezier interpolation. (This type of interpolation affects this keyframe only if keySpatialContinuous(keyIndex) is also true.) var spatialAutoBezier = pgk.keySpatialAutoBezier(index); comments.push("spatialAutoBezier: " + spatialAutoBezier.toString()); var spatialCont = pgk.keySpatialContinuous(index); comments.push("spatialCont: " + spatialCont.toString()); } var intpolIn = interpolationTypeString(pgk.keyInInterpolationType(index)); var intpolOut = interpolationTypeString(pgk.keyOutInterpolationType(index)); var intpolationType = pgk.keyframeInterpolationType; comments.push("intpolIn: " + (intpolIn) + ", intpolOut: " + (intpolOut) + ", intpolationType: " + intpolationType); var qmlEasing = "Easing.Linear"; if (intpolIn == "bezier" && intpolOut == "bezier") { qmlEasing = "Easing.InOutCubic"; } else if (intpolIn == "bezier") { qmlEasing = "Easing.InCubic"; } else if (intpolOut == "bezier") { qmlEasing = "Easing.OutCubic"; } // The KeyframeEase object encapsulates the keyframe ease settings of a layer’s AE property. // There are two types of ease, temporal and spatial, which are determined by the speed and influence settings. var tEaseIn = pgk.keyInTemporalEase(index); var tEaseOut = pgk.keyOutTemporalEase(index); comments.push("tEaseIn: " + objectValues(tEaseIn)); comments.push("tEaseOut: " + objectValues(tEaseOut)); // { influence: 0...100, speed: } var qmlBezierCurve = [ round2(tEaseIn[0].influence / 100), 0, round2(tEaseOut[0].influence / 100), 1, 1, 1 ]; // Easing bezier curves are supported from Qt Quick 2.0 onward... if (qtQuickVersion == "2.0") { qmlAnimSet.push( "easing.type: Easing.Bezier", "easing.bezierCurve: [" + qmlBezierCurve.toString() + "]" ); // control1, control2, end point: [cx1, cy1, cx2, cy2, endx, endy]. The last point must be 1,1. } else { // Should be Qt Quick 1.1 if (qmlEasing != "") { qmlAnimSet.push( "easing.type: " + qmlEasing, "// easing.bezierCurve might be: [" + qmlBezierCurve.toString() + "]" ); } } /* KeyframeInterpolationType.LINEAR KeyframeInterpolationType.BEZIER KeyframeInterpolationType.HOLD */ var temporalAutoBezier = pgk.keyTemporalAutoBezier(index); comments.push("temporalAutoBezier: " + temporalAutoBezier.toString()); // Temporal continuity affects this keyframe only if keyframeInterpolationType is KeyframeInterpolationType.BEZIER for both keyInInterpolation(keyIndex) and keyOutInterpolation(keyIndex) . var temporalCont = pgk.keyTemporalContinuous(index); comments.push("temporalCont: " + temporalCont.toString()); // Most of the time all return true //comments.push("isInterpolationTypeValid(KeyframeInterpolationType.LINEAR): " + pgk.isInterpolationTypeValid(KeyframeInterpolationType.LINEAR)); //comments.push("isInterpolationTypeValid(KeyframeInterpolationType.BEZIER): " + pgk.isInterpolationTypeValid(KeyframeInterpolationType.BEZIER)); //comments.push("isInterpolationTypeValid(KeyframeInterpolationType.HOLD): " + pgk.isInterpolationTypeValid(KeyframeInterpolationType.HOLD)); if (key == "position") { qmlAnimSet.push( "}", "NumberAnimation {", "\t" + "id: " + id + "_" + index.toString() + "_y", "\t" + "duration: " + parsedTime.toString() + " // ms", "\t" + "property: \"y\"", "\t" + "target: " + id + ".sTarget", "to: " + round2(parsedValue[1] - anchorY) + " // " + parsedValue[1] + " - " + anchorY ); // Easing bezier curves are supported from Qt Quick 2.0 onward... if (qtQuickVersion == "2.0") { qmlAnimSet.push( "easing.type: Easing.Bezier", "easing.bezierCurve: [" + qmlBezierCurve.toString() + "]" ); // control1, control2, end point: [cx1, cy1, cx2, cy2, endx, endy]. The last point must be 1,1. } else { // Should be Qt Quick 1.1 if (qmlEasing != "") { qmlAnimSet.push( "easing.type: " + qmlEasing, "// easing.bezierCurve might be: [" + qmlBezierCurve.toString() + "]" ); } } } // seems that opacity always has min and max, which are 0 and 100. if (pgk.hasMin) { var min = pgk.minValue; comments.push("min: " + min.toString()); } if (pgk.hasMax) { var max = pgk.maxValue; comments.push("max: " + max.toString()); } qmlAnimSet.push( "\t" + "// " + comments.join(lineFeed + "\t\t// "), "\t" + "", "}" // NumberAnimation or similar... ); if (key == "position") { qmlAnimSet.push( "}" // ParallelAnimation ); } // save the current for the next iteration prevTime = time; prevValue = parsedValue; qmlAnim.push( qmlAnimSet.join(lineFeed + "\t\t") ); } qmlAnim.push( "}" // SequenceAnimation //"}" // AnimationController ); // initial values // it seems these are no longer needed once the layer properties are checked earlier... /* if (typeof initValue !== "undefined") { if (key == "position") { qmlItem.push( "x: " + round2(initValue[0]).toString(), "y: " + round2(initValue[1]).toString() ); } else if (key == "scale") { // QML has only a single scale value... qmlItem.push( "scale: " + round2(initValue[0].toString() / 100) ); } else if (key == "opacity") { // please note this is commented out for testing purposes only... qmlItem.push( "// opacity: " + round2(initValue.toString() / 100) ); } else if (key == "rotation") { qmlItem.push( "rotation: " + initValue.toString() ); } } */ qmlItem.push( qmlAnim.join(lineFeed + "\t") ); } } return qmlItem.join(lineFeed + "\t"); } function renderCommonQML(content) { var lines = [ "import QtQuick " + qtQuickVersion, "import \"Prosessor.js\" as Prosessor;", "", "Rectangle {", "", "width: 1200", "height: 1200", "" ]; // add more stuff.... lines.push(content); lines.push( "", "}", // Rectangle "" ); return lines.join(lineFeed); } if (app.project) { app.beginUndoGroup(scriptName); if (app.project.file) { // set the default/initial value for folder outputFolder = app.project.file.path; BuildAndShowUI(thisObj); } else { alert(scriptName + "\n" + "Please save the current project first", "Project not saved", true); } app.endUndoGroup(); } else { alert(scriptName + "\n" + "Please open or create a project with compositions first before using this script", "Project does not exist", true); } } ExportCompTransition(this); } --------------------------------------------------------------------------------