├── 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 | 
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 | 
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 | 
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 |
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 | 
69 |
70 | 
71 |
72 | 
73 |
74 | 
75 |
76 | 
77 |
78 | 
79 |
80 | 
81 |
82 | 
83 |
84 | 
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 | }
--------------------------------------------------------------------------------