├── AnimateMate.sketchplugin └── Contents │ ├── Resources │ └── icon.icns │ └── Sketch │ ├── commands.js │ ├── library │ ├── Animate.js │ ├── Animation.js │ ├── Dialog.js │ ├── Gui.js │ ├── Utils.js │ ├── easing.js │ └── gifsicle │ └── manifest.json ├── LICENSE ├── README.md └── sketchpack.json /AnimateMate.sketchplugin/Contents/Resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Creatide/AnimateMate/75c3e8a1ec18d96ce097d98de228b2775827ef71/AnimateMate.sketchplugin/Contents/Resources/icon.icns -------------------------------------------------------------------------------- /AnimateMate.sketchplugin/Contents/Sketch/commands.js: -------------------------------------------------------------------------------- 1 | @import 'library/Dialog.js'; 2 | @import 'library/Utils.js'; 3 | 4 | function exportAnimation(context) { 5 | if (utils.init(context, true)) { 6 | dialog.exportAnimation(); 7 | } 8 | } 9 | 10 | function createAnimation(context) { 11 | if (utils.init(context)) { 12 | dialog.createAnimation(); 13 | } 14 | } 15 | 16 | function removeAnimation(context) { 17 | if (utils.init(context, false, true)) { 18 | dialog.removeAnimation(); 19 | } 20 | } 21 | 22 | function editAnimation(context) { 23 | if (utils.init(context)) { 24 | dialog.editAnimation(); 25 | } 26 | } 27 | 28 | function offsetAnimation(context) { 29 | if (utils.init(context)) { 30 | dialog.offsetAnimation(); 31 | } 32 | } 33 | 34 | function randomAnimation(context) { 35 | if (utils.init(context)) { 36 | dialog.randomAnimation(); 37 | } 38 | } 39 | 40 | function returnKeyframe(context) { 41 | if (utils.init(context, true)) { 42 | dialog.returnKeyframe(); 43 | } 44 | } 45 | 46 | function nextKeyframe(context) { 47 | if (utils.init(context, true)) { 48 | var keyframe = utils.closestValueAbove( 49 | utils.getKeyframeNumber(), animate.keyframeNumbers 50 | ); 51 | utils.setKeyframeNumber(keyframe); 52 | animate.returnKeyframe(keyframe); 53 | dialog.createBottomMessage(5, keyframe); 54 | } 55 | } 56 | 57 | function previousKeyframe(context) { 58 | if (utils.init(context, true)) { 59 | var keyframe = utils.closestValueBelow( 60 | utils.getKeyframeNumber(), animate.keyframeNumbers 61 | ); 62 | utils.setKeyframeNumber(keyframe); 63 | animate.returnKeyframe(keyframe); 64 | dialog.createBottomMessage(5, keyframe); 65 | } 66 | } 67 | 68 | function updateKeyframeValues(context) { 69 | if (utils.init(context, true)) { 70 | for (var i in tmpLayer = animate.animationLayers) { 71 | var animation = tmpLayer[i]; 72 | animation.setKeyframeValues(utils.getKeyframeNumber()); 73 | animation.updateLayerName(); 74 | } 75 | } 76 | } 77 | function reverseKeyframes(context) { 78 | if (utils.init(context)) { 79 | dialog.reverseKeyframes(); 80 | } 81 | } -------------------------------------------------------------------------------- /AnimateMate.sketchplugin/Contents/Sketch/library/Animate.js: -------------------------------------------------------------------------------- 1 | @import 'library/Animation.js'; 2 | 3 | var debugLog = false; 4 | var animate = new Animate(); 5 | function Animate () {} 6 | 7 | 8 | // ---------------------------------------- // 9 | // Initialize Object // 10 | // ---------------------------------------- // 11 | 12 | 13 | Animate.prototype.init = function (layers, loopNestedGroups) { 14 | 15 | this.animationLayers = []; 16 | this.otherLayers = []; 17 | this.allLayers = []; 18 | this.keyframeNumbers = []; 19 | this.startFrameNumber = 0; 20 | this.endFrameNumber = 0; 21 | this.exportName = utils.artboardName; 22 | this.referencePoints = ['Top Left', 'Center']; 23 | this.easingTypes = easing.getEasingNames(); 24 | this.defaultEasing = this.easingTypes[0]; 25 | this.layerMarks = { 26 | start: '{:', 27 | end: '}', 28 | values: ',', 29 | groups: ';', 30 | }; 31 | this.exportFolders = { 32 | tempFolder: null, 33 | exportFolder: null 34 | }; 35 | this.getLayers(layers, loopNestedGroups); 36 | 37 | return this.animationLayers.length; 38 | }; 39 | 40 | 41 | // ---------------------------------------- // 42 | // Command Functions // 43 | // ---------------------------------------- // 44 | 45 | 46 | Animate.prototype.exportAnimation = function (exportName, exportGif, exportPng, renderStartFrame, renderEndFrame, referencePoint, loopAnimation, delayAnimation, scaleValue, gifDither, gifOptimize, gifColors) { 47 | 48 | // Set base name for exported items 49 | this.exportName = exportName || this.exportName; 50 | 51 | // Set render range values 52 | var renderStartFrame = parseInt(renderStartFrame); 53 | var renderEndFrame = parseInt(renderEndFrame); 54 | if (renderEndFrame > this.endFrameNumber || renderEndFrame < this.startFrameNumber) renderEndFrame = this.endFrameNumber; 55 | if (renderStartFrame < 0 || renderStartFrame > renderEndFrame) renderStartFrame = 0; 56 | this.startFrameNumber = renderStartFrame; 57 | 58 | // Get number of digits from animation length for zero paddings. Add needed zero digits even the number is below 10 e.g. 01, 02, 03... 59 | var digitsCount = parseInt((this.endFrameNumber.toString()).length); 60 | var digitsNumber = digitsCount < 2 ? digitsCount + 1 : digitsCount; 61 | 62 | // Setup folder for animation 63 | this.createExportFolders(); 64 | // Test if folders is not empty 65 | if (!this.exportFolders.tempFolder || !this.exportFolders.exportFolder) return; 66 | 67 | // Store location of png files 68 | var pngFilesLocation = exportPng == 0 ? this.exportFolders.tempFolder.folderPath : this.exportFolders.exportFolder.folderPath; 69 | 70 | // Start timer and show bottom message about starting rendering process 71 | if (debugLog) dialog.createBottomMessage(1, utils.benchmarkTime.start()); 72 | 73 | // Calculate all keyframes and update values to object variable 74 | for (var j in this.animationLayers) { 75 | this.animationLayers[j].createAllKeyframes(); 76 | } 77 | 78 | var animationLayersLength = this.animationLayers.length; 79 | 80 | // Sort by layer level value (from most deepest to root) 81 | this.animationLayers.sort(utils.sortBy('layerLevel', true, parseInt)); 82 | 83 | // Log preprocessing render time 84 | if (debugLog) dialog.createLogMessage(1, ['pre-processing', utils.benchmarkTime.interval()]); 85 | 86 | // Loop animation frame through all frames in selected range 87 | for (var k = this.startFrameNumber; k <= renderEndFrame; k++) { 88 | 89 | // Loop all animation layers and apply values to layer 90 | for (var i = 0; i < animationLayersLength; i++) { 91 | 92 | var refLayer = this.animationLayers[i]; 93 | 94 | // Check that keyframe exist or it will crash with different length of animations 95 | if (refLayer.allKeyframes[k]) { 96 | refLayer.setOriginalValues(refLayer.allKeyframes[k], k, referencePoint); 97 | } 98 | } 99 | 100 | // Set scale value to default if it's wrongly typed 101 | var scaleValue = utils.isNumeric(scaleValue) && scaleValue > 0 ? scaleValue : 1; 102 | 103 | // Save PNG base images to selected location 104 | this.saveImagePNG(utils.zeroPadding(k, digitsNumber), pngFilesLocation, scaleValue); 105 | 106 | // Log frame render time 107 | if (debugLog) dialog.createLogMessage(1, [utils.zeroPadding(k, digitsNumber), utils.benchmarkTime.interval()]); 108 | } 109 | 110 | // Set layer original values back 111 | for (var l in this.animationLayers) { 112 | this.animationLayers[l].setOriginalValues(); 113 | } 114 | 115 | // Export / Create GIF animation from PNG images 116 | if (exportGif != 0 && this.exportFolders.tempFolder) { 117 | // Log GIF starting message 118 | if (debugLog) dialog.createLogMessage(2); 119 | this.saveImageGIF(pngFilesLocation, delayAnimation, loopAnimation, gifDither, gifOptimize, gifColors); 120 | } 121 | 122 | // Remove temporary folder 123 | this.exportFolders.tempFolder.fileManager.removeItemAtPath_error_(this.exportFolders.tempFolder.folderPath, null); 124 | 125 | // Show bottom message about starting rendering process 126 | if (debugLog) dialog.createBottomMessage(2, utils.benchmarkTime.stop()); 127 | }; 128 | 129 | 130 | Animate.prototype.createAnimation = function (easingType, keyframeNumber, changeEasingType) { 131 | 132 | var easingType = easingType || this.defaultEasing; 133 | var selectedEasingType = easingType; 134 | 135 | // Combine all layers together for loop 136 | this.allLayers = this.animationLayers.concat(this.otherLayers); 137 | var dialogAcceptAll = false; 138 | 139 | for (var i in tmpLayer = this.allLayers) { 140 | 141 | // If duplicate keyframes found ask user permission to overwrite 142 | if (!dialogAcceptAll && this.searchDuplicateKeyframe(tmpLayer[i], keyframeNumber)) { 143 | if (dialog.createDialogMessage(7, keyframeNumber) == 1001) { 144 | return; 145 | } else { 146 | dialogAcceptAll = true; 147 | } 148 | } 149 | 150 | // Update keyframe object values before build new layer name from those 151 | tmpLayer[i].setKeyframeValues(keyframeNumber); 152 | 153 | // Get random easing from array if not selected any specific 154 | if (selectedEasingType == 'Random Easing') easingType = this.easingTypes[Math.floor(Math.random() * this.easingTypes.length)]; 155 | 156 | // If animation exist in layer do not change easing type except user wanted so 157 | if (changeEasingType == true) { 158 | tmpLayer[i].updateLayerName(false, easingType); 159 | } else { 160 | tmpLayer[i].keyframesLength ? tmpLayer[i].updateLayerName() : tmpLayer[i].updateLayerName(false, easingType); 161 | } 162 | } 163 | }; 164 | 165 | 166 | Animate.prototype.removeAnimation = function (removeAll) { 167 | // Get layers again to loop nested groups and remove all animations 168 | if (removeAll) this.init(utils.layers, removeAll); 169 | for (var i in tmpLayer = this.animationLayers) { 170 | if (tmpLayer[i].layerBaseName.length == 0) { 171 | tmpLayer[i].layer.setName('EMPTY NAME'); 172 | } else { 173 | tmpLayer[i].layer.setName(tmpLayer[i].layerBaseName); 174 | } 175 | } 176 | }; 177 | 178 | 179 | Animate.prototype.offsetAnimation = function (offsetType, stepSize, responseValues) { 180 | 181 | var offsetType = offsetType.toLowerCase() || 'normal'; 182 | utils.objValuesToFloat(responseValues); 183 | 184 | // Calculate new values based to Offset type 185 | var offsetValues = function (originalValues, changeValues, additionalValue) { 186 | var returnValuesObj = {}; 187 | 188 | // Keyframe Number 189 | returnValuesObj.number = originalValues.number + changeValues.number + (changeValues.number != 0 ? changeValues.number < 0 ? -additionalValue : additionalValue : 0); 190 | 191 | // Position X 192 | returnValuesObj.x = originalValues.x + changeValues.x + (changeValues.x != 0 ? changeValues.x < 0 ? -additionalValue : additionalValue : 0); 193 | 194 | // Position Y 195 | returnValuesObj.y = originalValues.y + changeValues.y + (changeValues.y != 0 ? changeValues.y < 0 ? -additionalValue : additionalValue : 0); 196 | 197 | // Width 198 | returnValuesObj.width = originalValues.width + changeValues.width + (changeValues.width != 0 ? changeValues.width < 0 ? -additionalValue : additionalValue : 0); 199 | 200 | // Height 201 | returnValuesObj.height = originalValues.height + changeValues.height + (changeValues.height != 0 ? changeValues.height < 0 ? -additionalValue : additionalValue : 0); 202 | 203 | // Rotation 204 | returnValuesObj.rotation = originalValues.rotation + changeValues.rotation + (changeValues.rotation != 0 ? changeValues.rotation < 0 ? -additionalValue : additionalValue : 0); 205 | 206 | // Opacity (0-100 values conversion included) 207 | returnValuesObj.opacity = ((originalValues.opacity * 100) + changeValues.opacity + (changeValues.opacity != 0 ? changeValues.opacity < 0 ? -additionalValue * 10 : additionalValue * 10 : 0)) / 100; 208 | 209 | return returnValuesObj; 210 | } 211 | 212 | // Loop all animations to make chosen offset type for layer names 213 | for (var i in tmpLayer = this.animationLayers) { 214 | 215 | for (var j = 0; j < tmpLayer[i].keyframesLength; j++) { 216 | 217 | var refKeyframe = tmpLayer[i].keyframes[j]; 218 | var keyframeNumber = refKeyframe.number; 219 | 220 | switch (offsetType) { 221 | case 'normal': 222 | var offsetValuesObj = offsetValues(refKeyframe, responseValues, 0); 223 | tmpLayer[i].setKeyframeValues(keyframeNumber, offsetValuesObj, true, j); 224 | break; 225 | case 'stepped (layer)': 226 | var offsetValuesObj = offsetValues(refKeyframe, responseValues, j * stepSize); 227 | tmpLayer[i].setKeyframeValues(keyframeNumber, offsetValuesObj, true, j); 228 | break; 229 | case 'stepped (selection)': 230 | var offsetValuesObj = offsetValues(refKeyframe, responseValues, i * stepSize); 231 | tmpLayer[i].setKeyframeValues(keyframeNumber, offsetValuesObj, true, j); 232 | break; 233 | } 234 | } 235 | 236 | tmpLayer[i].updateLayerName(); 237 | } 238 | }; 239 | 240 | 241 | Animate.prototype.editAnimation = function (baseName, jsonKeyframes) { 242 | try { 243 | var convertedKeyframeData = JSON.parse('[' + jsonKeyframes + ']'); 244 | this.animationLayers[0].keyframes = convertedKeyframeData; 245 | this.animationLayers[0].updateLayerName(baseName, false); 246 | } catch (error) { 247 | dialog.createDialogMessage(9); 248 | log(error) 249 | } 250 | }; 251 | 252 | 253 | Animate.prototype.returnKeyframe = function (keyframeNumber) { 254 | for (var i in tmpLayer = this.animationLayers) { 255 | var searchIndex = utils.searchObjectArrayIndex(tmpLayer[i].keyframes, 'number', keyframeNumber); 256 | var refKeyframe = tmpLayer[i].keyframes[searchIndex]; 257 | tmpLayer[i].setOriginalValues(refKeyframe); 258 | } 259 | }; 260 | 261 | 262 | Animate.prototype.randomAnimation = function (responseValuesObj) { 263 | 264 | var easingType = responseValuesObj.easingType || this.defaultEasing; 265 | var selectedEasingType = easingType; 266 | 267 | // Combine all layers together for loop 268 | this.allLayers = this.animationLayers.concat(this.otherLayers); 269 | 270 | // Convert all keyframe values to float 271 | utils.objValuesToFloat(responseValuesObj); 272 | 273 | // Get how many frames is in total animation length 274 | var keyframesCount = responseValuesObj.animationLength / responseValuesObj.keyframeSpacing; 275 | if (isNaN(keyframesCount)) keyframesCount = 0; 276 | 277 | // Loop all layers 278 | for (var l in tmpLayer = this.allLayers) { 279 | 280 | // Get random easing from array if not selected any specific 281 | if (selectedEasingType == 'Random Easing') easingType = this.easingTypes[Math.floor(Math.random() * this.easingTypes.length)]; 282 | 283 | var keyframesArray = []; 284 | 285 | for (var i = 0; i <= keyframesCount; i++) { 286 | 287 | var tmpKeyframeObj = {}; 288 | tmpKeyframeObj.number = (i * responseValuesObj.animationLength) / keyframesCount; 289 | 290 | // Loop every property in object and make random value 291 | for (var k in tmpLayer[l].originalValues) { 292 | 293 | // If disabled channel then use original values 294 | if (responseValuesObj[k + 'Disable']) { 295 | tmpKeyframeObj[k] = tmpLayer[l].originalValues[k]; 296 | } 297 | else { 298 | 299 | var randomValue = utils.getRandomFloat(responseValuesObj[k + 'Min'], responseValuesObj[k + 'Max']); 300 | 301 | // If using additive mode 302 | if (responseValuesObj[k + 'Additive']) { 303 | tmpKeyframeObj[k] = tmpLayer[l].originalValues[k] + randomValue; 304 | } else { 305 | tmpKeyframeObj[k] = randomValue; 306 | } 307 | } 308 | } 309 | 310 | // If need to keep image scale ratio / proportion 311 | if (responseValuesObj['scaleRatio']) { 312 | var aspectRatioValue = utils.getAspectRatio(tmpLayer[l].originalValues.width, tmpLayer[l].originalValues.height, tmpKeyframeObj.width, tmpKeyframeObj.height); 313 | tmpKeyframeObj.width = aspectRatioValue.width; 314 | tmpKeyframeObj.height = aspectRatioValue.height; 315 | } 316 | 317 | utils.objValuesToFloat(tmpKeyframeObj); 318 | keyframesArray.push(tmpKeyframeObj); 319 | } 320 | 321 | // Create looping animation by copying first keyframe to last 322 | if (responseValuesObj.animationLoop) { 323 | var tmpNumber = keyframesArray[keyframesArray.length - 1].number; 324 | keyframesArray[keyframesArray.length - 1] = utils.cloneObj(keyframesArray[0]); 325 | keyframesArray[keyframesArray.length - 1].number = tmpNumber; 326 | } 327 | 328 | tmpLayer[l].keyframes = keyframesArray; 329 | tmpLayer[l].updateLayerName(false, easingType); 330 | } 331 | }; 332 | 333 | 334 | Animate.prototype.reverseKeyframes = function (keyframeFrom, keyframeTo) { 335 | 336 | var rangeIndexes = { 337 | from: undefined, 338 | to: undefined 339 | } 340 | 341 | // Reverse single animation 342 | if (keyframeFrom && keyframeTo) { 343 | // Search keyframe index numbers 344 | rangeIndexes.from = utils.searchObjectArrayIndex(this.animationLayers[0].keyframes, 'number', keyframeFrom); 345 | rangeIndexes.to = utils.searchObjectArrayIndex(this.animationLayers[0].keyframes, 'number', keyframeTo); 346 | 347 | 348 | // Switch index numbers if start index is bigger than end index 349 | if (rangeIndexes.from > rangeIndexes.to) { 350 | var tmpVal = rangeIndexes.from; 351 | rangeIndexes.from = rangeIndexes.to; 352 | rangeIndexes.to = tmpVal; 353 | } 354 | 355 | // Reverse keyframe numbers in array by range 356 | var endIndex = rangeIndexes.to; 357 | for ( var i = rangeIndexes.from; i < endIndex; i++ ) { 358 | 359 | // Switch values from first and last 360 | var aVal = this.animationLayers[0].keyframes[i].number; 361 | var bVal = this.animationLayers[0].keyframes[endIndex].number; 362 | this.animationLayers[0].keyframes[i].number = bVal; 363 | this.animationLayers[0].keyframes[endIndex].number = aVal; 364 | 365 | // Decrease range to affect next value from end 366 | endIndex--; 367 | } 368 | 369 | this.animationLayers[0].updateLayerName(); 370 | } 371 | 372 | // Reverse multiple animations if single layer not selected 373 | else { 374 | 375 | // Loop all animations 376 | for (var i in tmpLayer = this.animationLayers) { 377 | 378 | var endIndex = tmpLayer[i].keyframes.length - 1; 379 | 380 | // Loop all keyframes in current animation 381 | for ( var k = 0; k < endIndex; k++ ) { 382 | 383 | // Switch values from first and last 384 | var aVal = tmpLayer[i].keyframes[k].number; 385 | var bVal = tmpLayer[i].keyframes[endIndex].number; 386 | tmpLayer[i].keyframes[k].number = bVal; 387 | tmpLayer[i].keyframes[endIndex].number = aVal; 388 | 389 | // Decrease range to affect next value from end 390 | endIndex--; 391 | } 392 | 393 | tmpLayer[i].updateLayerName(); 394 | } 395 | } 396 | }; 397 | 398 | 399 | // ---------------------------------------- // 400 | // Get Layers // 401 | // ---------------------------------------- // 402 | 403 | 404 | // Loop all layers, get animations and create objects 405 | Animate.prototype.getLayers = function (layers, loopNestedGroups) { 406 | 407 | // Create array from keyframes 408 | var keyframeValuesToArray = function (keyframeValues) { 409 | 410 | var keyframeValues = keyframeValues || undefined; 411 | 412 | if (keyframeValues) { 413 | 414 | // Create keyframe object to hold all keyframe data 415 | var keyframesArray = []; 416 | for (var i = 0; i < keyframeValues.length; i++) { 417 | 418 | var keyframeObj = {}; 419 | var valuesArray = keyframeValues[i].split(animate.layerMarks.values); 420 | var valuesArrayLength = valuesArray.length; 421 | 422 | // Loop all values from array and set them to object properties 423 | for (var k = 0; k < valuesArrayLength; k++) { 424 | keyframeObj.number = valuesArray[0]; 425 | keyframeObj.x = valuesArray[1]; 426 | keyframeObj.y = valuesArray[2]; 427 | keyframeObj.width = valuesArray[3]; 428 | keyframeObj.height = valuesArray[4]; 429 | keyframeObj.rotation = valuesArray[5]; 430 | keyframeObj.opacity = valuesArray[6]; 431 | } 432 | 433 | // Convert all keyframe values to float 434 | utils.objValuesToFloat(keyframeObj); 435 | 436 | // Update keyframe numbers array for dialog dropdown list usage 437 | animate.keyframeNumbers.push(keyframeObj.number); 438 | // Remove doubles from keyframe numbers 439 | animate.keyframeNumbers = utils.uniqueNumber(animate.keyframeNumbers); 440 | 441 | // Update animation end frame 442 | animate.endFrameNumber = keyframeObj.number > animate.endFrameNumber ? keyframeObj.number : animate.endFrameNumber; 443 | 444 | // Push new keyframe object to array 445 | keyframesArray.push(keyframeObj); 446 | } 447 | 448 | // Sort keyframe numbers 449 | animate.keyframeNumbers.sort(function(a,b){return a - b}); 450 | 451 | // Sort array by keyframe numbers 452 | keyframesArray.sort(utils.sortBy('number', false, parseInt)); 453 | 454 | return keyframesArray; 455 | } 456 | } 457 | 458 | 459 | // Parse layer name to keyframes and create animation object 460 | var parseLayerName = function (layer, layerID) { 461 | 462 | // Parse animation from layer name and create Animation object 463 | var layerName = layer.name(); 464 | if (layerName.search(animate.layerMarks.start) != -1) { 465 | 466 | var animationString = layerName.split(animate.layerMarks.start).pop().split(animate.layerMarks.end)[0]; 467 | var keyframeValues = animationString.split(animate.layerMarks.groups); 468 | 469 | // Remove and create esing type from first item of array 470 | var easingType = keyframeValues.shift(); 471 | 472 | // Remove last empty grp because of last group split mark 473 | keyframeValues.pop(); 474 | 475 | // Create array of keyframes 476 | var keyframesArray = keyframeValuesToArray(keyframeValues); 477 | 478 | // Create new animation object and add it to animations array 479 | if (keyframesArray) { 480 | var animationObj = new Animation(layer, animationString, easingType, keyframesArray, layerID); 481 | animate.animationLayers.push(animationObj); 482 | } 483 | } else { 484 | var nonAnimatedObj = new Animation(layer, undefined, undefined, undefined, layerID); 485 | animate.otherLayers.push(nonAnimatedObj); 486 | } 487 | } 488 | 489 | 490 | // Loop all layers 491 | var loopLayers = function (layers, callback) { 492 | 493 | for (var i = 0; i < layers.count(); i++) { 494 | 495 | var refLayer = layers.objectAtIndex(i); 496 | var layerID = NSProcessInfo.processInfo().globallyUniqueString(); 497 | 498 | if (refLayer.isMemberOfClass(MSLayerGroup)) { 499 | callback(refLayer); 500 | parseLayerName(refLayer, layerID); 501 | if (loopNestedGroups) loopLayers(refLayer.layers(), callback); 502 | } else { 503 | callback(refLayer); 504 | parseLayerName(refLayer, layerID); 505 | } 506 | } 507 | } 508 | 509 | loopLayers(layers, function (layer) {}); 510 | }; 511 | 512 | 513 | // ---------------------------------------- // 514 | // Exporting // 515 | // ---------------------------------------- // 516 | 517 | 518 | Animate.prototype.saveImagePNG = function (keyframeNumber, exportFolder, scaleValue) { 519 | var exportFolder = exportFolder || this.exportFolders.tempFolder.folderPath; 520 | var fileName = exportFolder.stringByAppendingPathComponent(this.exportName + '_' + keyframeNumber + '.png'); 521 | if (utils.sketchVersion >= 41) { 522 | utils.doc.saveArtboardOrSlice_toFile_(utils.artboard, fileName); 523 | } 524 | else { 525 | // Work only with sketch < 41 version 526 | var scaleSlice = [MSExportRequest requestWithRect:utils.artboardRect scale:scaleValue]; 527 | utils.doc.saveArtboardOrSlice_toFile_(scaleSlice, fileName); 528 | } 529 | }; 530 | 531 | 532 | Animate.prototype.saveImageGIF = function (pngFilesLocation, loopValue, delayValue, gifDither, gifOptimizeLevel, gifColors) { 533 | 534 | var loop = ' -l'; 535 | var loopValue = parseInt(loopValue); 536 | var colorsValue = parseInt(gifColors); 537 | var dither = parseInt(gifDither) ? ' --dither' : ''; 538 | 539 | // Colors between 2-256 value 540 | colorsValue = colorsValue >= 2 && colorsValue <= 256 ? ' --colors=' + colorsValue : ''; 541 | 542 | // Switch optimize levels 543 | var optimize = ''; 544 | switch (parseInt(gifOptimizeLevel)) { 545 | case 1: 546 | optimize = ' -O1'; 547 | break; 548 | case 2: 549 | optimize = ' -O2'; 550 | break; 551 | case 3: 552 | optimize = ' -O3'; 553 | break; 554 | } 555 | 556 | // Prevent Gifsicle one extra loop round 557 | if (loopValue && !isNaN(loopValue)) { 558 | if (loopValue === 1) { 559 | loop = ' --no-loopcount'; 560 | } else { 561 | loop = ' -l' + (loopValue - 1); 562 | } 563 | } 564 | 565 | var delay = parseFloat(delayValue) ? ' -d' + parseFloat(delayValue) : ' -d0'; 566 | var gifExportName = this.exportName + '_' + this.startFrameNumber + '-' + this.endFrameNumber; 567 | 568 | // Gifsicle Manual: http://www.lcdf.org/gifsicle/man.html 569 | var gifConverter = utils.scriptLibraryPath + "/gifsicle"; 570 | 571 | // Create settings for conversion process 572 | var convertTask = NSTask.alloc().init(); 573 | var createTask = NSTask.alloc().init(); 574 | var exportFolder = this.exportFolders.exportFolder.folderPath; 575 | var tmpFolder = this.exportFolders.tempFolder.folderPath; 576 | 577 | // Create bash command arguments 578 | //var convertGifImages = "find \"" + pngFilesLocation + "\" -name '*.png' -exec sips -s format gif -o \"" + tmpFolder + "\" {}.gif {} \\;"; 579 | 580 | var convertGifImages = "find \"" + pngFilesLocation + "\" -iname '*.png' -type f -exec sh -c 'sips -s format gif \"$0\" --out \"" + tmpFolder + "\"' {} \\;"; 581 | 582 | var convertGifAnimation = "find \"" + tmpFolder + "\" -iname '*.gif' -execdir bash -c '\"" + gifConverter + "\"" + colorsValue + dither + optimize + loop + delay + " '*.gif' -o \"" + exportFolder + '/' + gifExportName + '.gif' + "\"' \\;"; 583 | 584 | //log("AnimateMate: " + convertGifImages ); 585 | 586 | // Create GIF Image Sequence from exist PNG images 587 | convertTask.setLaunchPath("/bin/bash"); 588 | convertTask.setArguments(["-c", convertGifImages]); 589 | convertTask.launch(); 590 | convertTask.waitUntilExit(); 591 | 592 | if (convertTask.terminationStatus() != 0) { 593 | dialog.createBottomMessage(4, 'GIF images conversion failed'); 594 | return; 595 | } 596 | 597 | // Create GIF animation from converted images 598 | createTask.setLaunchPath("/bin/bash"); 599 | createTask.setArguments(["-c", convertGifAnimation]); 600 | createTask.launch(); 601 | createTask.waitUntilExit(); 602 | 603 | if (createTask.terminationStatus() == 0) { 604 | dialog.createBottomMessage(3, this.exportFolders.exportFolder.folderPath); 605 | } else { 606 | dialog.createBottomMessage(4, 'GIF animation conversion failed'); 607 | } 608 | }; 609 | 610 | 611 | Animate.prototype.createExportFolders = function () { 612 | 613 | var createNewFolder = function (setCustomPath) { 614 | 615 | var newFolderPath, uniqueString; 616 | var fileManager = NSFileManager.defaultManager(); 617 | var returnObj = {}; 618 | 619 | // Create export path dialog 620 | if (setCustomPath) { 621 | newFolderPath = dialog.setExportPath(); 622 | if (newFolderPath == -1) { 623 | dialog.createBottomMessage(4, 'Export path selection canceled'); 624 | return false; 625 | } 626 | } 627 | // Temporary directory if custom path is false 628 | else { 629 | var tmpPathUrl = NSTemporaryDirectory(); 630 | uniqueString = NSProcessInfo.processInfo().globallyUniqueString(); 631 | newFolderPath = tmpPathUrl.stringByAppendingPathComponent(uniqueString); 632 | fileManager.createDirectoryAtPath_withIntermediateDirectories_attributes_error(newFolderPath, true, null, null); 633 | } 634 | 635 | returnObj.fileManager = fileManager; 636 | returnObj.folderPath = newFolderPath; 637 | 638 | // Check that export location is valid and show error if not 639 | if (!returnObj.fileManager.fileExistsAtPath(newFolderPath)) { 640 | dialog.createDialogMessage(6); 641 | return null; 642 | } 643 | 644 | return returnObj; 645 | } 646 | 647 | // Update export folders to object properties 648 | this.exportFolders.exportFolder = createNewFolder(true); 649 | this.exportFolders.tempFolder = createNewFolder(); 650 | }; 651 | 652 | 653 | // ---------------------------------------- // 654 | // Helpers // 655 | // ---------------------------------------- // 656 | 657 | 658 | Animate.prototype.searchDuplicateKeyframe = function (layer, searchNumber) { 659 | for (var i = 0; i < layer.keyframesLength; i++) { 660 | if (layer.keyframes[i].number == searchNumber) { 661 | return true; 662 | } 663 | } 664 | }; 665 | 666 | // The MIT License (MIT) 667 | // 668 | // Copyright (c) 2016 Creatide / Sakari Niittymaa 669 | // creatide.com - hello@creatide.com 670 | // 671 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 672 | // this software and associated documentation files (the "Software"), to deal in 673 | // the Software without restriction, including without limitation the rights to 674 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 675 | // the Software, and to permit persons to whom the Software is furnished to do so, 676 | // subject to the following conditions: 677 | // 678 | // The above copyright notice and this permission notice shall be included in all 679 | // copies or substantial portions of the Software. 680 | // 681 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 682 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 683 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 684 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 685 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 686 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /AnimateMate.sketchplugin/Contents/Sketch/library/Animation.js: -------------------------------------------------------------------------------- 1 | function Animation(layer, animationString, easingType, keyframes, layerID) { 2 | this.layer = layer; 3 | this.layerName = layer.name(); 4 | this.layerID = layerID; 5 | this.layerType = this.setLayerType(); 6 | this.layerLevel = this.getLayerLevel(); 7 | this.layerBaseName = this.layerName.split(animate.layerMarks.start)[0]; 8 | this.originalValues = this.getOriginalValues(); 9 | this.animationString = animationString || undefined; 10 | this.animationFullString = animate.layerMarks.start + this.animationString + animate.layerMarks.end; 11 | this.easingType = easingType || animate.defaultEasing; 12 | this.allKeyframes = []; 13 | this.keyframes = keyframes || []; 14 | this.keyframesLength = this.keyframes ? this.keyframes.length : 0; 15 | } 16 | 17 | 18 | Animation.prototype.updateLayerName = function (newBaseName, newEasingType, overwriteName) { 19 | 20 | var newEasingType = newEasingType || this.easingType; 21 | var rebuildAnimationString = animate.layerMarks.start + newEasingType + animate.layerMarks.groups; 22 | var keyframesLength = this.keyframes.length; 23 | 24 | this.keyframes.sort(utils.sortBy('number', false, parseInt)); 25 | 26 | for (var i = 0; i < keyframesLength; i++) { 27 | rebuildAnimationString += utils.objToString(this.keyframes[i]) + animate.layerMarks.groups; 28 | } 29 | 30 | rebuildAnimationString += animate.layerMarks.end; 31 | 32 | var newLayerName = this.layerBaseName + rebuildAnimationString; 33 | 34 | // Change layer base name if setted 35 | if (newBaseName) newLayerName = newLayerName.replace(this.layerBaseName, newBaseName); 36 | 37 | this.layer.setName(newLayerName); 38 | }; 39 | 40 | 41 | Animation.prototype.setKeyframeValues = function (keyframeNumber, valuesObject, changeKeyframeNumber, loopIndex) { 42 | 43 | var refKeyframe = null; 44 | var loopIndex = loopIndex || null; 45 | var valuesObject = valuesObject || this.getOriginalValues(); 46 | var duplicates = this.searchDuplicateKeyframes(keyframeNumber); 47 | 48 | if (!isNaN(duplicates)) { 49 | 50 | refKeyframe = this.keyframes[duplicates]; 51 | 52 | // Offset animation skip duplicates if not in current loop index 53 | if (loopIndex && loopIndex != duplicates) { 54 | refKeyframe = this.keyframes[loopIndex]; 55 | } 56 | 57 | } else { 58 | this.keyframes[this.keyframesLength] = {}; 59 | refKeyframe = this.keyframes[this.keyframesLength]; 60 | } 61 | 62 | refKeyframe.number = changeKeyframeNumber ? valuesObject.number : keyframeNumber; 63 | refKeyframe.x = valuesObject.x; 64 | refKeyframe.y = valuesObject.y; 65 | refKeyframe.width = valuesObject.width; 66 | refKeyframe.height = valuesObject.height; 67 | 68 | // Fix rotation issue after 3.8 update 69 | if ( utils.sketchVersion >= '3.8' ) { 70 | refKeyframe.rotation = valuesObject.rotation; 71 | } else { 72 | refKeyframe.rotation = valuesObject.rotation >= 360 ? 0 : valuesObject.rotation; 73 | } 74 | 75 | refKeyframe.opacity = Math.min(Math.max(parseFloat(valuesObject.opacity), 0), 1); 76 | }; 77 | 78 | 79 | Animation.prototype.createAllKeyframes = function () { 80 | 81 | var keyframesArray = []; 82 | 83 | // Create animation based main keyframes 84 | for (var i = 0; i < this.keyframesLength; i++) { 85 | 86 | var currentKeyframe = this.keyframes[i]; 87 | var nextKeyframe = utils.arrayNext(this.keyframes, i); 88 | var keyframeDifference = nextKeyframe ? Math.max(0, nextKeyframe.number - currentKeyframe.number - 1) : undefined; 89 | 90 | // Push current keyframe first and also to last keyframe because there is no next available 91 | keyframesArray.push(currentKeyframe); 92 | 93 | // If there is next keyframe available 94 | if (nextKeyframe && !isNaN(currentKeyframe.number) && !isNaN(nextKeyframe.number) && keyframeDifference) { 95 | 96 | // Calculate rotation direction 97 | // var rotDirection = (nextKeyframe.rotation - currentKeyframe.rotation + 360) % 360 > 180; 98 | // if (rotDirection) currentKeyframe.rotation = -(360 - currentKeyframe.rotation); 99 | 100 | // Crete between keyframes 101 | for (var j = 1; j <= keyframeDifference; j++) { 102 | 103 | var keyframeObj = {}; 104 | 105 | keyframeObj.number = parseInt(currentKeyframe.number) + j; 106 | 107 | keyframeObj.x = easing.getEasingValue(currentKeyframe.x, nextKeyframe.x, keyframeDifference, j, this.easingType); 108 | 109 | keyframeObj.y = easing.getEasingValue(currentKeyframe.y, nextKeyframe.y, keyframeDifference, j, this.easingType); 110 | 111 | keyframeObj.width = easing.getEasingValue(currentKeyframe.width, nextKeyframe.width, keyframeDifference, j, this.easingType); 112 | 113 | keyframeObj.height = easing.getEasingValue(currentKeyframe.height, nextKeyframe.height, keyframeDifference, j, this.easingType); 114 | 115 | keyframeObj.rotation = easing.getEasingValue(currentKeyframe.rotation, nextKeyframe.rotation, keyframeDifference, j, this.easingType); 116 | 117 | keyframeObj.opacity = easing.getEasingValue(currentKeyframe.opacity, nextKeyframe.opacity, keyframeDifference, j, this.easingType); 118 | 119 | keyframesArray.push(keyframeObj); 120 | } 121 | } 122 | } 123 | 124 | // Create/duplicate first main keyframes to beginning between (0 - firstKeyframe.number) 125 | var firstKeyframe = keyframesArray[0]; 126 | if (firstKeyframe.number != 0) { 127 | 128 | var tmpLength = firstKeyframe.number - 1; 129 | 130 | // Duplicate loop for make same keyframes 131 | while (tmpLength >= 0) { 132 | var refKeyframe = utils.cloneObj(firstKeyframe); 133 | refKeyframe.number = parseInt(tmpLength); 134 | keyframesArray.unshift(refKeyframe); 135 | tmpLength--; 136 | } 137 | } 138 | 139 | this.allKeyframes = keyframesArray; 140 | }; 141 | 142 | 143 | Animation.prototype.searchDuplicateKeyframes = function (keyframeNumber) { 144 | for (var i = 0; i < this.keyframesLength; i++) { 145 | if (this.keyframes[i].number == keyframeNumber) { 146 | return i; 147 | } 148 | } 149 | }; 150 | 151 | 152 | Animation.prototype.setLayerType = function () { 153 | if (this.layer.isMemberOfClass(MSLayerGroup)) { 154 | return 'group'; 155 | } else if (this.layer.isMemberOfClass(MSArtboardGroup)) { 156 | return 'artboard'; 157 | } else { 158 | return 'layer'; 159 | } 160 | }; 161 | 162 | 163 | Animation.prototype.getLayerLevel = function () { 164 | 165 | var parent = this.layer.parentGroup(); 166 | var levelNum = 0; 167 | 168 | while (parent.isMemberOfClass(MSLayerGroup)) { 169 | levelNum++; 170 | parent = parent.parentGroup(); 171 | } 172 | return levelNum; 173 | }; 174 | 175 | 176 | Animation.prototype.getParentGroups = function (layer) { 177 | 178 | var layer = layer || this.layer; 179 | var parents = {}; 180 | var parentGroups = []; 181 | 182 | var parentLoop = function (parent) { 183 | if (parent.isMemberOfClass(MSLayerGroup)) { 184 | parentGroups.push(parent) 185 | parentLoop(parent.parentGroup()); 186 | } else if (parent.isMemberOfClass(MSArtboardGroup)) { 187 | parents.artboard = parent; 188 | } 189 | } 190 | 191 | parentLoop(layer.parentGroup()); 192 | parents.groups = parentGroups; 193 | return parents; 194 | }; 195 | 196 | 197 | Animation.prototype.getParentValues = function (layer) { 198 | 199 | var layer = layer || this.layer; 200 | var animationLayers = null, parents = null, parentGroupValues = null; 201 | animationLayers = animate.animationLayers; 202 | parents = this.getParentGroups(layer); 203 | parentGroupValues = { 204 | x: 0, 205 | y: 0 206 | }; 207 | 208 | // Create absolute position values by looping parents values 209 | for (var i = 0; i < parents.groups.length; i++) { 210 | var groupRect = parents.groups[i].rect(); 211 | parentGroupValues.x += groupRect.origin.x; 212 | parentGroupValues.y += groupRect.origin.y; 213 | } 214 | 215 | return parentGroupValues; 216 | }; 217 | 218 | 219 | // Note: Nested layers/groups inside of groups are relative position values based to group postion values. 220 | // These values is made through parent group values. This function effects layer naming and setting layer original values for resetting positions. 221 | // e.g. layer x position inside of group is 0 even group x is something else. 222 | Animation.prototype.getOriginalValues = function (layer) { 223 | 224 | var layer = layer || this.layer; 225 | var parentGroupValues = this.getParentValues(); 226 | var returnObj = {}; 227 | 228 | // Updated based on this bug report https://github.com/Creatide/AnimateMate/issues/27 229 | // It seems that changing this one prevents object jumping after exporting, even exported content seems to be ok 230 | // Tested with Sketch 44 version. Not sure if there is problems with other versions. 231 | returnObj.x = Math.round((layer.frame().x()) * 100) / 100; 232 | returnObj.y = Math.round((layer.frame().y()) * 100) / 100; 233 | 234 | returnObj.width = Math.round(layer.frame().width() * 100) / 100; 235 | returnObj.height = Math.round(layer.frame().height() * 100) / 100; 236 | 237 | returnObj.rotation = Math.round((360 - layer.rotation()) * 100) / 100; 238 | returnObj.opacity = Math.round(layer.style().contextSettings().opacity() * 100) / 100; 239 | 240 | return returnObj; 241 | }; 242 | 243 | 244 | // Set actual physical position to layer object. 245 | Animation.prototype.setOriginalValues = function (valuesObj, keyframeNumber, referencePoint, keepProportions) { 246 | 247 | var refValues = valuesObj || this.originalValues; 248 | 249 | // Keep aspect ratio of item if chosen 250 | if (keepProportions) { 251 | var originalProportionState = this.layer.frame().constrainProportions(); 252 | this.layer.frame().setConstrainProportions(true); 253 | this.layer.frame().setWidth(refValues.width); 254 | // Return original state of proportion lock 255 | this.layer.frame().setConstrainProportions(originalProportionState); 256 | } else { 257 | this.layer.frame().setWidth(refValues.width); 258 | this.layer.frame().setHeight(refValues.height); 259 | } 260 | 261 | // Calculate center reference point 262 | if (referencePoint == 'Center') { 263 | 264 | var oldKeyframe = utils.arrayPrev(this.allKeyframes, keyframeNumber); 265 | oldKeyframe = oldKeyframe || refValues; 266 | 267 | var currentX = !keyframeNumber || refValues.x != oldKeyframe.x ? refValues.x : this.layer.frame().x(); 268 | var currentY = !keyframeNumber || refValues.y != oldKeyframe.y ? refValues.y : this.layer.frame().y(); 269 | 270 | this.layer.frame().setX(currentX - ((this.layer.frame().width() - oldKeyframe.width) / 2)); 271 | this.layer.frame().setY(currentY - ((this.layer.frame().height() - oldKeyframe.height) / 2)); 272 | } 273 | // Use top left for default reference point 274 | else { 275 | this.layer.frame().setX(refValues.x); 276 | this.layer.frame().setY(refValues.y); 277 | } 278 | 279 | this.layer.setRotation(360 - refValues.rotation); 280 | this.layer.style().contextSettings().setOpacity(refValues.opacity); 281 | }; 282 | 283 | // The MIT License (MIT) 284 | // 285 | // Copyright (c) 2016 Creatide / Sakari Niittymaa 286 | // creatide.com - hello@creatide.com 287 | // 288 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 289 | // this software and associated documentation files (the "Software"), to deal in 290 | // the Software without restriction, including without limitation the rights to 291 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 292 | // the Software, and to permit persons to whom the Software is furnished to do so, 293 | // subject to the following conditions: 294 | // 295 | // The above copyright notice and this permission notice shall be included in all 296 | // copies or substantial portions of the Software. 297 | // 298 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 299 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 300 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 301 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 302 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 303 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 304 | -------------------------------------------------------------------------------- /AnimateMate.sketchplugin/Contents/Sketch/library/Dialog.js: -------------------------------------------------------------------------------- 1 | @import 'library/Gui.js'; 2 | 3 | var dialog = new Dialog(); 4 | function Dialog () {} 5 | 6 | // ---------------------------------------- // 7 | // Command Dialogs // 8 | // ---------------------------------------- // 9 | 10 | // EXPORT ANIMATION / RENDER ANIMATION 11 | // "shortcut": "ctrl option cmd a" 12 | Dialog.prototype.exportAnimation = function () { 13 | 14 | // Warn if there is no animation in selected 15 | if (animate.animationLayers.length == 0) { 16 | dialog.createDialogMessage(3); 17 | return false; 18 | } 19 | 20 | var elements = [ 21 | { 22 | group: 'window', 23 | title: 'Export Animation', 24 | description: 'Export selected item(s) to animation. If there is nothing selected everything that have animation in current artboard is included the final animation export.', 25 | icon: 'icon.icns', 26 | reset: true 27 | }, 28 | { 29 | group: 'Export Settings', 30 | columns: 2, 31 | fontSize: 10, 32 | fontBold: true, 33 | uppercase: true, 34 | height: 15, 35 | items: [ 36 | { 37 | defaultId: 'exportGif', 38 | type: 'checkbox', 39 | value: 'GIF Animation', 40 | checked: false, 41 | column: 0 42 | }, 43 | { 44 | defaultId: 'exportPng', 45 | type: 'checkbox', 46 | value: 'PNG Sequence', 47 | checked: true, 48 | column: 1 49 | } 50 | ] 51 | }, 52 | { 53 | group: 'group', 54 | columns: 1, 55 | items: [ 56 | { 57 | type: 'input', 58 | value: animate.exportName, 59 | label: 'Export Base Name', 60 | column: 0 61 | } 62 | ] 63 | }, 64 | { 65 | group: 'group', 66 | columns: 2, 67 | items: [ 68 | { 69 | type: 'input', 70 | label: 'Start Frame', 71 | value: 0, 72 | column: 0 73 | }, 74 | { 75 | type: 'input', 76 | label: 'End Frame', 77 | value: animate.endFrameNumber, 78 | column: 1 79 | } 80 | ] 81 | }, 82 | { 83 | group: 'group', 84 | columns: 2, 85 | items: [ 86 | { 87 | defaultId: 'exportScaleValue', 88 | type: 'input', 89 | label: 'Scale (1 = 100%)', 90 | value: 1, 91 | column: 0 92 | }, 93 | { 94 | defaultId: 'exportAnchorPoint', 95 | default: 0, 96 | type: 'dropdown', 97 | label: 'Anchor Point', 98 | value: animate.referencePoints, 99 | column: 1 100 | } 101 | ] 102 | }, 103 | { 104 | group: 'GIF Settings', 105 | columns: 2, 106 | fontSize: 10, 107 | fontBold: true, 108 | uppercase: true, 109 | height: 15, 110 | items: [ 111 | { 112 | defaultId: 'exportGifDelay', 113 | type: 'input', 114 | label: 'Delay', 115 | value: 3, 116 | column: 0 117 | }, 118 | { 119 | defaultId: 'exportGifLoop', 120 | type: 'input', 121 | label: 'Loop Count (0 = infinite)', 122 | value: 0, 123 | column: 1 124 | } 125 | ] 126 | }, 127 | { 128 | group: 'group', 129 | columns: 3, 130 | items: [ 131 | { 132 | defaultId: 'exportGifDither', 133 | type: 'checkbox', 134 | value: 'Dither', 135 | checked: false, 136 | column: 2 137 | }, 138 | { 139 | defaultId: 'exportGifColors', 140 | type: 'input', 141 | label: 'Colors (2-256)', 142 | value: '0 = Disabled', 143 | column: 1 144 | }, 145 | { 146 | defaultId: 'exportGifOptimize', 147 | type: 'dropdown', 148 | label: 'Optimize Level', 149 | value: ['Disabled', 1, 2, 3], 150 | default: 3, 151 | column: 0 152 | } 153 | 154 | ] 155 | }, 156 | { 157 | group: 'Selection Info', 158 | columns: 2, 159 | fontSize: 10, 160 | fontBold: true, 161 | uppercase: true, 162 | height: 15, 163 | items: [ 164 | { 165 | type: 'label', 166 | value: 'Active Selection', 167 | height: 16, 168 | column: 0 169 | }, 170 | { 171 | type: 'label', 172 | value: utils.allLayersActive ? 'All layers selected' : utils.selection.count() + ' layers selected', 173 | fontBold: true, 174 | height: 16, 175 | fontColor: utils.selection.count() > 0 ? '#ff0000' : '#00aaff', 176 | column: 1 177 | } 178 | ] 179 | }, 180 | { 181 | group: 'group', 182 | columns: 2, 183 | items: [ 184 | { 185 | type: 'label', 186 | value: 'Animation Length', 187 | height: 16, 188 | column: 0 189 | }, 190 | { 191 | type: 'label', 192 | value: animate.endFrameNumber + ' frames', 193 | fontBold: true, 194 | height: 16, 195 | column: 1 196 | } 197 | ] 198 | }, 199 | { 200 | group: 'group', 201 | columns: 2, 202 | items: [ 203 | { 204 | type: 'label', 205 | value: 'Animated Layers', 206 | height: 16, 207 | column: 0 208 | }, 209 | { 210 | type: 'label', 211 | value: animate.animationLayers.length + ' layers', 212 | fontBold: true, 213 | height: 16, 214 | column: 1 215 | } 216 | ] 217 | } 218 | ]; 219 | 220 | // Warn user if nothing or artboard is selected 221 | var selectedAnswer = 1000; 222 | if (!utils.allLayersActive) { 223 | selectedAnswer = dialog.createDialogMessage(5); 224 | } 225 | 226 | if (selectedAnswer == 1000) { 227 | 228 | // Load default values before create dialog window 229 | var elementsDefaults = dialog.defaultValues(elements); 230 | 231 | var response = gui.createCustomForm(elements, true, true); 232 | 233 | if (response[0] == 1000) { 234 | 235 | // Save new default values 236 | dialog.defaultValues(elements, response[1], undefined); 237 | 238 | animate.exportAnimation(response[1][2].value, response[1][0].value, response[1][1].value, response[1][3].value, response[1][4].value, response[1][6].value, response[1][7].value, response[1][8].value, response[1][5].value, response[1][11].value, response[1][9].value, response[1][10].value); 239 | 240 | } else if (response[0] == 1002) { 241 | dialog.defaultValues(elements, undefined, elementsDefaults[1]); 242 | } 243 | } 244 | }; 245 | 246 | 247 | // CREATE ANIMATION / CREATE NEW KEYFRAME 248 | // "shortcut": "ctrl option cmd k" 249 | Dialog.prototype.createAnimation = function () { 250 | 251 | // Add random easing to easing array dropdown list 252 | animate.easingTypes.push('Random Easing'); 253 | 254 | var elements = [ 255 | { 256 | group: 'window', 257 | title: 'Create New Animation(s)', 258 | description: 'Select "Easing Type" and set "Keyframe number". Easing type only setted for the first time when creating animation to layer.', 259 | icon: 'icon.icns' 260 | }, 261 | { 262 | group: 'Animation Values', 263 | columns: 2, 264 | fontSize: 10, 265 | fontBold: true, 266 | uppercase: true, 267 | height: 15, 268 | items: [ 269 | { 270 | defaultId: 'createKfNumber', 271 | type: 'input', 272 | label: 'Keyframe Number', 273 | value: 0, 274 | column: 0 275 | }, 276 | { 277 | defaultId: 'createEasingType', 278 | type: 'dropdown', 279 | default: 0, 280 | value: animate.easingTypes, 281 | label: 'Easing Type', 282 | column: 1 283 | } 284 | ] 285 | }, 286 | { 287 | group: 'group', 288 | columns: 1, 289 | items: [ 290 | { 291 | defaultId: 'createChangeEasingType', 292 | type: 'checkbox', 293 | value: 'Change Easing Type', 294 | checked: false, 295 | column: 0 296 | } 297 | ] 298 | } 299 | ]; 300 | 301 | // Warn user if nothing or artboard is selected 302 | if (utils.allLayersActive) { 303 | dialog.createDialogMessage(4); 304 | return false; 305 | } else { 306 | 307 | // Load default values before create dialog window 308 | var elementsDefaults = dialog.defaultValues(elements); 309 | 310 | var response = gui.createCustomForm(elements, true, true); 311 | 312 | // Remove extra easing type (Random Easing) from array that was made for dialog 313 | animate.easingTypes.pop(); 314 | 315 | if (response[0] == 1000){ 316 | 317 | // Save new default values 318 | dialog.defaultValues(elements, response[1]); 319 | 320 | animate.createAnimation(response[1][1].value, response[1][0].value, response[1][2].value); 321 | 322 | utils.setKeyframeNumber(response[1][0].value); 323 | } 324 | else if (response[0] == 1002) { 325 | dialog.defaultValues(elements, undefined, elementsDefaults[1]); 326 | } 327 | } 328 | }; 329 | 330 | 331 | // REMOVE ANIMATION 332 | // "shortcut": "ctrl option cmd d" 333 | Dialog.prototype.removeAnimation = function () { 334 | 335 | var elements = [ 336 | { 337 | group: 'window', 338 | title: 'Remove All Animations', 339 | description: 'You not have anything selected. This remove all animations from current artboard. Do you want to remove all animations?', 340 | icon: 'icon.icns' 341 | } 342 | ]; 343 | 344 | var removeAll = false; 345 | var response; 346 | 347 | if (utils.artboard && utils.selection.count()) { 348 | response = [1000]; 349 | } else { 350 | removeAll = true; 351 | response = gui.createCustomForm(elements, true) 352 | } 353 | 354 | if (response[0] == 1000) animate.removeAnimation(removeAll); 355 | }; 356 | 357 | 358 | // EDIT ANIMATION 359 | // "shortcut": "ctrl option cmd l" 360 | Dialog.prototype.editAnimation = function () { 361 | 362 | // Warn user about no animation and stop editing 363 | if (animate.animationLayers.length == 0) { 364 | dialog.createDialogMessage(3); 365 | return false; 366 | } else { 367 | // Build JSON string for edit textbox and remove firs/last marks and whitespace 368 | var jsonData = JSON.stringify(animate.animationLayers[0].keyframes, null, ' ').slice(2, -2).replace(/ /g, ''); 369 | } 370 | 371 | var elements = [ 372 | { 373 | group: 'window', 374 | title: 'Edit Animation', 375 | description: 'Edit animation values through panel by setting custom values to chosen frame. You can edit only one animation at the time.', 376 | icon: 'icon.icns' 377 | }, 378 | { 379 | group: 'Layer Base Name', 380 | columns: 1, 381 | fontSize: 10, 382 | fontBold: true, 383 | uppercase: true, 384 | height: 15, 385 | items: [ 386 | { 387 | type: 'input', 388 | value: animate.animationLayers[0].layerBaseName, 389 | column: 0 390 | } 391 | ] 392 | }, 393 | { 394 | group: 'Animation Values', 395 | columns: 1, 396 | fontSize: 10, 397 | fontBold: true, 398 | uppercase: true, 399 | height: 15, 400 | items: [ 401 | { 402 | type: 'textbox', 403 | value: jsonData, 404 | height: 400, 405 | column: 0 406 | } 407 | ] 408 | } 409 | ]; 410 | 411 | // Warn user if there is more than one layer selected 412 | if (utils.selection.count() > 1 || utils.allLayersActive) { 413 | dialog.createDialogMessage(8); 414 | return false; 415 | } 416 | 417 | var response = gui.createCustomForm(elements, true); 418 | if (response[0] == 1000) animate.editAnimation(response[1][0].value, response[1][1].value); 419 | }; 420 | 421 | 422 | // OFFSET ANIMATION 423 | // "shortcut": "ctrl option cmd o" 424 | Dialog.prototype.offsetAnimation = function () { 425 | 426 | var elements = [ 427 | { 428 | group: 'window', 429 | title: 'Offset Animation(s)', 430 | description: 'Offset selected animations by custom values.', 431 | icon: 'icon.icns' 432 | }, 433 | { 434 | group: 'Offset Type', 435 | columns: 2, 436 | fontSize: 10, 437 | fontBold: true, 438 | uppercase: true, 439 | height: 15, 440 | items: [ 441 | { 442 | defaultId: 'offsetOffsetType', 443 | type: 'dropdown', 444 | default: 0, 445 | value: ['Normal', 'Stepped (Layer)', 'Stepped (Selection)'], 446 | label: 'Offset Type', 447 | column: 0 448 | }, 449 | { 450 | defaultId: 'offsetStepSize', 451 | type: 'input', 452 | label: 'Step Size', 453 | value: 0, 454 | column: 1 455 | } 456 | ] 457 | }, 458 | { 459 | group: 'Keyframes', 460 | columns: 1, 461 | fontSize: 10, 462 | fontBold: true, 463 | uppercase: true, 464 | height: 15, 465 | items: [ 466 | { 467 | defaultId: 'offsetOffsetKfNumbers', 468 | type: 'input', 469 | label: 'Offset Keyframe Numbers', 470 | value: 0, 471 | column: 0 472 | } 473 | ] 474 | }, 475 | { 476 | group: 'Offset Values', 477 | columns: 2, 478 | fontSize: 10, 479 | fontBold: true, 480 | uppercase: true, 481 | height: 15, 482 | items: [ 483 | { 484 | defaultId: 'offsetPosX', 485 | type: 'input', 486 | label: 'Position X', 487 | value: 0, 488 | column: 0 489 | }, 490 | { 491 | defaultId: 'offsetPosY', 492 | type: 'input', 493 | label: 'Position Y', 494 | value: 0, 495 | column: 1 496 | } 497 | ] 498 | }, 499 | { 500 | group: 'group', 501 | columns: 2, 502 | items: [ 503 | { 504 | defaultId: 'offsetWidth', 505 | type: 'input', 506 | label: 'Width', 507 | value: 0, 508 | column: 0 509 | }, 510 | { 511 | defaultId: 'offsetHeight', 512 | type: 'input', 513 | label: 'Height', 514 | value: 0, 515 | column: 1 516 | } 517 | ] 518 | }, 519 | { 520 | group: 'group', 521 | columns: 2, 522 | items: [ 523 | { 524 | defaultId: 'offsetRotation', 525 | type: 'input', 526 | label: 'Rotation (º)', 527 | value: 0, 528 | column: 0 529 | }, 530 | { 531 | defaultId: 'offsetOpacity', 532 | type: 'input', 533 | label: 'Opacity', 534 | value: 0, 535 | column: 1 536 | } 537 | ] 538 | } 539 | ]; 540 | 541 | // Load default values before create dialog window 542 | var elementsDefaults = dialog.defaultValues(elements); 543 | 544 | var response = gui.createCustomForm(elements, true, true); 545 | var offsetType = response[1][0].value; 546 | var stepSize = response[1][1].value; 547 | var responseValuesObj = { 548 | number: response[1][2].value, 549 | x: response[1][3].value, 550 | y: response[1][4].value, 551 | width: response[1][5].value, 552 | height: response[1][6].value, 553 | rotation: response[1][7].value, 554 | opacity: response[1][8].value, 555 | } 556 | 557 | // Remove periods from input values 558 | utils.objRemovePropertyCharacter(responseValuesObj, ','); 559 | 560 | if (response[0] == 1000) { 561 | 562 | // Save new default values 563 | dialog.defaultValues(elements, response[1]); 564 | 565 | animate.offsetAnimation(offsetType, stepSize, responseValuesObj); 566 | } 567 | else if (response[0] == 1002) { 568 | dialog.defaultValues(elements, undefined, elementsDefaults[1]); 569 | } 570 | }; 571 | 572 | 573 | // RESTORE KEYFRAME TO ITEM 574 | // "shortcut": "ctrl option cmd r" 575 | Dialog.prototype.returnKeyframe = function () { 576 | 577 | // Warn if there is no animation in selected 578 | if (animate.animationLayers.length == 0) { 579 | dialog.createDialogMessage(3); 580 | return false; 581 | } 582 | 583 | // Warn user if there is more than one layer selected 584 | if (utils.selection.count() > 1 || utils.allLayersActive) { 585 | dialog.createDialogMessage(10); 586 | } 587 | 588 | var elements = [ 589 | { 590 | group: 'window', 591 | title: 'Restore Keyframe to Item', 592 | description: 'This will restore selected keyframe values to item. This is good for checking visually item states in different keyframes. This feature is also pretty useful with using custom states and jumping between those.', 593 | icon: 'icon.icns' 594 | }, 595 | { 596 | group: 'Keyframe Number', 597 | columns: 1, 598 | fontSize: 10, 599 | fontBold: true, 600 | uppercase: true, 601 | height: 15, 602 | items: [ 603 | { 604 | type: 'dropdown', 605 | default: 0, 606 | value: animate.keyframeNumbers, 607 | column: 0 608 | } 609 | ] 610 | } 611 | ]; 612 | 613 | var response = gui.createCustomForm(elements, true); 614 | if (response[0] == 1000) animate.returnKeyframe(response[1][0].value); 615 | }; 616 | 617 | 618 | // REVERSE KEYFRAMES 619 | // "shortcut": "ctrl option cmd b" 620 | Dialog.prototype.reverseKeyframes = function () { 621 | 622 | // Warn if there is no animation in selected 623 | if (animate.animationLayers.length == 0) { 624 | dialog.createDialogMessage(3); 625 | return false; 626 | } 627 | 628 | // Reverse all animations and warn user about it 629 | if (utils.selection.count() > 1 || utils.allLayersActive) { 630 | var elements = [ 631 | { 632 | group: 'window', 633 | title: 'Reverse All Animations', 634 | description: 'You not have multiple animations selected. This will reverse all selected animations. Select only one layer to reverse single animation by range. Do you want to reverse all selected layers animations?', 635 | icon: 'icon.icns' 636 | } 637 | ]; 638 | 639 | var response = gui.createCustomForm(elements, true); 640 | if (response[0] == 1000) animate.reverseKeyframes(); 641 | } 642 | 643 | // Reverse only single animation 644 | else { 645 | var elements = [ 646 | { 647 | group: 'window', 648 | title: 'Reverse Keyframes', 649 | description: 'This will reverse keyframes by selecting custom range of keyframes. Select first and last keyframes and all keyframes inside of that range will be reversed. By default it uses first and last keyframe to affect all keyframes.', 650 | icon: 'icon.icns' 651 | }, 652 | { 653 | group: 'Reverse Keyframes Range', 654 | columns: 2, 655 | fontSize: 10, 656 | fontBold: true, 657 | uppercase: true, 658 | height: 15, 659 | items: [ 660 | { 661 | type: 'dropdown', 662 | label: 'From', 663 | default: 0, 664 | value: animate.keyframeNumbers, 665 | column: 0 666 | }, 667 | { 668 | type: 'dropdown', 669 | label: 'To', 670 | default: animate.keyframeNumbers.length - 1, 671 | value: animate.keyframeNumbers, 672 | column: 1 673 | } 674 | ] 675 | } 676 | ]; 677 | 678 | var response = gui.createCustomForm(elements, true); 679 | if (response[0] == 1000) animate.reverseKeyframes(response[1][0].value, response[1][1].value); 680 | } 681 | }; 682 | 683 | 684 | // RANDOM ANIMATION 685 | // "shortcut": "ctrl option cmd g" 686 | Dialog.prototype.randomAnimation = function () { 687 | 688 | // Add random easing to easing array dropdown list 689 | animate.easingTypes.unshift('Random Easing'); 690 | 691 | var elements = [ 692 | { 693 | group: 'window', 694 | title: 'Random Animation', 695 | description: 'Generate random animation to selected layers by custom values.', 696 | icon: 'icon.icns' 697 | }, 698 | { 699 | group: 'Animation', 700 | columns: 2, 701 | fontSize: 10, 702 | fontBold: true, 703 | uppercase: true, 704 | height: 15, 705 | items: [ 706 | { 707 | defaultId: 'randomAnimationLength', 708 | type: 'input', 709 | label: 'Animation Length', 710 | value: 30, 711 | column: 0 712 | }, 713 | { 714 | defaultId: 'randomKfSpacing', 715 | type: 'input', 716 | label: 'Keyframe Spacing', 717 | value: 10, 718 | column: 1 719 | } 720 | ] 721 | }, 722 | { 723 | group: 'group', 724 | columns: 2, 725 | items: [ 726 | { 727 | defaultId: 'randomEasingType', 728 | type: 'dropdown', 729 | default: 0, 730 | value: animate.easingTypes, 731 | label: 'Easing Type', 732 | column: 0 733 | }, 734 | { 735 | defaultId: 'randomLoopAnimation', 736 | type: 'checkbox', 737 | label: 'Looping', 738 | value: 'Loop Animation', 739 | checked: false, 740 | column: 1 741 | } 742 | ] 743 | }, 744 | { 745 | group: 'Position', 746 | columns: 4, 747 | fontSize: 10, 748 | fontBold: true, 749 | uppercase: true, 750 | height: 15, 751 | items: [ 752 | { 753 | defaultId: 'randomPosMinX', 754 | type: 'input', 755 | label: 'Min X', 756 | value: 0, 757 | column: 0 758 | }, 759 | { 760 | defaultId: 'randomPosMaxX', 761 | type: 'input', 762 | label: 'Max X', 763 | value: utils.artboardSize.width, 764 | column: 1 765 | }, 766 | { 767 | defaultId: 'randomPosMinY', 768 | type: 'input', 769 | label: 'Min Y', 770 | value: 0, 771 | column: 2 772 | }, 773 | { 774 | defaultId: 'randomPosMaxY', 775 | type: 'input', 776 | label: 'Max Y', 777 | value: utils.artboardSize.height, 778 | column: 3 779 | } 780 | ] 781 | }, 782 | { 783 | group: 'group', 784 | columns: 3, 785 | items: [ 786 | { 787 | defaultId: 'randomPosDisableX', 788 | type: 'checkbox', 789 | value: 'Disable X', 790 | checked: false, 791 | column: 0 792 | }, 793 | { 794 | defaultId: 'randomPosDisableY', 795 | type: 'checkbox', 796 | value: 'Disable Y', 797 | checked: false, 798 | column: 1 799 | }, 800 | { 801 | defaultId: 'randomPosAdditive', 802 | type: 'checkbox', 803 | value: 'Additive', 804 | checked: false, 805 | column: 2 806 | } 807 | ] 808 | }, 809 | { 810 | group: 'Scale', 811 | columns: 4, 812 | fontSize: 10, 813 | fontBold: true, 814 | uppercase: true, 815 | height: 15, 816 | items: [ 817 | { 818 | defaultId: 'randomScaleMinX', 819 | type: 'input', 820 | label: 'Min Width', 821 | value: 100, 822 | column: 0 823 | }, 824 | { 825 | defaultId: 'randomScaleMaxX', 826 | type: 'input', 827 | label: 'Max Width', 828 | value: 120, 829 | column: 1 830 | }, 831 | { 832 | defaultId: 'randomScaleMinY', 833 | type: 'input', 834 | label: 'Min Height', 835 | value: 100, 836 | column: 2 837 | }, 838 | { 839 | defaultId: 'randomScaleMaxY', 840 | type: 'input', 841 | label: 'Max Height', 842 | value: 120, 843 | column: 3 844 | } 845 | ] 846 | }, 847 | { 848 | group: 'group', 849 | columns: 2, 850 | items: [ 851 | { 852 | defaultId: 'randomScaleDisableX', 853 | type: 'checkbox', 854 | value: 'Disable Width', 855 | checked: true, 856 | column: 0 857 | }, 858 | { 859 | defaultId: 'randomScaleDisableY', 860 | type: 'checkbox', 861 | value: 'Disable Height', 862 | checked: true, 863 | column: 1 864 | } 865 | ] 866 | }, 867 | { 868 | group: 'group', 869 | columns: 2, 870 | items: [ 871 | { 872 | defaultId: 'randomScaleKeepAspect', 873 | type: 'checkbox', 874 | value: 'Keep Aspect Ratio', 875 | checked: false, 876 | column: 0 877 | }, 878 | { 879 | defaultId: 'randomScaleAdditive', 880 | type: 'checkbox', 881 | value: 'Additive', 882 | checked: false, 883 | column: 1 884 | } 885 | ] 886 | }, 887 | { 888 | group: 'Rotation', 889 | columns: 2, 890 | fontSize: 10, 891 | fontBold: true, 892 | uppercase: true, 893 | height: 15, 894 | items: [ 895 | { 896 | defaultId: 'randomRotMinRot', 897 | type: 'input', 898 | label: 'Min Rotation', 899 | value: 0, 900 | column: 0 901 | }, 902 | { 903 | defaultId: 'randomRotMaxRot', 904 | type: 'input', 905 | label: 'Max Rotation', 906 | value: 90, 907 | column: 1 908 | } 909 | ] 910 | }, 911 | { 912 | group: 'group', 913 | columns: 2, 914 | items: [ 915 | { 916 | defaultId: 'randomRotDisableRot', 917 | type: 'checkbox', 918 | value: 'Disable Rotation', 919 | checked: true, 920 | column: 0 921 | }, 922 | { 923 | defaultId: 'randomRotAdditive', 924 | type: 'checkbox', 925 | value: 'Additive', 926 | checked: false, 927 | column: 1 928 | } 929 | ] 930 | }, 931 | { 932 | group: 'Opacity', 933 | columns: 2, 934 | fontSize: 10, 935 | fontBold: true, 936 | uppercase: true, 937 | height: 15, 938 | items: [ 939 | { 940 | defaultId: 'randomOpacityMinOpacity', 941 | type: 'input', 942 | label: 'Min Opacity', 943 | value: 1, 944 | column: 0 945 | }, 946 | { 947 | defaultId: 'randomOpacityMaxOpacity', 948 | type: 'input', 949 | label: 'Max Opacity', 950 | value: 1, 951 | column: 1 952 | } 953 | ] 954 | }, 955 | { 956 | group: 'group', 957 | columns: 2, 958 | items: [ 959 | { 960 | defaultId: 'randomOpacityDisableOpacity', 961 | type: 'checkbox', 962 | value: 'Disable Opacity', 963 | checked: true, 964 | column: 0 965 | }, 966 | { 967 | defaultId: 'randomOpacityAdditive', 968 | type: 'checkbox', 969 | value: 'Additive', 970 | checked: false, 971 | column: 1 972 | } 973 | ] 974 | } 975 | ]; 976 | 977 | // Load default values before create dialog window 978 | var elementsDefaults = dialog.defaultValues(elements); 979 | 980 | var response = gui.createCustomForm(elements, true, true); 981 | var responseValuesObj = { 982 | animationLength: response[1][0].value, 983 | keyframeSpacing: response[1][1].value, 984 | easingType: response[1][2].value, 985 | animationLoop: response[1][3].value, 986 | xMin: response[1][4].value, 987 | xMax: response[1][5].value, 988 | yMin: response[1][6].value, 989 | yMax: response[1][7].value, 990 | xDisable: response[1][8].value, 991 | yDisable: response[1][9].value, 992 | xAdditive: response[1][10].value, 993 | yAdditive: response[1][10].value, 994 | widthMin: response[1][11].value, 995 | widthMax: response[1][12].value, 996 | heightMin: response[1][13].value, 997 | heightMax: response[1][14].value, 998 | widthDisable: response[1][15].value, 999 | heightDisable: response[1][16].value, 1000 | scaleRatio: response[1][17].value, 1001 | widthAdditive: response[1][18].value, 1002 | heightAdditive: response[1][18].value, 1003 | rotationMin: response[1][19].value, 1004 | rotationMax: response[1][20].value, 1005 | rotationDisable: response[1][21].value, 1006 | rotationAdditive: response[1][22].value, 1007 | opacityMin: response[1][23].value, 1008 | opacityMax: response[1][24].value, 1009 | opacityDisable: response[1][25].value, 1010 | opacityAdditive: response[1][26].value 1011 | } 1012 | 1013 | // Remove periods from input values 1014 | utils.objRemovePropertyCharacter(responseValuesObj, ','); 1015 | 1016 | // Remove extra easing type (Random Easing) from array that was made for dialog 1017 | animate.easingTypes.shift(); 1018 | 1019 | if (response[0] == 1000) { 1020 | // Save new default values 1021 | dialog.defaultValues(elements, response[1]); 1022 | animate.randomAnimation(responseValuesObj); 1023 | } 1024 | else if (response[0] == 1002) { 1025 | dialog.defaultValues(elements, undefined, elementsDefaults[1]); 1026 | } 1027 | 1028 | }; 1029 | 1030 | 1031 | // ---------------------------------------- // 1032 | // Remember Dialog Values // 1033 | // ---------------------------------------- // 1034 | 1035 | // Got this idea from: https://github.com/abynim/SketchPlugin-Remember 1036 | // Thanks: @abynim 1037 | // Used in dialogs: Export, Create, Offset, Random 1038 | 1039 | Dialog.prototype.defaultValues = function (elements, responseValues, resetDefaults) { 1040 | 1041 | var uiDefaults = {}; 1042 | var userDefaults = {}; 1043 | var storedDefaults = {}; 1044 | var responseValues = responseValues; 1045 | 1046 | // Get / Update stored default values to memory 1047 | function getStoredDefaults(initialValues) { 1048 | 1049 | var refPluginDomain = utils.pluginDomain; 1050 | var defaults = [[NSUserDefaults standardUserDefaults] objectForKey: refPluginDomain]; 1051 | var defaultValues = {}; 1052 | 1053 | for (var key in defaults) { 1054 | defaultValues[key] = defaults[key]; 1055 | } 1056 | 1057 | for (var key in initialValues) { 1058 | if (defaultValues[key] == null) defaultValues[key] = initialValues[key]; 1059 | } 1060 | 1061 | storedDefaults = defaultValues; 1062 | } 1063 | 1064 | // Save new default values to memory 1065 | function saveDefaults(newValues) { 1066 | 1067 | var refPluginDomain = utils.pluginDomain; 1068 | 1069 | if (refPluginDomain) { 1070 | 1071 | var defaults = [[NSUserDefaults standardUserDefaults] objectForKey: refPluginDomain]; 1072 | var defaultValues = {}; 1073 | 1074 | for (var key in defaults) { 1075 | defaultValues[key] = defaults[key]; 1076 | } 1077 | 1078 | for (var key in newValues) { 1079 | if (defaultValues[key] != newValues[key]) { 1080 | defaultValues[key] = newValues[key]; 1081 | } 1082 | } 1083 | 1084 | // Replace defaults with new object 1085 | var defaultsRef = [NSUserDefaults standardUserDefaults]; 1086 | [defaultsRef setObject: defaultValues forKey: refPluginDomain]; 1087 | 1088 | // Comment out to clear NSUserDefaults 1089 | // [defaultsRef setObject: null forKey: refPluginDomain]; 1090 | // log("AnimateMate: " + defaults); 1091 | 1092 | storedDefaults = defaults; 1093 | } 1094 | } 1095 | 1096 | // Collect values for UI and USER defined presets 1097 | function getDefaultPresets() { 1098 | 1099 | var foundObjectsArr = utils.findObjByProperty(elements, 'defaultId', 'items', true); 1100 | 1101 | // Search pairs from submitted values and default values 1102 | function searchMatchResponseValues(tmpObj) { 1103 | var tmpId = tmpObj.searchId; 1104 | var refObjId = tmpId.join(''); 1105 | for (var i = 0; i < responseValues.length; i++) { 1106 | var responseId = (responseValues[i]['id'].split(':')).slice(0, -1).join(''); 1107 | if (refObjId == responseId) { 1108 | //log("AnimateMate: " + responseValues[i].value + " :: " + tmpObj.defaultId); 1109 | return responseValues[i].value; 1110 | } 1111 | } 1112 | } 1113 | 1114 | // Update default values to objects 1115 | for (var obj in foundObjectsArr) { 1116 | 1117 | var refObj = foundObjectsArr[obj]; 1118 | 1119 | switch (refObj.type) { 1120 | case 'input': 1121 | uiDefaults[refObj.defaultId] = refObj.value; 1122 | if (responseValues) userDefaults[refObj.defaultId] = searchMatchResponseValues(refObj); 1123 | break; 1124 | case 'checkbox': 1125 | uiDefaults[refObj.defaultId] = refObj.checked; 1126 | if (responseValues) userDefaults[refObj.defaultId] = searchMatchResponseValues(refObj); 1127 | break; 1128 | case 'dropdown': 1129 | uiDefaults[refObj.defaultId] = refObj.value[refObj.default]; 1130 | //uiDefaults[refObj.defaultId] = refObj.default; 1131 | if (responseValues) userDefaults[refObj.defaultId] = searchMatchResponseValues(refObj); 1132 | break; 1133 | } 1134 | } 1135 | 1136 | // Initialize stored defaults 1137 | getStoredDefaults(uiDefaults); 1138 | 1139 | // Save new values / Reset values 1140 | if (resetDefaults) { 1141 | saveDefaults(resetDefaults); 1142 | } else if (responseValues) { 1143 | saveDefaults(userDefaults); 1144 | } 1145 | } 1146 | 1147 | // Replace object property values with other object property values if there's same property names 1148 | function updateElements (origObj, newValObj) { 1149 | 1150 | var refObj = origObj; 1151 | 1152 | function getObject(refObj) { 1153 | if (refObj instanceof Array) { 1154 | for (var i = 0; i < refObj.length; i++) { 1155 | getObject(refObj[i]); 1156 | } 1157 | } else { 1158 | for (var oldProp in refObj) { 1159 | if (refObj[oldProp] instanceof Array) { 1160 | for (var i = 0; i < refObj[oldProp].length; i++) { 1161 | getObject(refObj[oldProp][i]); 1162 | } 1163 | } 1164 | for (var newProp in newValObj) { 1165 | if (refObj[oldProp] == newProp) { 1166 | switch (refObj.type) { 1167 | case 'input': 1168 | refObj.value = newValObj[newProp]; 1169 | break; 1170 | case 'checkbox': 1171 | refObj.checked = newValObj[newProp]; 1172 | break; 1173 | case 'dropdown': 1174 | //log(refObj.value[refObj.default] + " " + newValObj[newProp]) 1175 | var newDropdownIndex = utils.searchArrayIndex(refObj.value, newValObj[newProp]); 1176 | refObj.default = newDropdownIndex; 1177 | break; 1178 | } 1179 | } 1180 | } 1181 | } 1182 | } 1183 | } 1184 | getObject(refObj); 1185 | return refObj; 1186 | } 1187 | 1188 | getDefaultPresets(); 1189 | // Return array 1: updated elements 2: original element defaults 1190 | return [updateElements(elements, storedDefaults), uiDefaults]; 1191 | }; 1192 | 1193 | 1194 | // ---------------------------------------- // 1195 | // Export Path // 1196 | // ---------------------------------------- // 1197 | 1198 | 1199 | Dialog.prototype.setExportPath = function () { 1200 | 1201 | var openDialog = NSOpenPanel.openPanel(); 1202 | 1203 | openDialog.setCanChooseDirectories(true); 1204 | openDialog.setCanChooseFiles(false); 1205 | openDialog.setAllowsMultipleSelection(false); 1206 | openDialog.setCanCreateDirectories(true); 1207 | //openDialog.showsResizeIndicator(); 1208 | //openDialog.showsHiddenFiles(); 1209 | //openDialog.setAllowedFileTypes(["gif, png"]); 1210 | openDialog.setTitle('Export'); 1211 | openDialog.setMessage('Export Animation to Folder'); 1212 | openDialog.setPrompt('Select Folder'); 1213 | 1214 | if (openDialog.runModal() == NSOKButton) { 1215 | return openDialog.URL().path(); 1216 | } else { 1217 | return -1; 1218 | } 1219 | }; 1220 | 1221 | 1222 | // ---------------------------------------- // 1223 | // Messages // 1224 | // ---------------------------------------- // 1225 | 1226 | // Pop-up dialog messages 1227 | Dialog.prototype.createDialogMessage = function (messageId, optionalMessage) { 1228 | 1229 | switch (messageId) { 1230 | 1231 | case 1: 1232 | gui.createDialogMessage("Alert", "You do not have any artboard active. Active one artboard to continue.", false, 'icon.icns'); 1233 | break; 1234 | 1235 | case 2: 1236 | gui.createDialogMessage("Error", "There was an error in animation data string in layer name. Every animation have keyframes and those contains as many values. If you have edited animation keyframes manually this can lead errors if there is not all values in place.", false, 'icon.icns'); 1237 | break; 1238 | 1239 | case 3: 1240 | gui.createDialogMessage("Info", "There is no any animations in selected layers or document. Create new animation or select layer or artboard that have animations", false, 'icon.icns'); 1241 | break; 1242 | 1243 | case 4: 1244 | gui.createDialogMessage("Alert", "You not have anything selected or artboard is selected. Select layer or group to make new animation.", false, 'icon.icns'); 1245 | break; 1246 | 1247 | case 5: 1248 | return gui.createDialogMessage("Export Specific Layer(s)", "You have selected individual layer(s) to export. This will export only selected layers animations and others will be static. Deselect all or select artboard to export all animations.", true, 'icon.icns'); 1249 | break; 1250 | 1251 | case 6: 1252 | gui.createDialogMessage("Export Location Error", "There is problem with export location. Please try again and select proper folder to export animation.", false, 'icon.icns'); 1253 | break; 1254 | 1255 | case 7: 1256 | return gui.createDialogMessage("Overwrite", "There is already keyframe in number " + optionalMessage + ".\nDo you want to overwrite exist keyframe?", true, 'icon.icns'); 1257 | break; 1258 | 1259 | case 8: 1260 | gui.createDialogMessage("Select Only One", "You've selected multiple layers or animations. You can use this function only with one animation at the time. Select one layer with animation and try again.", false, 'icon.icns'); 1261 | break; 1262 | 1263 | case 9: 1264 | gui.createDialogMessage("Data Failure", "There was an error in data conversion process. You've wrong parameters or missing values in input text. Please try again.", false, 'icon.icns'); 1265 | break; 1266 | 1267 | case 10: 1268 | gui.createDialogMessage("Multiple Item Selected", "You've selected multiple layers or animations. All available keyframes will be shown but only items with same keyframe numbers will be affected.", false, 'icon.icns'); 1269 | break; 1270 | 1271 | case 11: 1272 | gui.createDialogMessage("Export Failed", "Export process failed.", false, 'icon.icns'); 1273 | break; 1274 | } 1275 | }; 1276 | 1277 | 1278 | // Bottom show text only messages 1279 | Dialog.prototype.createBottomMessage = function (messageId, optionalMessage) { 1280 | 1281 | switch (messageId) { 1282 | 1283 | case 1: 1284 | gui.createInfoMessage("Rendering Images. (" + optionalMessage + ")"); 1285 | log(utils.scriptName + " rendering process started..."); 1286 | break; 1287 | 1288 | case 2: 1289 | gui.createInfoMessage("Rendering complete in time: " + optionalMessage); 1290 | log(utils.scriptName + " export process complete (" + optionalMessage + ")"); 1291 | break; 1292 | 1293 | case 3: 1294 | gui.createInfoMessage("Animated GIF created succesfully to: " + optionalMessage); 1295 | log(utils.scriptName + " created animated GIF succesfully (" + optionalMessage + ")"); 1296 | break; 1297 | 1298 | case 4: 1299 | gui.createInfoMessage("Process Failed. (" + optionalMessage + ")"); 1300 | log(utils.scriptName + " Process Failed (" + optionalMessage + ")"); 1301 | break; 1302 | 1303 | case 5: 1304 | gui.createInfoMessage("Keyframe Selected:" + optionalMessage ); 1305 | log(utils.scriptName + "Keyframe Selected:" + optionalMessage ); 1306 | break; 1307 | } 1308 | }; 1309 | 1310 | // Log messages 1311 | Dialog.prototype.createLogMessage = function (messageId, optionalMessage) { 1312 | 1313 | switch (messageId) { 1314 | 1315 | case 1: 1316 | log(utils.scriptName + " frame " + optionalMessage[0] + " done in " + optionalMessage[1][0] + " (Total: " + optionalMessage[1][1] + ")"); 1317 | break; 1318 | 1319 | case 2: 1320 | log(utils.scriptName + " starting GIF conversion process. It could take long time to complete. GIF file flickering while processing, so do not panic!"); 1321 | break; 1322 | } 1323 | }; 1324 | 1325 | // The MIT License (MIT) 1326 | // 1327 | // Copyright (c) 2016 Creatide / Sakari Niittymaa 1328 | // creatide.com - hello@creatide.com 1329 | // 1330 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 1331 | // this software and associated documentation files (the "Software"), to deal in 1332 | // the Software without restriction, including without limitation the rights to 1333 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 1334 | // the Software, and to permit persons to whom the Software is furnished to do so, 1335 | // subject to the following conditions: 1336 | // 1337 | // The above copyright notice and this permission notice shall be included in all 1338 | // copies or substantial portions of the Software. 1339 | // 1340 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1341 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 1342 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 1343 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 1344 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 1345 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /AnimateMate.sketchplugin/Contents/Sketch/library/Gui.js: -------------------------------------------------------------------------------- 1 | var gui = new Gui(); 2 | 3 | // ---------------------------------------- // 4 | // UI Style Settings // 5 | // ---------------------------------------- // 6 | 7 | function Gui () { 8 | this.defaults = { 9 | rectX: 0, 10 | rectY: 0, 11 | rectW: 300, 12 | rectH: 25, 13 | itemMarginX: 6, 14 | itemMarginY: 5, 15 | inputHeight: 25, 16 | labelHeight: 20, 17 | fontSize: 12, 18 | fontColor: '#000000', 19 | groupLabelFontSize: 13, 20 | groupLabelFontColor: '#969696', 21 | emptyTitle: 'Title', 22 | emptyMessage: 'Message', 23 | emptyLabel: 'Label', 24 | emptyArrayValues: ['First Item', 'Second Item', 'Third Item'], 25 | autoIdSplitter: ':' 26 | } 27 | } 28 | 29 | 30 | // ---------------------------------------- // 31 | // UI Parts // 32 | // ---------------------------------------- // 33 | 34 | // Alert window base setup 35 | Gui.prototype.makeBaseDialog = function (useCancelBtn, useResetBtn) { 36 | var alert = COSAlertWindow.new(); 37 | alert.addButtonWithTitle('OK'); 38 | if (useCancelBtn) alert.addButtonWithTitle('Cancel'); 39 | if (useResetBtn) alert.addButtonWithTitle('Reset Defaults'); 40 | return alert; 41 | }; 42 | 43 | // Label 44 | Gui.prototype.makeLabel = function (text, fontSize, fontColor, bold, frameArray) { 45 | var text = text || this.defaults.emptyLabel; 46 | var fontSize = fontSize || this.defaults.fontSize; 47 | var fontColor = hexToRgb(fontColor); 48 | var bold = bold || false; 49 | var frameArray = frameArray || [this.defaults.rectX, this.defaults.rectY, this.defaults.rectW, this.defaults.inputHeight]; 50 | var frame = NSMakeRect(frameArray[0], frameArray[1], frameArray[2], frameArray[3]); 51 | var label = NSTextField.alloc().initWithFrame(frame); 52 | label.setStringValue(text); 53 | label.textColor = NSColor.colorWithDeviceRed_green_blue_alpha_(fontColor[0], fontColor[1], fontColor[2], 1.0); 54 | label.setFont((bold) ? NSFont.boldSystemFontOfSize(fontSize) : NSFont.systemFontOfSize(fontSize)); 55 | label.setEditable(false); 56 | label.setSelectable(false); 57 | label.setDrawsBackground(false); 58 | label.setBezeled(false); 59 | return label; 60 | }; 61 | 62 | // Textbox 63 | Gui.prototype.makeTextbox = function (text, fontSize, bold, selectable, editable, frameArray) { 64 | var text = text || this.defaults.emptyMessage; 65 | var fontSize = fontSize || this.defaults.fontSize; 66 | var bold = bold || false; 67 | var frameArray = frameArray || [this.defaults.rectX, this.defaults.rectY, this.defaults.rectW, this.defaults.inputHeight]; 68 | var frame = NSMakeRect(frameArray[0], frameArray[1], frameArray[2], frameArray[3]); 69 | var textbox = NSTextField.alloc().initWithFrame(frame); 70 | textbox.setStringValue(text); 71 | textbox.setFont((bold) ? NSFont.boldSystemFontOfSize(fontSize) : NSFont.systemFontOfSize(fontSize)); 72 | textbox.setEditable(editable); 73 | textbox.setSelectable(selectable); 74 | return textbox; 75 | }; 76 | 77 | // Dropdown 78 | Gui.prototype.makeDropdown = function (valuesArray, frameArray) { 79 | var valuesArray = valuesArray || this.defaults.emptyArrayValues; 80 | var frameArray = frameArray || [this.defaults.rectX, this.defaults.rectY, this.defaults.rectW, this.defaults.inputHeight]; 81 | var frame = NSMakeRect(frameArray[0], frameArray[1], frameArray[2], frameArray[3]); 82 | var combo = NSComboBox.alloc().initWithFrame(frame); 83 | combo.addItemsWithObjectValues(valuesArray); 84 | return combo; 85 | }; 86 | 87 | // Checkbox 88 | Gui.prototype.makeCheckbox = function (text, checked, frameArray) { 89 | var checked = (checked == false) ? NSOffState : NSOnState; 90 | var frameArray = frameArray || [this.defaults.rectX, this.defaults.rectY, this.defaults.rectW, this.defaults.inputHeight]; 91 | var frame = NSMakeRect(frameArray[0], frameArray[1], frameArray[2], frameArray[3]); 92 | var checkbox = NSButton.alloc().initWithFrame(frame); 93 | checkbox.setTitle(text); 94 | checkbox.setState(checked); 95 | checkbox.setButtonType(NSSwitchButton); 96 | checkbox.setBezelStyle(0); 97 | return checkbox; 98 | }; 99 | 100 | 101 | // ---------------------------------------- // 102 | // UI Windows - Simple // 103 | // ---------------------------------------- // 104 | 105 | // Info Message to Bottom of Screen 106 | Gui.prototype.createInfoMessage = function (message) { 107 | var message = message || this.defaults.emptyMessage; 108 | utils.doc.showMessage(message); 109 | }; 110 | 111 | // Basic dialog message 112 | Gui.prototype.createDialogMessage = function (title, message, useCancelBtn, iconName) { 113 | 114 | // Make dialog base 115 | var alert = this.makeBaseDialog(useCancelBtn); 116 | var title = title || this.defaults.emptyTitle; 117 | var message = message || this.defaults.emptyMessage; 118 | if (title) alert.setMessageText(title); 119 | if (message) alert.setInformativeText(message); 120 | 121 | // Set custom icon for window 122 | if (iconName) { 123 | var icon = NSImage.alloc().initByReferencingFile(utils.scriptResourcesPath + '/' + iconName); 124 | alert.setIcon(icon); 125 | } 126 | 127 | var responseCode = alert.runModal(); 128 | 129 | return responseCode; 130 | } 131 | 132 | // Create single dropdown dialog 133 | Gui.prototype.createDropdownDialog = function (title, message, valuesArray, defaultIndex, useCancelBtn, iconName) { 134 | 135 | // Make dialog base 136 | var alert = this.makeBaseDialog(useCancelBtn); 137 | var title = title || this.defaults.emptyTitle; 138 | var message = message || this.defaults.emptyMessage; 139 | var valuesArray = valuesArray || this.defaults.emptyArrayValues; 140 | var defaultIndex = defaultIndex || 0; 141 | if (title) alert.setMessageText(title); 142 | if (message) alert.setInformativeText(message); 143 | 144 | var dropdown = this.makeDropdown(valuesArray); 145 | dropdown.selectItemAtIndex(defaultIndex); 146 | alert.addAccessoryView(dropdown); 147 | 148 | // Set custom icon for window 149 | if (iconName) { 150 | var icon = NSImage.alloc().initByReferencingFile(utils.scriptResourcesPath + '/' + iconName); 151 | alert.setIcon(icon); 152 | } 153 | 154 | var responseCode = alert.runModal(); 155 | var inputs = [dropdown.indexOfSelectedItem()]; 156 | 157 | return [responseCode, inputs]; 158 | }; 159 | 160 | 161 | // ---------------------------------------- // 162 | // UI Windows - Custom Form // 163 | // ---------------------------------------- // 164 | 165 | // Create custom inputs form 166 | Gui.prototype.createCustomForm = function (inputObjectsArray, useCancelBtn, useResetBtn) { 167 | 168 | // Window porperties 169 | var winObj, winWidth; 170 | var winHeight = 0; 171 | 172 | // Separate window object from array and update width 173 | if (inputObjectsArray[0].group == 'window') { 174 | winObj = inputObjectsArray.shift(); 175 | winWidth = winObj.width || this.defaults.rectW; 176 | } 177 | 178 | // Create arrays to hold data for later in build dialog 179 | var groupArray = []; 180 | var inputCollector = []; 181 | 182 | // GROUP loop 183 | var objLength = inputObjectsArray.length; 184 | for (var i = 0; i < objLength; i++) { 185 | 186 | // Create label for group if there is custom value other than 'window' or 'group' 187 | if (inputObjectsArray[i].group.toLowerCase() != 'window' && inputObjectsArray[i].group.toLowerCase() != 'group' && inputObjectsArray[i].group != null && inputObjectsArray[i].group != '') { 188 | 189 | var grpLabelFontSize = inputObjectsArray[i].fontSize || this.defaults.groupLabelFontSize; 190 | var grpLabelFontColor = inputObjectsArray[i].fontColor || this.defaults.groupLabelFontColor; 191 | var grpLabelFontBold = inputObjectsArray[i].fontBold || false; 192 | var grpLabelHeight = inputObjectsArray[i].height || this.defaults.rectH; 193 | var grpLabel = inputObjectsArray[i].uppercase ? inputObjectsArray[i].group.toUpperCase() : inputObjectsArray[i].group; 194 | 195 | groupArray.push(this.makeLabel(grpLabel, grpLabelFontSize, grpLabelFontColor, grpLabelFontBold, [0, 0 + this.defaults.itemMarginY, this.defaults.rectW, grpLabelHeight])); 196 | } 197 | 198 | // Create object to hold data for group 199 | var newGrp = []; 200 | var columnsArray = []; 201 | var groupHeight = 0; 202 | 203 | // Create new group rect 204 | var refGroup = inputObjectsArray[i]; 205 | 206 | // COLUMNS loop 207 | for (var j = 0; j < refGroup.columns; j++) { 208 | 209 | var itemsArray = []; 210 | var columnWidth = winWidth / refGroup.columns; 211 | var columnHeight = 0; 212 | 213 | // Reverse array to get items right order in window 214 | var refItems = refGroup.items.reverse(); 215 | 216 | // ITEMS loop 217 | for (var k = 0; k < refItems.length; k++) { 218 | 219 | var refItem = refGroup.items[k]; 220 | 221 | // Pick only target columns 222 | if (refItem.column == j) { 223 | 224 | // Create automatic ID selector for input (group:column:input) 225 | var autoInputID = i.toString() + this.defaults.autoIdSplitter + j.toString() + this.defaults.autoIdSplitter + k.toString(); 226 | 227 | // Basic values for item 228 | var itemHeight = refItem.height || this.defaults.inputHeight; 229 | var fontSize = refItem.fontSize || this.defaults.fontSize; 230 | var fontColor = refItem.fontColor || this.defaults.fontColor; 231 | var fontBold = refItem.fontBold || false; 232 | var fontUppercase = refItem.uppercase || false; 233 | var textSelectable = refItem.selectable || true; 234 | var textEditable = refItem.editable || true; 235 | var labelText = fontUppercase ? refItem.label.toUpperCase() : refItem.label; 236 | 237 | // Make UI items base them input type 238 | switch (refItem.type.toLowerCase()) { 239 | 240 | case 'label': 241 | var newItem = this.makeLabel(refItem.value, fontSize, fontColor, fontBold, [0, columnHeight + this.defaults.itemMarginY, columnWidth - this.defaults.itemMarginX, itemHeight]); 242 | // Push item to arrays 243 | itemsArray.push(newItem); 244 | columnHeight += itemHeight + this.defaults.itemMarginY; 245 | break; 246 | 247 | case 'input': 248 | var newItem = NSTextField.alloc().initWithFrame(NSMakeRect(0, columnHeight + this.defaults.itemMarginY, columnWidth - this.defaults.itemMarginX, itemHeight)); 249 | // Set default value if values exist 250 | newItem.setStringValue((refItem.value != null) ? refItem.value : ""); 251 | // Push item to arrays 252 | itemsArray.push(newItem); 253 | inputCollector.push({id: autoInputID, item: newItem}); 254 | columnHeight += itemHeight + this.defaults.itemMarginY; 255 | // If label exist 256 | if (labelText) { 257 | itemsArray.push(this.makeLabel(labelText, fontSize, fontColor, fontBold, [0, columnHeight, columnWidth - this.defaults.itemMarginX, this.defaults.labelHeight])); 258 | columnHeight += this.defaults.labelHeight; 259 | } 260 | break; 261 | 262 | case 'textbox': 263 | var newItem = this.makeTextbox(refItem.value, fontSize, fontBold, textSelectable, textEditable, [0, columnHeight + this.defaults.itemMarginY, columnWidth - this.defaults.itemMarginX, itemHeight]); 264 | // Push item to arrays 265 | itemsArray.push(newItem); 266 | inputCollector.push({id: autoInputID, item: newItem}); 267 | columnHeight += itemHeight + this.defaults.itemMarginY; 268 | // If label exist 269 | if (labelText) { 270 | itemsArray.push(this.makeLabel(labelText, fontSize, fontColor, fontBold, [0, columnHeight, columnWidth - this.defaults.itemMarginX, this.defaults.labelHeight])); 271 | columnHeight += this.defaults.labelHeight; 272 | } 273 | break; 274 | 275 | case 'dropdown': 276 | var defaultIndex = refItem.default || 0; 277 | var newItem = this.makeDropdown(refItem.value, [0, columnHeight + this.defaults.itemMarginY, columnWidth - this.defaults.itemMarginX, itemHeight]); 278 | // Set default index value to item 279 | newItem.selectItemAtIndex(defaultIndex); 280 | // Push item to arrays 281 | itemsArray.push(newItem); 282 | inputCollector.push({id: autoInputID, item: newItem}); 283 | columnHeight += itemHeight + this.defaults.itemMarginY; 284 | // If label exist 285 | if (labelText) { 286 | itemsArray.push(this.makeLabel(labelText, fontSize, fontColor, fontBold, [0, columnHeight, columnWidth - this.defaults.itemMarginX, this.defaults.labelHeight])); 287 | columnHeight += this.defaults.labelHeight; 288 | } 289 | break; 290 | 291 | case 'checkbox': 292 | var checkedState = refItem.checked || false; 293 | var newItem = this.makeCheckbox(refItem.value, checkedState, [0, columnHeight + this.defaults.itemMarginY, columnWidth - this.defaults.itemMarginX, itemHeight]); 294 | // Push item to arrays 295 | itemsArray.push(newItem); 296 | inputCollector.push({id: autoInputID, item: newItem}); 297 | columnHeight += itemHeight + this.defaults.itemMarginY; 298 | // If label exist 299 | if (labelText) { 300 | itemsArray.push(this.makeLabel(labelText, fontSize, fontColor, fontBold, [0, columnHeight, columnWidth - this.defaults.itemMarginX, this.defaults.labelHeight])); 301 | columnHeight += this.defaults.labelHeight; 302 | } 303 | break; 304 | 305 | } 306 | } 307 | } 308 | 309 | // Push column rect to array stack before join items to it 310 | var newColumn = NSView.alloc().initWithFrame(NSMakeRect(columnWidth * j, 0, columnWidth, columnHeight)); 311 | 312 | // Get highest columns height for group height 313 | groupHeight = groupHeight < columnHeight ? columnHeight : groupHeight; 314 | 315 | // Make subviews for column from items 316 | for (var l = 0; l < itemsArray.length; l++) { 317 | newColumn.addSubview(itemsArray[l]); 318 | } 319 | 320 | // Push column to array for later use in groups 321 | columnsArray.push(newColumn); 322 | } 323 | 324 | // Create new group 325 | var newGroup = NSView.alloc().initWithFrame(NSMakeRect(0, winHeight, this.defaults.rectW, groupHeight)); 326 | 327 | // Make subviews for group from columns 328 | for (var m = 0; m < columnsArray.length; m++) { 329 | newGroup.addSubview(columnsArray[m]); 330 | } 331 | 332 | // Push new group to array for later use in main dialog 333 | groupArray.push(newGroup); 334 | 335 | // Update window size value 336 | winHeight += groupHeight; 337 | } 338 | 339 | // Make dialog base 340 | var alert = this.makeBaseDialog(useCancelBtn, useResetBtn); 341 | var title = winObj.title || this.defaults.emptyTitle; 342 | var message = winObj.description || this.defaults.emptyMessage; 343 | var iconName = winObj.icon || false; 344 | if (title) alert.setMessageText(title); 345 | if (message) alert.setInformativeText(message); 346 | 347 | // Set custom icon for window 348 | if (iconName) { 349 | var icon = NSImage.alloc().initByReferencingFile(utils.scriptResourcesPath + '/' + iconName); 350 | alert.setIcon(icon); 351 | } 352 | 353 | // Loop all groups and drop those in order to window 354 | for (var i = 0; i < groupArray.length; i++) { 355 | alert.addAccessoryView(groupArray[i]); 356 | } 357 | 358 | // Get return code and call window 359 | var responseCode = alert.runModal(); 360 | 361 | // Generate inputs to readable return objects with answers 362 | for (var n = 0; n < inputCollector.length; n++) { 363 | inputCollector[n].value = inputCollector[n].item.stringValue(); 364 | } 365 | 366 | return [responseCode, inputCollector]; 367 | }; 368 | 369 | 370 | // ---------------------------------------- // 371 | // Helpers // 372 | // ---------------------------------------- // 373 | 374 | // Convert hex values to RGB values in range of 0-1 375 | function hexToRgb(hex, returnStringVal) { 376 | var hex = hex.replace('#', ''); 377 | var returnStringVal = returnStringVal || false; 378 | var bigint = parseInt(hex, 16); 379 | var r = +(1 / 255 * ((bigint >> 16) & 255)).toFixed(2); 380 | var g = +(1 / 255 * ((bigint >> 8) & 255)).toFixed(2); 381 | var b = +(1 / 255 * (bigint & 255)).toFixed(2); 382 | return returnStringVal ? [r, g, b].join() : [r, g, b]; 383 | } 384 | 385 | // The MIT License (MIT) 386 | // 387 | // Copyright (c) 2016 Creatide / Sakari Niittymaa 388 | // creatide.com - hello@creatide.com 389 | // 390 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 391 | // this software and associated documentation files (the "Software"), to deal in 392 | // the Software without restriction, including without limitation the rights to 393 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 394 | // the Software, and to permit persons to whom the Software is furnished to do so, 395 | // subject to the following conditions: 396 | // 397 | // The above copyright notice and this permission notice shall be included in all 398 | // copies or substantial portions of the Software. 399 | // 400 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 401 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 402 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 403 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 404 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 405 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /AnimateMate.sketchplugin/Contents/Sketch/library/Utils.js: -------------------------------------------------------------------------------- 1 | const Settings = require('sketch/settings'); 2 | @import 'library/easing.js'; 3 | @import 'library/Animate.js'; 4 | 5 | var utils = new Utils(); 6 | 7 | function Utils() { 8 | // General values 9 | this.scriptName = 'AnimateMate'; 10 | this.pluginDomain = "com.creatide.sketch.animatemate"; 11 | this.sketchVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:"CFBundleShortVersionString"]; 12 | this.scriptPath = null; 13 | this.scriptPathRoot = null; 14 | this.scriptResourcesPath = null; 15 | this.scriptLibraryPath = null, 16 | this.scriptURL = null; 17 | this.doc = null; 18 | this.page = null; 19 | this.artboard = null; 20 | this.selection = null; 21 | this.layers = null; 22 | this.layersCount = 0; 23 | this.answerBtn = [1000, false]; 24 | } 25 | 26 | Utils.prototype.init = function (context, loopNestedGroups, forceContinue) { 27 | 28 | this.scriptPath = context.scriptPath; 29 | this.scriptPathRoot = this.scriptPath.stringByDeletingLastPathComponent(); 30 | this.scriptResourcesPath = this.scriptPathRoot.stringByDeletingLastPathComponent() + '/Resources'; 31 | this.scriptLibraryPath = this.scriptPathRoot + '/library'; 32 | this.scriptURL = context.scriptURL; 33 | this.doc = context.document; 34 | this.selection = context.selection; 35 | this.page = this.doc.currentPage(); 36 | this.artboard = this.page.currentArtboard(); 37 | this.allLayersActive = false; 38 | 39 | if (this.artboard) { 40 | 41 | // Set artboard name 42 | this.artboardName = this.artboard.name(); 43 | 44 | // Get artboard rect for size 45 | this.artboardRect = this.artboard.rect(); 46 | this.artboardSize = { 47 | width: this.artboardRect.size.width, 48 | height: this.artboardRect.size.height 49 | }; 50 | 51 | if (this.selection.count() > 0) { 52 | this.layers = this.selection; 53 | // If artboard is selected use all layers from that 54 | if (this.selection.firstObject().isMemberOfClass(MSArtboardGroup)) { 55 | this.layers = this.artboard.layers(); 56 | this.allLayersActive = true; 57 | } 58 | } 59 | // If nothing is selected use all layers from artboard 60 | else { 61 | this.layers = this.artboard.layers(); 62 | this.allLayersActive = true; 63 | } 64 | 65 | // Update layers count number 66 | this.layersCount = this.layers.count(); 67 | 68 | // Init main animate object 69 | animate.init(this.layers, loopNestedGroups); 70 | 71 | // Force continue to return true even there is no animation layers or all layers is selected 72 | if (forceContinue) return true; 73 | 74 | // Initialize animation layers in conditional and check if there is all layers active 75 | if (!animate && this.allLayersActive) { 76 | dialog.createDialogMessage(3); 77 | return false; 78 | } 79 | 80 | return true; 81 | 82 | } else { 83 | // No artboard selected warning 84 | dialog.createDialogMessage(1); 85 | return false; 86 | } 87 | }; 88 | 89 | 90 | Utils.prototype.getKeyframeNumber = function() { 91 | return Settings.settingForKey('AnimateMateFrame') || 0; 92 | } 93 | 94 | Utils.prototype.setKeyframeNumber = function(keyframe) { 95 | log('saved keyframe'+ keyframe); 96 | Settings.setSettingForKey('AnimateMateFrame', keyframe); 97 | } 98 | 99 | // ---------------------------------------- // 100 | // Helpers // 101 | // ---------------------------------------- // 102 | 103 | Utils.prototype.getRandomFloat = function (min, max, round) { 104 | var round = round || true, 105 | randomNum = Math.random() * (max - min) + min; 106 | if (round) randomNum = Math.round(randomNum * 100) / 100; 107 | return randomNum; 108 | }; 109 | 110 | Utils.prototype.getRandomInt = function (min, max) { 111 | return Math.floor(Math.random() * (max - min + 1)) + min; 112 | }; 113 | 114 | Utils.prototype.isNumeric = function (n) { 115 | return !isNaN(parseFloat(n)) && isFinite(n); 116 | }; 117 | 118 | Utils.prototype.allNumbers = function (arr) { 119 | for (i in arr) { 120 | if (utils.isNumeric(arr[i])) return true; 121 | } 122 | return false; 123 | }; 124 | 125 | Utils.prototype.arrayNext = function (arr, i) { 126 | return arr[++i]; 127 | }; 128 | 129 | Utils.prototype.arrayPrev = function (arr, i) { 130 | return arr[--i]; 131 | }; 132 | 133 | Utils.prototype.closestValueAbove = function(value, arr) { 134 | for (var i = 0; i < arr.length; i++){ 135 | if (arr[i] > value) return arr[i]; 136 | } 137 | return arr[arr.length-1] || 0; 138 | }; 139 | 140 | Utils.prototype.closestValueBelow = function(value, arr) { 141 | for (var i = arr.length-1; i >= 0; i--){ 142 | if (arr[i] < value) return arr[i]; 143 | } 144 | return arr[0] || 0; 145 | }; 146 | 147 | Utils.prototype.zeroPadding = function (num, places) { 148 | var zero = places - num.toString().length + 1; 149 | return Array(+(zero > 0 && zero)).join("0") + num; 150 | }; 151 | 152 | // Remove target character from object properties 153 | // Remove periods that NSTextField adds automatically to inputs in Gui.js (newItem.setStringValue()) 154 | Utils.prototype.objRemovePropertyCharacter = function (obj, removeChar) { 155 | for (var prop in obj) { 156 | obj[prop] = obj[prop].replace(removeChar, ""); 157 | } 158 | } 159 | 160 | // http://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-clone-an-object 161 | Utils.prototype.cloneObj = function (obj) { 162 | var target = {}; 163 | for (var i in obj) { 164 | if (obj.hasOwnProperty(i)) { 165 | target[i] = obj[i]; 166 | } 167 | } 168 | return target; 169 | }; 170 | 171 | // http://stackoverflow.com/questions/979256/sorting-an-array-of-javascript-objects 172 | Utils.prototype.sortBy = function (field, reverse, primer) { 173 | 174 | var key = primer ? 175 | function (x) { 176 | return primer(x[field]) 177 | } : 178 | function (x) { 179 | return x[field] 180 | }; 181 | 182 | reverse = !reverse ? 1 : -1; 183 | 184 | return function (a, b) { 185 | return a = key(a), b = key(b), reverse * ((a > b) - (b > a)); 186 | } 187 | }; 188 | 189 | // http://stackoverflow.com/questions/6913512/how-to-sort-an-array-of-objects-by-multiple-fields 190 | Utils.prototype.sortByMulti = function () { 191 | var fields = [].slice.call(arguments), 192 | n_fields = fields.length; 193 | 194 | return function (A, B) { 195 | var a, b, field, key, primer, reverse, result, i; 196 | 197 | for (i = 0; i < n_fields; i++) { 198 | result = 0; 199 | field = fields[i]; 200 | 201 | key = typeof field === 'string' ? field : field.name; 202 | 203 | a = A[key]; 204 | b = B[key]; 205 | 206 | if (typeof field.primer !== 'undefined') { 207 | a = field.primer(a); 208 | b = field.primer(b); 209 | } 210 | 211 | reverse = (field.reverse) ? -1 : 1; 212 | 213 | if (a < b) result = reverse * -1; 214 | if (a > b) result = reverse * 1; 215 | if (result !== 0) break; 216 | } 217 | return result; 218 | } 219 | }; 220 | 221 | Utils.prototype.searchArrayIndex = function (array, value) { 222 | var arrayLength = array.length; 223 | for (var i = 0; i < arrayLength; i++) { 224 | if (array[i] == value) return i; 225 | } 226 | return null; 227 | }; 228 | 229 | // http://stackoverflow.com/questions/7364150/find-object-by-id-in-array-of-javascript-objects 230 | Utils.prototype.searchObjectArrayIndex = function (array, key, value) { 231 | var arrayLength = array.length; 232 | for (var i = 0; i < arrayLength; i++) { 233 | if (array[i][key] == value) return i; 234 | } 235 | return null; 236 | }; 237 | 238 | // http://stackoverflow.com/questions/5612787/converting-an-object-to-a-string 239 | Utils.prototype.objToString = function (obj, incPropertyName) { 240 | var str = ''; 241 | for (var p in obj) { 242 | if (obj.hasOwnProperty(p)) { 243 | if (incPropertyName) str += p + ':'; 244 | str += obj[p] + ','; 245 | } 246 | } 247 | str = str.slice(0, -1); 248 | return str; 249 | }; 250 | 251 | 252 | // http://stackoverflow.com/questions/8072323/best-way-to-prevent-handle-divide-by-0-in-javascript 253 | Utils.prototype.notZero = function (n) { 254 | n = +n; 255 | if (!n) { 256 | n = 0; 257 | } 258 | return n; 259 | }; 260 | 261 | 262 | // Convert all values to float and round by two decimals 263 | Utils.prototype.objValuesToFloat = function (obj) { 264 | for (var p in obj) { 265 | try { 266 | obj[p] = Math.round(parseFloat(obj[p]) * 100) / 100; 267 | } catch (e) { 268 | log(e); 269 | } 270 | } 271 | }; 272 | 273 | 274 | Utils.prototype.uniqueNumber = function (a) { 275 | return a.sort().filter(function (item, pos, ary) { 276 | return !pos || item != ary[pos - 1]; 277 | }) 278 | }; 279 | 280 | 281 | // http://stackoverflow.com/questions/3971841/how-to-resize-images-proportionally-keeping-the-aspect-ratio 282 | Utils.prototype.getAspectRatio = function (srcWidth, srcHeight, maxWidth, maxHeight) { 283 | var ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight); 284 | return { 285 | width: srcWidth * ratio, 286 | height: srcHeight * ratio 287 | }; 288 | }; 289 | 290 | 291 | Utils.prototype.formatTime = function (ms) { 292 | var days, hours, minutes, seconds, milliseconds; 293 | milliseconds = Math.floor((ms / 10) % 100); 294 | seconds = Math.floor(((ms / 1000) % 60)); 295 | minutes = Math.floor((((ms / 1000) / 60) % 60)); 296 | hours = Math.floor(((((ms / 1000) / 60) / 60) % 24)); 297 | 298 | if (hours < 10) { 299 | hours = "0" + hours; 300 | } 301 | if (minutes < 10) { 302 | minutes = "0" + minutes; 303 | } 304 | if (seconds < 10) { 305 | seconds = "0" + seconds; 306 | } 307 | if (milliseconds < 10) { 308 | milliseconds = "0" + milliseconds; 309 | } 310 | var time = hours + ':' + minutes + ':' + seconds + ':' + milliseconds; 311 | return time; 312 | }; 313 | 314 | 315 | // Based to https://stackoverflow.com/questions/15523514/find-by-key-deep-in-nested-json-object 316 | // Used to find only objects that have specific properties even it's inside of property array 317 | // incSearchIndes: this value will give index numbers where index values target is located 318 | Utils.prototype.findObjByProperty = function (obj, searchProperty, arrayName, incSearchIndex) { 319 | 320 | var arrayName = arrayName || null; 321 | var result = []; 322 | 323 | function getObject(refObj, foundId) { 324 | if (refObj instanceof Array) { 325 | for (var i = 0; i < refObj.length; i++) { 326 | getObject(refObj[i], i); 327 | } 328 | } else { 329 | // Loop every objects 330 | for (var prop in refObj) { 331 | 332 | // Search only from array by names 333 | if (prop == arrayName || arrayName == null) { 334 | 335 | var refObjArr = refObj[prop]; 336 | var refObjArrLength = refObjArr.length; 337 | 338 | for (var i = 0; i < refObjArrLength; i++) { 339 | 340 | for (var propName in refObjArr[i]) { 341 | 342 | if (propName == searchProperty) { 343 | if (incSearchIndex) { 344 | refObjArr[i]["searchId"] = [foundId, i]; 345 | } 346 | result.push(refObjArr[i]); 347 | } 348 | } 349 | } 350 | } 351 | } 352 | } 353 | } 354 | getObject(obj, undefined); 355 | return result; 356 | }; 357 | 358 | 359 | // ---------------------------------------- // 360 | // Debug // 361 | // ---------------------------------------- // 362 | 363 | 364 | Utils.prototype.logObjProperties = function (obj) { 365 | for (var p in obj) { 366 | log(p + ": " + obj[p]); 367 | } 368 | }; 369 | 370 | 371 | Utils.prototype.benchmarkTime = { 372 | startTime: null, 373 | intervalTime: null, 374 | endTime: null, 375 | start: function () { 376 | this.startTime = new Date(); 377 | this.intervalTime = this.startTime; 378 | }, 379 | interval: function () { 380 | var currentTime = null, 381 | soloRenderTime = null, 382 | renderTimePoint = null; 383 | currentTime = new Date(); 384 | soloRenderTime = Math.abs(this.intervalTime - currentTime); 385 | renderTimePoint = Math.abs(this.intervalTime - this.startTime) + soloRenderTime; 386 | this.intervalTime = currentTime; 387 | return [utils.formatTime(soloRenderTime), utils.formatTime(renderTimePoint)]; 388 | }, 389 | stop: function () { 390 | this.endTime = new Date(); 391 | return utils.formatTime(Math.abs(this.endTime - this.startTime)); 392 | } 393 | }; 394 | 395 | 396 | Utils.prototype.benchmarkLoop = { 397 | startTime: null, 398 | endTime: null, 399 | runStatus: false, 400 | start: function (currentIndex, endIndex, logIndexes) { 401 | if (logIndexes) { 402 | var tempTime = new Date(); 403 | log(utils.scriptName + " INDEX: " + currentIndex + " TIME: " + utils.formatTime(Math.abs(tempTime - this.startTime))); 404 | } 405 | if (!this.runStatus) { 406 | this.runStatus = true; 407 | this.startTime = new Date(); 408 | } 409 | if (currentIndex == endIndex - 1) { 410 | this.endTime = new Date(); 411 | log(utils.scriptName + " loop benchmark time: " + utils.formatTime(Math.abs(this.endTime - this.startTime))); 412 | this.reset(); 413 | } 414 | }, 415 | reset: function () { 416 | this.startTime = null; 417 | this.endTime = null; 418 | this.runStatus = false; 419 | } 420 | }; 421 | -------------------------------------------------------------------------------- /AnimateMate.sketchplugin/Contents/Sketch/library/easing.js: -------------------------------------------------------------------------------- 1 | var easing = { 2 | 3 | linearEase: function (currentIteration, startValue, changeInValue, totalIterations) { 4 | return changeInValue * currentIteration / totalIterations + startValue; 5 | }, 6 | 7 | easeInQuad: function (currentIteration, startValue, changeInValue, totalIterations) { 8 | return changeInValue * (currentIteration /= totalIterations) * currentIteration + startValue; 9 | }, 10 | 11 | easeOutQuad: function (currentIteration, startValue, changeInValue, totalIterations) { 12 | return -changeInValue * (currentIteration /= totalIterations) * (currentIteration - 2) + startValue; 13 | }, 14 | 15 | easeInOutQuad: function (currentIteration, startValue, changeInValue, totalIterations) { 16 | if ((currentIteration /= totalIterations / 2) < 1) { 17 | return changeInValue / 2 * currentIteration * currentIteration + startValue; 18 | } 19 | return -changeInValue / 2 * ((--currentIteration) * (currentIteration - 2) - 1) + startValue; 20 | }, 21 | 22 | easeInCubic: function (currentIteration, startValue, changeInValue, totalIterations) { 23 | return changeInValue * Math.pow(currentIteration / totalIterations, 3) + startValue; 24 | }, 25 | 26 | easeOutCubic: function (currentIteration, startValue, changeInValue, totalIterations) { 27 | return changeInValue * (Math.pow(currentIteration / totalIterations - 1, 3) + 1) + startValue; 28 | }, 29 | 30 | easeInOutCubic: function (currentIteration, startValue, changeInValue, totalIterations) { 31 | if ((currentIteration /= totalIterations / 2) < 1) { 32 | return changeInValue / 2 * Math.pow(currentIteration, 3) + startValue; 33 | } 34 | return changeInValue / 2 * (Math.pow(currentIteration - 2, 3) + 2) + startValue; 35 | }, 36 | 37 | easeInQuart: function (currentIteration, startValue, changeInValue, totalIterations) { 38 | return changeInValue * Math.pow(currentIteration / totalIterations, 4) + startValue; 39 | }, 40 | 41 | easeOutQuart: function (currentIteration, startValue, changeInValue, totalIterations) { 42 | return -changeInValue * (Math.pow(currentIteration / totalIterations - 1, 4) - 1) + startValue; 43 | }, 44 | 45 | easeInOutQuart: function (currentIteration, startValue, changeInValue, totalIterations) { 46 | if ((currentIteration /= totalIterations / 2) < 1) { 47 | return changeInValue / 2 * Math.pow(currentIteration, 4) + startValue; 48 | } 49 | return -changeInValue / 2 * (Math.pow(currentIteration - 2, 4) - 2) + startValue; 50 | }, 51 | 52 | easeInQuint: function (currentIteration, startValue, changeInValue, totalIterations) { 53 | return changeInValue * Math.pow(currentIteration / totalIterations, 5) + startValue; 54 | }, 55 | 56 | easeOutQuint: function (currentIteration, startValue, changeInValue, totalIterations) { 57 | return changeInValue * (Math.pow(currentIteration / totalIterations - 1, 5) + 1) + startValue; 58 | }, 59 | 60 | easeInOutQuint: function (currentIteration, startValue, changeInValue, totalIterations) { 61 | if ((currentIteration /= totalIterations / 2) < 1) { 62 | return changeInValue / 2 * Math.pow(currentIteration, 5) + startValue; 63 | } 64 | return changeInValue / 2 * (Math.pow(currentIteration - 2, 5) + 2) + startValue; 65 | }, 66 | 67 | easeInSine: function (currentIteration, startValue, changeInValue, totalIterations) { 68 | return changeInValue * (1 - Math.cos(currentIteration / totalIterations * (Math.PI / 2))) + startValue; 69 | }, 70 | 71 | easeOutSine: function (currentIteration, startValue, changeInValue, totalIterations) { 72 | return changeInValue * Math.sin(currentIteration / totalIterations * (Math.PI / 2)) + startValue; 73 | }, 74 | 75 | easeInOutSine: function (currentIteration, startValue, changeInValue, totalIterations) { 76 | return changeInValue / 2 * (1 - Math.cos(Math.PI * currentIteration / totalIterations)) + startValue; 77 | }, 78 | 79 | easeInExpo: function (currentIteration, startValue, changeInValue, totalIterations) { 80 | return changeInValue * Math.pow(2, 10 * (currentIteration / totalIterations - 1)) + startValue; 81 | }, 82 | 83 | easeOutExpo: function (currentIteration, startValue, changeInValue, totalIterations) { 84 | return changeInValue * (-Math.pow(2, -10 * currentIteration / totalIterations) + 1) + startValue; 85 | }, 86 | 87 | easeInOutExpo: function (currentIteration, startValue, changeInValue, totalIterations) { 88 | if ((currentIteration /= totalIterations / 2) < 1) { 89 | return changeInValue / 2 * Math.pow(2, 10 * (currentIteration - 1)) + startValue; 90 | } 91 | return changeInValue / 2 * (-Math.pow(2, -10 * --currentIteration) + 2) + startValue; 92 | }, 93 | 94 | easeInCirc: function (currentIteration, startValue, changeInValue, totalIterations) { 95 | return changeInValue * (1 - Math.sqrt(1 - (currentIteration /= totalIterations) * currentIteration)) + startValue; 96 | }, 97 | 98 | easeOutCirc: function (currentIteration, startValue, changeInValue, totalIterations) { 99 | return changeInValue * Math.sqrt(1 - (currentIteration = currentIteration / totalIterations - 1) * currentIteration) + startValue; 100 | }, 101 | 102 | easeInOutCirc: function (currentIteration, startValue, changeInValue, totalIterations) { 103 | if ((currentIteration /= totalIterations / 2) < 1) { 104 | return changeInValue / 2 * (1 - Math.sqrt(1 - currentIteration * currentIteration)) + startValue; 105 | } 106 | return changeInValue / 2 * (Math.sqrt(1 - (currentIteration -= 2) * currentIteration) + 1) + startValue; 107 | }, 108 | easeInElastic: function (currentIteration, startValue, changeInValue, totalIterations) { 109 | var s = 1.70158; 110 | var p = 0; 111 | var a = changeInValue; 112 | if (currentIteration == 0) return startValue; 113 | if ((currentIteration /= totalIterations) == 1) return startValue + changeInValue; 114 | if (!p) p = totalIterations * .3; 115 | if (a < Math.abs(changeInValue)) { 116 | a = changeInValue; 117 | var s = p / 4; 118 | } else var s = p / (2 * Math.PI) * Math.asin(changeInValue / a); 119 | return -(a * Math.pow(2, 10 * (currentIteration -= 1)) * Math.sin((currentIteration * totalIterations - s) * (2 * Math.PI) / p)) + startValue; 120 | }, 121 | 122 | easeOutElastic: function (currentIteration, startValue, changeInValue, totalIterations) { 123 | var s = 1.70158; 124 | var p = 0; 125 | var a = changeInValue; 126 | if (currentIteration == 0) return startValue; 127 | if ((currentIteration /= totalIterations) == 1) return startValue + changeInValue; 128 | if (!p) p = totalIterations * .3; 129 | if (a < Math.abs(changeInValue)) { 130 | a = changeInValue; 131 | var s = p / 4; 132 | } else var s = p / (2 * Math.PI) * Math.asin(changeInValue / a); 133 | return a * Math.pow(2, -10 * currentIteration) * Math.sin((currentIteration * totalIterations - s) * (2 * Math.PI) / p) + changeInValue + startValue; 134 | }, 135 | 136 | easeInOutElastic: function (currentIteration, startValue, changeInValue, totalIterations) { 137 | var s = 1.70158; 138 | var p = 0; 139 | var a = changeInValue; 140 | if (currentIteration == 0) return startValue; 141 | if ((currentIteration /= totalIterations / 2) == 2) return startValue + changeInValue; 142 | if (!p) p = totalIterations * (.3 * 1.5); 143 | if (a < Math.abs(changeInValue)) { 144 | a = changeInValue; 145 | var s = p / 4; 146 | } else var s = p / (2 * Math.PI) * Math.asin(changeInValue / a); 147 | if (currentIteration < 1) return -.5 * (a * Math.pow(2, 10 * (currentIteration -= 1)) * Math.sin((currentIteration * totalIterations - s) * (2 * Math.PI) / p)) + startValue; 148 | return a * Math.pow(2, -10 * (currentIteration -= 1)) * Math.sin((currentIteration * totalIterations - s) * (2 * Math.PI) / p) * .5 + changeInValue + startValue; 149 | }, 150 | 151 | easeInBack: function (currentIteration, startValue, changeInValue, totalIterations, s) { 152 | if (s == undefined) s = 1.70158; 153 | return changeInValue * (currentIteration /= totalIterations) * currentIteration * ((s + 1) * currentIteration - s) + startValue; 154 | }, 155 | 156 | easeOutBack: function (currentIteration, startValue, changeInValue, totalIterations, s) { 157 | if (s == undefined) s = 1.70158; 158 | return changeInValue * ((currentIteration = currentIteration / totalIterations - 1) * currentIteration * ((s + 1) * currentIteration + s) + 1) + startValue; 159 | }, 160 | 161 | easeInOutBack: function (currentIteration, startValue, changeInValue, totalIterations, s) { 162 | if (s == undefined) s = 1.70158; 163 | if ((currentIteration /= totalIterations / 2) < 1) return changeInValue / 2 * (currentIteration * currentIteration * (((s *= (1.525)) + 1) * currentIteration - s)) + startValue; 164 | return changeInValue / 2 * ((currentIteration -= 2) * currentIteration * (((s *= (1.525)) + 1) * currentIteration + s) + 2) + startValue; 165 | }, 166 | 167 | easeInBounce: function (currentIteration, startValue, changeInValue, totalIterations) { 168 | return changeInValue - easing.easeOutBounce(totalIterations - currentIteration, 0, changeInValue, totalIterations) + startValue; 169 | }, 170 | 171 | easeOutBounce: function (currentIteration, startValue, changeInValue, totalIterations) { 172 | if ((currentIteration /= totalIterations) < (1 / 2.75)) { 173 | return changeInValue * (7.5625 * currentIteration * currentIteration) + startValue; 174 | } else if (currentIteration < (2 / 2.75)) { 175 | return changeInValue * (7.5625 * (currentIteration -= (1.5 / 2.75)) * currentIteration + .75) + startValue; 176 | } else if (currentIteration < (2.5 / 2.75)) { 177 | return changeInValue * (7.5625 * (currentIteration -= (2.25 / 2.75)) * currentIteration + .9375) + startValue; 178 | } else { 179 | return changeInValue * (7.5625 * (currentIteration -= (2.625 / 2.75)) * currentIteration + .984375) + startValue; 180 | } 181 | }, 182 | 183 | easeInOutBounce: function (currentIteration, startValue, changeInValue, totalIterations) { 184 | if (currentIteration < totalIterations / 2) return easing.easeInBounce(currentIteration * 2, 0, changeInValue, totalIterations) * .5 + startValue; 185 | return easing.easeOutBounce(currentIteration * 2 - totalIterations, 0, changeInValue, totalIterations) * .5 + changeInValue * .5 + startValue; 186 | } 187 | }; 188 | 189 | // Returns all available easings 190 | easing.getEasingNames = function () { 191 | var returnArr = []; 192 | for (var property in easing) { 193 | if (easing.hasOwnProperty(property)) { 194 | returnArr.push(property) 195 | } 196 | } 197 | // Remove this "getEasingNames" and "getEasingValue" properties from list 198 | returnArr.pop(); 199 | returnArr.pop(); 200 | 201 | return returnArr; 202 | }; 203 | 204 | // Shortcut to evaluate to get easing value 205 | easing.getEasingValue = function (currentValue, nextValue, difference, indexValue, easingType, roundValue) { 206 | var roundValue = roundValue || true; 207 | var returnValue = currentValue == nextValue ? currentValue : eval('easing.' + easingType + '(' + indexValue + ',' + currentValue + ',' + (nextValue - currentValue) + ',' + difference + ')'); 208 | return roundValue ? Math.round(returnValue * 100) / 100) : returnValue; 209 | }; 210 | 211 | /* 212 | * 213 | * TERMS OF USE - EASING EQUATIONS 214 | * 215 | * Open source under the BSD License. 216 | * 217 | * Copyright © 2001 Robert Penner 218 | * All rights reserved. 219 | * 220 | * Redistribution and use in source and binary forms, with or without modification, 221 | * are permitted provided that the following conditions are met: 222 | * 223 | * Redistributions of source code must retain the above copyright notice, this list of 224 | * conditions and the following disclaimer. 225 | * Redistributions in binary form must reproduce the above copyright notice, this list 226 | * of conditions and the following disclaimer in the documentation and/or other materials 227 | * provided with the distribution. 228 | * 229 | * Neither the name of the author nor the names of contributors may be used to endorse 230 | * or promote products derived from this software without specific prior written permission. 231 | * 232 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 233 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 234 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 235 | * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 236 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 237 | * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 238 | * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 239 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 240 | * OF THE POSSIBILITY OF SUCH DAMAGE. 241 | * 242 | */ -------------------------------------------------------------------------------- /AnimateMate.sketchplugin/Contents/Sketch/library/gifsicle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Creatide/AnimateMate/75c3e8a1ec18d96ce097d98de228b2775827ef71/AnimateMate.sketchplugin/Contents/Sketch/library/gifsicle -------------------------------------------------------------------------------- /AnimateMate.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AnimateMate", 3 | "description": "Create your animations directly in Sketch using AnimateMate.", 4 | "author": "Creatide", 5 | "authorEmail": "hello@creatide.com", 6 | "homepage": "http://github.com/Creatide/AnimateMate", 7 | "version": "0.1.9", 8 | "identifier": "com.creatide.sketch.animatemate", 9 | "compatibleVersion": "3.5.2", 10 | "bundleVersion": 1, 11 | "commands": [ 12 | { 13 | "name": "Export Animation", 14 | "identifier": "export", 15 | "script": "commands.js", 16 | "handler": "exportAnimation", 17 | "shortcut": "ctrl option cmd a" 18 | }, 19 | { 20 | "name": "Create Animation", 21 | "identifier": "keyframe", 22 | "script": "commands.js", 23 | "handler": "createAnimation", 24 | "shortcut": "ctrl option cmd k" 25 | }, 26 | { 27 | "name": "Edit Animation", 28 | "identifier": "edit", 29 | "script": "commands.js", 30 | "handler": "editAnimation", 31 | "shortcut": "ctrl option cmd l" 32 | }, 33 | { 34 | "name": "Offset Animation", 35 | "identifier": "offset", 36 | "script": "commands.js", 37 | "handler": "offsetAnimation", 38 | "shortcut": "ctrl option cmd o" 39 | }, 40 | { 41 | "name": "Random Animation", 42 | "identifier": "random", 43 | "script": "commands.js", 44 | "handler": "randomAnimation", 45 | "shortcut": "ctrl option cmd g" 46 | }, 47 | { 48 | "name": "Delete Animation", 49 | "identifier": "remove", 50 | "script": "commands.js", 51 | "handler": "removeAnimation", 52 | "shortcut": "ctrl option cmd d" 53 | }, 54 | { 55 | "name": "Return Keyframe", 56 | "identifier": "return", 57 | "script": "commands.js", 58 | "handler": "returnKeyframe", 59 | "shortcut": "ctrl option cmd r" 60 | }, 61 | { 62 | "name": "Next Keyframe", 63 | "identifier": "next", 64 | "script": "commands.js", 65 | "handler": "nextKeyframe", 66 | "shortcut": "ctrl option cmd ." 67 | }, 68 | { 69 | "name": "Previous Keyframe", 70 | "identifier": "previous", 71 | "script": "commands.js", 72 | "handler": "previousKeyframe", 73 | "shortcut": "ctrl option cmd ," 74 | }, 75 | { 76 | "name": "Update Keyframe", 77 | "identifier": "update", 78 | "script": "commands.js", 79 | "handler": "updateKeyframeValues", 80 | "shortcut": "ctrl option cmd /" 81 | }, 82 | { 83 | "name": "Reverse Keyframes", 84 | "identifier": "reverse", 85 | "script": "commands.js", 86 | "handler": "reverseKeyframes", 87 | "shortcut": "ctrl option cmd b" 88 | } 89 | ], 90 | "menu": { 91 | "title": "AnimateMate", 92 | "items": [ 93 | "keyframe", 94 | "offset", 95 | "random", 96 | "edit", 97 | "remove", 98 | "return", 99 | "reverse", 100 | "export", 101 | "next", 102 | "previous", 103 | "update" 104 | ] 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Creatide 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AnimateMate Plugin For Sketch 2 | ============================= 3 | 4 | ```diff 5 | - AnimateMate project is deprecated 6 | - Unfortunately I don't have time to update the AnimateMate plugin anymore. 7 | - If you create updates, please do send me pull requests and I'll update it to the master. 8 | ``` 9 | 10 | #### "Create your animations directly in Sketch using AnimateMate." 11 | 12 | ![AnimateMate - Logo Animation](https://github.com/Creatide/AnimateMate/blob/gh-pages/img/readme/AnimateMate_LogoAnimation_GitHub.gif) 13 | 14 |

