├── .gitignore ├── docs ├── preview-arrange.gif └── preview-rotation.gif ├── Skatter.sketchplugin └── Contents │ └── Sketch │ ├── shared.js │ ├── random-rotate.js │ ├── manifest.json │ └── arrange.js ├── LICENSE ├── skatter-appcast.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | 4 | *.zip 5 | -------------------------------------------------------------------------------- /docs/preview-arrange.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshdjuric/Skatter/HEAD/docs/preview-arrange.gif -------------------------------------------------------------------------------- /docs/preview-rotation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshdjuric/Skatter/HEAD/docs/preview-rotation.gif -------------------------------------------------------------------------------- /Skatter.sketchplugin/Contents/Sketch/shared.js: -------------------------------------------------------------------------------- 1 | function randomRotate(layer) { 2 | layer.setRotation(Math.random() * 360); 3 | } 4 | 5 | function removeItems(_items) { 6 | for (var i = 0; i < _items.count(); i++) { 7 | _items[i].remove(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Skatter.sketchplugin/Contents/Sketch/random-rotate.js: -------------------------------------------------------------------------------- 1 | @import 'shared.js' 2 | 3 | function onRun(context) { 4 | 5 | var doc = context.document; 6 | var selection = context.selection 7 | var sketch = context.api() 8 | 9 | if (selection.count() > 0) { 10 | for (var i = 0; i < selection.count(); i++) { 11 | randomRotate(selection.objectAtIndex(i)) 12 | } 13 | } 14 | else { 15 | doc.showMessage("Hey stoops! You need to select something first."); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /Skatter.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Skatter", 3 | "identifier" : "com.sketchapp.Skatter", 4 | "version" : "1.0.4", 5 | "description" : "Attempting to build a plugin with a set of commands to enable shape scattering. Currently just rootates randomly... shortcut key included 💥", 6 | "authorEmail" : "joshdjuric@gmail.com", 7 | "author" : "Josh Djuric", 8 | "appcast" : "https://raw.githubusercontent.com/joshdjuric/Skatter/master/skatter-appcast.xml", 9 | "commands" : [ 10 | { 11 | "script" : "random-rotate.js", 12 | "handler" : "onRun", 13 | "shortcut" : "alt cmd R", 14 | "name" : "Random Rotate", 15 | "identifier" : "randomrotate" 16 | }, 17 | { 18 | "script" : "arrange.js", 19 | "handler" : "onRun", 20 | "shortcut" : "alt cmd A", 21 | "name" : "Arrange", 22 | "identifier" : "arrange" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Josh Djuric 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 | -------------------------------------------------------------------------------- /skatter-appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Skatter: A Sketch Plugin 5 | https://raw.githubusercontent.com/joshdjuric/Skatter/master/skatter-appcast.xml 6 | Create randomised patterns out of pre-determined shapes. 7 | en 8 | 9 | Version 1.0.3 10 | 11 | 13 |
  • Random size entropy now available!
  • 14 | 15 | ]]> 16 |
    17 | 18 |
    19 | 20 | Version 1.0.4 21 | 22 | 24 |
  • Added support for Sketch 45 app updates
  • 25 | 26 | ]]> 27 |
    28 | 29 |
    30 |
    31 |
    32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Skatter 2 | Currently a work in progress. I'd like Skatter to be a plugin with tools to assist with creating randomised patterns out of pre-determined shapes. 3 | 4 | ## Arrange skatter 5 | Select the layers you'd like to Skatter and it will fill the artboard. 6 | 7 | Shortcut key -> alt cmd A :tada: 8 | 9 | Todo: 10 | - Better artboard support 11 | - Add UI to adjust entropy, spacing, rotation and opacity handles 12 | - What would be sweet is if there could be a Skatter object which retained the handle settings that you could tweak after the initial skatter; might need some smarts to help with that one. 13 | 14 | Eventually, I'd like to implement some kind of dialog to adjust certain variables. Perhaps even extend the group class to allow adjusting handles after skattering. For the meantime; you can hack the plugin and edit the entropy and spaceScale variables for extra space and random positioning. 15 | 16 | ![PreviewArrange](https://raw.githubusercontent.com/joshdjuric/Skatter/master/docs/preview-arrange.gif "Preview arrange") 17 | 18 | ## Random rotate 19 | Select your layers and rotate them... randomly. 20 | 21 | Shortcut key -> alt cmd R :boom: 22 | 23 | Todo: 24 | - add entropy handle 25 | 26 | ![PreviewRotation](https://raw.githubusercontent.com/joshdjuric/Skatter/master/docs/preview-rotation.gif "Preview rotation") 27 | 28 | ## Beware 29 | This is currently kinda shit and largely un-tested. Seems to work with groups, text layers, shapes. However, many apologies if it crashes Sketch for you. 30 | -------------------------------------------------------------------------------- /Skatter.sketchplugin/Contents/Sketch/arrange.js: -------------------------------------------------------------------------------- 1 | // 2 | // This is no way near finished 3 | // 4 | // todo: 5 | // figure out cocoa so a dialog to manage randomness and entropy can be developed 6 | 7 | @import 'shared.js' 8 | 9 | // Base dimensions (if no artboard or parent group present) 10 | var _w = 1600 11 | var _h = 640 12 | 13 | // This will be the longest dimension of the selected layers 14 | var maxDimension = 0 15 | 16 | // Handles 17 | var positionEntropy = 0 18 | var spaceScale = 1 // NEVER SET THIS TO ZERO todo: error handling 19 | var sizeEntropy = 0 20 | 21 | // Layers and groups 22 | var selection = [] 23 | var layers = [] 24 | var container = null 25 | 26 | function onRun(context) { 27 | var sketch = context.api() 28 | var doc = context.document 29 | 30 | // Get selection 31 | selection = sketch.selectedDocument.selectedLayers 32 | layers = selection.nativeLayers 33 | 34 | // Ensure there is anything selected in the first place 35 | if (layers.count() == 0) { 36 | doc.showMessage('Hey stoops! You need to select some layers to arrange. Using zero instead.') 37 | return; 38 | } 39 | 40 | // Find a parent container (this could be smarter; at the moment it just gets the last found container) 41 | selection.iterate(function(layer){ 42 | if (layer.container.isArtboard || 43 | layer.container.isGroup) { 44 | container = layer.container 45 | } 46 | }) 47 | 48 | // Ask for scale handle value 49 | spaceScale = parseInt([doc askForUserInput:"Extra space scale %" initialValue:"0"]) 50 | 51 | // Make sure scale is set correctly 52 | if (isNaN(spaceScale)) { 53 | doc.showMessage('Hey stoops! Space scale needs to be a number. Using 1 instead.') 54 | spaceScale = 1 55 | } 56 | else { 57 | spaceScale = 1 + spaceScale / 100 58 | } 59 | 60 | // Ask for entropy handle value 61 | positionEntropy = parseInt([doc askForUserInput:"Entropy % (the larger the number the more random the Skatter)" initialValue:"0"]) 62 | 63 | // Make sure entropy is set correctly 64 | if (isNaN(positionEntropy)) { 65 | doc.showMessage('Hey stoops! Entropy needs to be a number.') 66 | positionEntropy = 0; 67 | } 68 | 69 | // Ask for size entropy handle value 70 | sizeEntropy = parseInt([doc askForUserInput:"Size entropy % (the larger the number the more random the sizes)" initialValue:"0"]) 71 | 72 | // Make sure entropy is set correctly 73 | if (isNaN(sizeEntropy)) { 74 | doc.showMessage('Hey stoops! Size entropy needs to be a number.') 75 | sizeEntropy = 0; 76 | } 77 | 78 | // Determine maxDimension based on largest dimension of selected layers 79 | for (var i = 0; i < layers.count(); i++) { 80 | maxDimension = Math.ceil(Math.max(maxDimension, Math.max(layers[i].frame().width(), layers[i].frame().height()))) 81 | parent = layers 82 | } 83 | 84 | // Adjust maxDimension by specified scale handle 85 | maxDimension *= spaceScale 86 | 87 | // We're ready to Skatter 88 | skatter() 89 | 90 | // Remove originals 91 | selection.iterate(function(layer){ 92 | layer.remove() 93 | }) 94 | } 95 | 96 | function skatter() { 97 | 98 | // I'd like the skatterings to a new group eventually 99 | var group = null 100 | 101 | // This variable is for ensuring limited consectutive skatterings 102 | var prevLayerName = '' 103 | 104 | // Determine dimensions to skatter in 105 | if (container != null) { 106 | _w = container.sketchObject.frame().width(); 107 | _h = container.sketchObject.frame().height(); 108 | } 109 | 110 | // Determine grid 111 | // Using ceil roughly uses max dimension but divides the space equally 112 | var cols = Math.ceil(_w / maxDimension) 113 | var rows = Math.ceil(_h / maxDimension) 114 | 115 | // Loop vars 116 | var rowCount = 0 117 | var colCount = 0 118 | 119 | // The proper unit space 120 | var unitX = _w / cols 121 | var unitY = _h / rows 122 | 123 | // Number of units required to fill grid 124 | var units = (cols + 1) * (rows + 1) 125 | var item = getRandomLayer(); 126 | 127 | // Let's skatter 128 | for (var i = 0; i < units; i++) { 129 | // Limit consectutive skatterings (if more than one item in selection) 130 | // This only works horizontally for now 131 | if (layers.count() > 1) { 132 | while(item.name() == prevLayerName) { 133 | item = getRandomLayer() 134 | } 135 | } 136 | 137 | prevLayerName = item.name() 138 | 139 | // Duplicate randomly selected item 140 | var dupe = item.duplicate() 141 | 142 | // Update name 143 | dupe.name = 'col: ' + colCount + ', row:' + rowCount 144 | 145 | // Apply size entropy 146 | var scaleWithEntropy = getSizeEntropyScale() 147 | dupe.frame().width = scaleWithEntropy * dupe.frame().width() 148 | dupe.frame().height = scaleWithEntropy * dupe.frame().height() 149 | 150 | // Determine position of new duplicate and apply any entropy 151 | var posX = applyPositionEntropy(colCount * unitX) 152 | var posY = applyPositionEntropy(rowCount * unitY) 153 | 154 | // Apply position 155 | dupe.frame().setX(posX - dupe.frame().width() / 2) 156 | dupe.frame().setY(posY - dupe.frame().height() / 2) 157 | 158 | // Apply rotation 159 | // This should be adjusted by a handle in future 160 | randomRotate(dupe) 161 | 162 | // Row/column counter 163 | if (colCount == cols) { 164 | colCount = 0 165 | rowCount++; 166 | } 167 | else { 168 | colCount++ 169 | } 170 | } 171 | } 172 | 173 | // Entropy algorithm 174 | function applyPositionEntropy(value) { 175 | return value + Math.floor(Math.random() * (Math.round(Math.random())?-positionEntropy:positionEntropy)) 176 | } 177 | 178 | function getSizeEntropyScale() { 179 | var rand = (sizeEntropy / 100) * Math.random() 180 | return (1 + (Math.round(Math.random())?-rand:rand)) 181 | } 182 | 183 | // Get next layer to duplicate 184 | // Managing consectutive skatterings should probably happen here in future 185 | function getRandomLayer() { 186 | return layers[Math.floor(Math.random() * layers.length)] 187 | } 188 | --------------------------------------------------------------------------------