15 | 16 | ##### *Haha, is this joke or what?! Why the heck do I need animation tools in Sketch.* 17 | 18 | You know feeling when you just need a simple animation for your awesome concept and you realize that it's a huge process to move all assets into some other application? That’s a lot of hassle! Like this example. 19 | 20 |
21 | 22 | ![AnimateMate - Slider Example](https://github.com/Creatide/AnimateMate/blob/gh-pages/img/readme/AnimateMate_Example_Slider_0-135.gif) 23 | 24 | ##### *I Know the Feeling!* 25 | 26 | That's why I created AnimateMate in order to produce and export simple animations straight out of Sketch. It's not exactly rocket science, but it can lighten your workflow in some cases. 27 | So let the game begin! 28 | 29 | ![AnimateMate - Game Example](https://github.com/Creatide/AnimateMate/blob/gh-pages/img/readme/AnimateMate_Example_PingPong_0-100.gif) 30 | 31 | 32 | ## Install 33 | 34 | Copy **`AnimateMate.sketchplugin`** to **Sketch** plugins folder. 35 | 36 | > Easiest way to go into your Sketch plugin folder:
37 | **Plugins > Manage Plugins > Reveal Plugins Folder** 38 | 39 | 40 | ## How to Use 41 | 42 | There are tons of ways to use **AnimateMate** and I'll try to explain basics in this video. 43 | 44 | AnimateMate Basics 47 | 48 | You could run into a situation when you're wondering why it's working this or that way... This plugin does not convert your **Sketch** to a fully featured animation software :) However, it'll help you in many simple cases, but if you need more complex animations you should consider some other animation tools. **AnimateMate** was made for creating simple animations at least for now, we'll see how it'll develop in the future... 49 | 50 | #### Shortcuts 51 | 52 | Command | Shortcut | Description 53 | :------------------- | :------------------------------------ | :---------------------------------------------------- 54 | **Create Animation** | *`ctrl` + `option` + `cmd` +* **`K`** | Create new animation / keyframe to layer. 55 | **Offset Animation** | *`ctrl` + `option` + `cmd` +* **`O`** | Offset animated and/or keyframe values. 56 | **Random Animation** | *`ctrl` + `option` + `cmd` +* **`G`** | Generate random animation to selected layers. 57 | **Edit Animation** | *`ctrl` + `option` + `cmd` +* **`L`** | Edit layer animation values in ordered text view. 58 | **Delete Animation** | *`ctrl` + `option` + `cmd` +* **`D`** | Delete animation from selected layer(s). 59 | **Return Keyframe** | *`ctrl` + `option` + `cmd` +* **`R`** | Return selected keyframe values to layer(s). 60 | **Export Animation** | *`ctrl` + `option` + `cmd` +* **`A`** | Export your animation to PNG or GIF format. 61 | **Reverse Keyframes** | *`ctrl` + `option` + `cmd` +* **`B`** | Reverse keyframes in single or multiple animations. 62 | 63 | 64 | ## Examples 65 | 66 | Here are a couple examples that are made using **AnimateMate** plugin in **Sketch**. You'll find more info and examples in the [AnimateMate.com](http://animatemate.com) web pages. 67 | 68 | ![Example 1](https://github.com/Creatide/AnimateMate/blob/gh-pages/img/readme/AnimateMate_Example_001.gif) 69 | 70 | ![Example 2](https://github.com/Creatide/AnimateMate/blob/gh-pages/img/readme/AnimateMate_Example_002.gif) 71 | 72 | ![Example 3](https://github.com/Creatide/AnimateMate/blob/gh-pages/img/readme/AnimateMate_Example_003_RandomAnimation.gif) 73 | 74 | ![Example 4](https://github.com/Creatide/AnimateMate/blob/gh-pages/img/readme/AnimateMate_Example_004_RandomAnimation.gif) 75 | 76 | ![Example 5](https://github.com/Creatide/AnimateMate/blob/gh-pages/img/readme/AnimateMate_Example_005_MaskAnimation.gif) 77 | 78 | ![Example 6](https://github.com/Creatide/AnimateMate/blob/gh-pages/img/readme/AnimateMate_Example_006_Over2kItemCountRenderTest.gif) 79 | 80 | ![Example 7](https://github.com/Creatide/AnimateMate/blob/gh-pages/img/readme/AnimateMate_Example_007_ManyItemsCountRenderTest.gif) 81 | 82 | ![Example 8](https://github.com/Creatide/AnimateMate/blob/gh-pages/img/readme/AnimateMate_Example_008_LoadingIndicators.gif) 83 | 84 | ![Example 9](https://github.com/Creatide/AnimateMate/blob/gh-pages/img/readme/AnimateMate_Example_CassetteDemo.gif) 85 | 86 | 87 | ## Known Bugs to Fix 88 | 89 | * [ ] Export scaling option doesn't work after Sketch 41 version. 90 | 91 | ## Roadmap & Feature Ideas 92 | 93 | * [x] Remember values in some input fields in dialogs 94 | * [ ] Visual timeline editor 95 | * [ ] Improve groups workflow 96 | * [ ] Text animations 97 | * [ ] Randomize exist keyframe values 98 | * [ ] Select a layer by keyframe numbers 99 | * [ ] Anchor helper object into the workflow 100 | * [x] Reverse keyframes 101 | * [ ] Spread keyframes to a given time 102 | * [ ] Multiple easing types in animations 103 | * [x] More options to GIF exporting 104 | 105 | 106 | ## Colloboration & Feedback 107 | 108 | If you fork AnimateMate and create some useful updates, please do send me pull requests so I can include your work to AnimateMate and give you credit here! 109 | 110 | There is so much to do in order to make it better! Collaboration is very welcome and all feedback is greatly appreciated [hello@creatide.com](mailto:hello@creatide.com) 111 | 112 | > I'm a Visual Designer and a hobbyist coder. So if you're a more experienced with code, I know we can make it even better :) 113 | 114 | 115 | ## About 116 | 117 | I think **Sketch** is super useful in many design tasks. But while using Sketch and as well many other graphic design softwares I was always longing for the basic level of animation tools. Usually it's only the basic move-, rotation-, scale- or transparency animations that needed to visualise something simple. 118 | 119 | I think most of the graphical design sofwares should include at least some very basic level of animation tools. Like in Photoshop I've nowadays found many good uses for animation tools. Too often it would be hard work to transfer all assets to other software just for creating simple animations. Still, it doesn't need to be such a complex system... 120 | 121 | ### Author 122 | 123 | **AnimateMate** made by [Creatide](http://creatide.com) *([Sakari Niittymaa](http://sakari.niittymaa.com))* 124 | 125 | > Creatide is not a company nor a big factory. It's a single Designer who also loves coding and improving workflow. I create tools for my personal usage in order to relieve my design process, so every tool is used by myself. Some of those tools end up being released to the public under this name and domain. 126 | 127 | > Working as a designer makes your come up with different ideas on how your workflow could be improved. My approach: when you got an idea for a specific tool, create it, then you have it. That's what Creatide is all about. 128 | 129 | ### Contributors 130 | 131 | Thank you all for your contributions. Special thanks to these people for writing code: 132 | 133 | [@headlessme](https://github.com/headlessme) 134 | 135 | ### Licence 136 | 137 | The MIT License (MIT) 138 | Copyright (c) 2017 [Creatide](http://creatide.com) *([Sakari Niittymaa](http://sakari.niittymaa.com))* 139 | 140 | ### Used Libraries & Tools 141 | 142 | - [Gifsicle](https://github.com/kohler/gifsicle) 143 | - [Robert Penner's Easing Functions](http://robertpenner.com/easing/) 144 | -------------------------------------------------------------------------------- /sketchpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AnimateMate", 3 | "description": "Create your animations directly in Sketch using AnimateMate.", 4 | "tags": ["animatemate", "animation", "animate", "mate", "image", "export", "sequence", "animated", "png", "gif", "motion", "keyframe", "video"] 5 | } --------------------------------------------------------------------------------