├── README.md ├── appcast.xml ├── push-and-shove.sketchplugin └── Contents │ └── Sketch │ ├── manifest.json │ └── script.cocoascript └── readme-assets ├── demo1.gif ├── demo2.gif └── demo3.gif /README.md: -------------------------------------------------------------------------------- 1 | # Push and Shove 2 | A tiny Sketch plugin for manipulating layers in various evil ways: 3 | 4 | - Resize layers backwards (from the bottom right edge) by just adding ```^``` to your combo: ```^ ⌘ →``` (ctrl + cmd + any arrow key) and ```^ ⇧ ⌘ →``` for 10px (ctrl + shift + cmd + any arrow key) 5 | 6 | Like this 7 | 8 | ![Resize layers backwards](readme-assets/demo1.gif) 9 | 10 | - Move layers half a pixel in any direction with ```^ ⎇ →``` (ctrl + alt + any arrow key) 11 | - Resize layers half a pixel in any direction with ```^ ⇧ ⎇ →``` (ctrl + shift + alt + any arrow key) 12 | 13 | And this 14 | 15 | ![Move layers half a pixel to align odd sized shapes](readme-assets/demo2.gif) 16 | 17 | - Align layers horizontally and vertically accuretly (similar to "Pixel Fitting" setting) with ```⌘ [``` and ```⌘ ]``` (cmd + bracket keys). 18 | - Align layers horizontally and vertically to artboard with ```⌘ ⎇ [``` and ```⌘ ⎇ ]``` (cmd + alt + bracket keys). 19 | 20 | Or maybe this 21 | 22 | ![Align layers accuretly](readme-assets/demo3.gif) 23 | 24 | Also: 25 | 26 | - Toggle the proportions constraint of layers with ```^ ⌘ c``` (ctrl + cmd + C). 27 | - Replace a layer with one(s) you've just copied with ```^ ⇧ ⌘ v``` (ctrl + shift + cmd + V). 28 | 29 | ## How to Install 30 | 1. Download and open ```push-and-shove-master.zip``` 31 | 2. Open ```push-and-shove.sketchplugin``` (Sketch will automatically install the plugin) 32 | 33 | ## Notes 34 | * Tested on Sketch 3.8.3 35 | * Toggle proportions constraint works with any layers except for text and slices 36 | * Since Sketch 3.7, canvas somtimes flashes after performing one of the plugin actions, haven't figured out yet how to fix this :) -------------------------------------------------------------------------------- /appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Push and Shove 5 | https://raw.githubusercontent.com/ozzik/push-and-shove/master/appcast.xml 6 | 7 | en 8 | 9 | Version 1.2.2 10 | 11 | 13 |
  • Minor update v1.2.2
  • 14 | 15 | ]]> 16 |
    17 | 18 |
    19 |
    20 |
    -------------------------------------------------------------------------------- /push-and-shove.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author" : "Oz Pinhas", 3 | "commands" : [ 4 | { 5 | "script" : "script.cocoascript", 6 | "handler" : "resize_invert_up", 7 | "shortcut" : "ctrl cmd ↑", 8 | "name" : "Increase Height Backwards", 9 | "identifier" : "resize_invert_up" 10 | }, 11 | { 12 | "script" : "script.cocoascript", 13 | "handler" : "resize_invert_up_10", 14 | "shortcut" : "ctrl cmd shift ↑", 15 | "name" : "Increase Height 10px Backwards", 16 | "identifier" : "resize_invert_up_10" 17 | }, 18 | { 19 | "script" : "script.cocoascript", 20 | "handler" : "resize_invert_right", 21 | "shortcut" : "ctrl cmd →", 22 | "name" : "Increase Width Backwards", 23 | "identifier" : "resize_invert_right" 24 | }, 25 | { 26 | "script" : "script.cocoascript", 27 | "handler" : "resize_invert_right_10", 28 | "shortcut" : "ctrl cmd shift →", 29 | "name" : "Increase Width 10px Backwards", 30 | "identifier" : "resize_invert_right_10" 31 | }, 32 | { 33 | "script" : "script.cocoascript", 34 | "handler" : "resize_invert_down", 35 | "shortcut" : "ctrl cmd ↓", 36 | "name" : "Decrease Height Backwards", 37 | "identifier" : "resize_invert_down" 38 | }, 39 | { 40 | "script" : "script.cocoascript", 41 | "handler" : "resize_invert_down_10", 42 | "shortcut" : "ctrl cmd shift ↓", 43 | "name" : "Decrease Height 10px Backwards", 44 | "identifier" : "resize_invert_down_10" 45 | }, 46 | { 47 | "script" : "script.cocoascript", 48 | "handler" : "resize_invert_left", 49 | "shortcut" : "ctrl cmd ←", 50 | "name" : "Decrease Width Backwards", 51 | "identifier" : "resize_invert_left" 52 | }, 53 | { 54 | "script" : "script.cocoascript", 55 | "handler" : "resize_invert_left_10", 56 | "shortcut" : "ctrl cmd shift ←", 57 | "name" : "Decrease Width 10px Backwards", 58 | "identifier" : "resize_invert_left_10" 59 | }, 60 | { 61 | "script" : "script.cocoascript", 62 | "handler" : "move_halfway_up", 63 | "shortcut" : "ctrl alt ↑", 64 | "name" : "Move Halfway Up", 65 | "identifier" : "move_halfway_up" 66 | }, 67 | { 68 | "script" : "script.cocoascript", 69 | "handler" : "move_halfway_right", 70 | "shortcut" : "ctrl alt →", 71 | "name" : "Move Halfway Right", 72 | "identifier" : "move_halfway_right" 73 | }, 74 | { 75 | "script" : "script.cocoascript", 76 | "handler" : "move_halfway_down", 77 | "shortcut" : "ctrl alt ↓", 78 | "name" : "Move Halfway Down", 79 | "identifier" : "move_halfway_down" 80 | }, 81 | { 82 | "script" : "script.cocoascript", 83 | "handler" : "move_halfway_left", 84 | "shortcut" : "ctrl alt ←", 85 | "name" : "Move Halfway Left", 86 | "identifier" : "move_halfway_left" 87 | }, 88 | { 89 | "script" : "script.cocoascript", 90 | "handler" : "resize_halfway_up", 91 | "shortcut" : "ctrl shift alt ↑", 92 | "name" : "Resize Halfway Up", 93 | "identifier" : "resize_halfway_up" 94 | }, 95 | { 96 | "script" : "script.cocoascript", 97 | "handler" : "resize_halfway_right", 98 | "shortcut" : "ctrl shift alt →", 99 | "name" : "Resize Halfway Right", 100 | "identifier" : "resize_halfway_right" 101 | }, 102 | { 103 | "script" : "script.cocoascript", 104 | "handler" : "resize_halfway_down", 105 | "shortcut" : "ctrl shift alt ↓", 106 | "name" : "Resize Halfway Down", 107 | "identifier" : "resize_halfway_down" 108 | }, 109 | { 110 | "script" : "script.cocoascript", 111 | "handler" : "resize_halfway_left", 112 | "shortcut" : "ctrl shift alt ←", 113 | "name" : "Resize Halfway Left", 114 | "identifier" : "resize_halfway_left" 115 | }, 116 | { 117 | "script" : "script.cocoascript", 118 | "handler" : "align_horizontally", 119 | "shortcut" : "cmd ]", 120 | "name" : "Align Horizontally", 121 | "identifier" : "align_horizontally" 122 | }, 123 | { 124 | "script" : "script.cocoascript", 125 | "handler" : "align_horizontally_to_artboard", 126 | "shortcut" : "cmd alt ]", 127 | "name" : "Align Horizontally to Artboard", 128 | "identifier" : "align_horizontally_to_artboard" 129 | }, 130 | { 131 | "script" : "script.cocoascript", 132 | "handler" : "align_vertically", 133 | "shortcut" : "cmd [", 134 | "name" : "Align Vertically", 135 | "identifier" : "align_vertically" 136 | }, 137 | { 138 | "script" : "script.cocoascript", 139 | "handler" : "align_vertically_to_artboard", 140 | "shortcut" : "cmd alt [", 141 | "name" : "Align Vertically to Artboard", 142 | "identifier" : "align_vertically_to_artboard" 143 | }, 144 | { 145 | "script" : "script.cocoascript", 146 | "handler" : "dothetoggle", 147 | "shortcut" : "cmd ctrl c", 148 | "name" : "Toggle Proportions", 149 | "identifier" : "toggleproportions" 150 | }, 151 | { 152 | "script" : "script.cocoascript", 153 | "handler" : "pasteAndReplace", 154 | "shortcut" : "cmd ctrl shift v", 155 | "name" : "Paste and Replace", 156 | "identifier" : "pasteandreplace" 157 | }, 158 | { 159 | "script" : "script.cocoascript", 160 | "handler" : "deleteAndKeepSelection", 161 | "shortcut" : "cmd ⌫", 162 | "name" : "Delete (and Keep Selection)", 163 | "identifier" : "deleteandkeepselection" 164 | } 165 | ], 166 | "menu": { 167 | "isRoot": true, 168 | "items": [ 169 | { 170 | "title": "Push and Shove", 171 | "items": [ 172 | { 173 | "title": "Resize Backwards", 174 | "items": [ 175 | "resize_invert_up", 176 | "resize_invert_up_10", 177 | "resize_invert_right", 178 | "resize_invert_right_10", 179 | "resize_invert_down", 180 | "resize_invert_down_10", 181 | "resize_invert_left", 182 | "resize_invert_left_10" 183 | ] 184 | }, 185 | { 186 | "title": "Move Halfway", 187 | "items": [ 188 | "move_halfway_up", 189 | "move_halfway_right", 190 | "move_halfway_down", 191 | "move_halfway_left" 192 | ] 193 | }, 194 | { 195 | "title": "Resize Halfway", 196 | "items": [ 197 | "resize_halfway_up", 198 | "resize_halfway_right", 199 | "resize_halfway_down", 200 | "resize_halfway_left" 201 | ] 202 | }, 203 | { 204 | "title": "Align", 205 | "items": [ 206 | "align_horizontally", 207 | "align_horizontally_to_artboard", 208 | "align_vertically", 209 | "align_vertically_to_artboard" 210 | ] 211 | }, 212 | "toggleproportions", 213 | "pasteandreplace", 214 | "deleteandkeepselection" 215 | ] 216 | } 217 | ] 218 | }, 219 | "identifier" : "co.ozzik.pushandshove", 220 | "version" : "1.2.2", 221 | "description" : "Resizing, aligning and generally manipulating layers wickedly", 222 | "authorEmail" : "hey@ozzik.co", 223 | "name" : "Push and Shove", 224 | "appcast": "https://raw.githubusercontent.com/ozzik/push-and-shove/master/appcast.xml" 225 | } 226 | -------------------------------------------------------------------------------- /push-and-shove.sketchplugin/Contents/Sketch/script.cocoascript: -------------------------------------------------------------------------------- 1 | // Halfway methods 2 | var manipulate = function(context, axis, offset, isReverse) { 3 | var selection = context.selection, 4 | layer, 5 | layerFrame, 6 | isConstrained, 7 | layerWidth, 8 | layerHeight; 9 | 10 | for (var i = 0; i < selection.count(); i++) { 11 | layer = selection.objectAtIndex(i), 12 | layerFrame = layer.frame(), 13 | isConstrained = layer.frame().constrainProportions(), 14 | layerWidth = layer.frame().width(), 15 | layerHeight = layer.frame().height(); 16 | 17 | if (axis === "x") { 18 | layerFrame.setX(layerFrame.x() + offset); 19 | } else if (axis === "y") { 20 | layerFrame.setY(layerFrame.y() + offset); 21 | } else if (axis === "width") { 22 | layerFrame.setWidth(layerFrame.width() + offset); 23 | isReverse && layerFrame.setX(layerFrame.x() + (offset * (offset ? -1 : 1))); 24 | } else { 25 | layerFrame.setHeight(layerFrame.height() + offset); 26 | isReverse && layerFrame.setY(layerFrame.y() + (offset * (offset ? -1 : 1))); 27 | } 28 | 29 | // Fixing new positioning due to constraints 30 | if (isReverse && isConstrained) { 31 | layerWidth -= layerFrame.width(); // New values to adjust accordingly 32 | layerHeight -= layerFrame.height(); 33 | 34 | if (axis === "width") { 35 | layerFrame.setY(layerFrame.y() + (layerHeight * (offset ? 1 : -1))); 36 | } else { 37 | layerFrame.setX(layerFrame.x() + (layerWidth * (offset ? 1 : -1))); 38 | } 39 | } 40 | } 41 | 42 | context.document.reloadInspector(); 43 | }; 44 | 45 | var move_halfway_up = function(context) { 46 | manipulate(context, "y", -.5); 47 | }; 48 | var move_halfway_right = function(context) { 49 | manipulate(context, "x", .5); 50 | }; 51 | var move_halfway_down = function(context) { 52 | manipulate(context, "y", .5); 53 | }; 54 | var move_halfway_left = function(context) { 55 | manipulate(context, "x", -.5); 56 | }; 57 | 58 | var resize_halfway_up = function(context) { 59 | manipulate(context, "height", -.5); 60 | }; 61 | var resize_halfway_right = function(context) { 62 | manipulate(context, "width", .5); 63 | }; 64 | var resize_halfway_down = function(context) { 65 | manipulate(context, "height", .5); 66 | }; 67 | var resize_halfway_left = function(context) { 68 | manipulate(context, "width", -.5); 69 | }; 70 | 71 | // Reverse methods 72 | var resize_invert_up = function(context) { 73 | manipulate(context, "height", 1, true); 74 | }; 75 | var resize_invert_right = function(context) { 76 | manipulate(context, "width", -1, true); 77 | }; 78 | var resize_invert_down = function(context) { 79 | manipulate(context, "height", -1, true); 80 | }; 81 | var resize_invert_left = function(context) { 82 | manipulate(context, "width", 1, true); 83 | }; 84 | var resize_invert_up_10 = function(context) { 85 | manipulate(context, "height", 10, true); 86 | }; 87 | var resize_invert_right_10 = function(context) { 88 | manipulate(context, "width", -10, true); 89 | }; 90 | var resize_invert_down_10 = function(context) { 91 | manipulate(context, "height", -10, true); 92 | }; 93 | var resize_invert_left_10 = function(context) { 94 | manipulate(context, "width", 10, true); 95 | }; 96 | 97 | // Toggle proportions contraint methods 98 | var dothetoggle = function(context) { 99 | var selection = context.selection; 100 | 101 | for (var i = 0; i < selection.count(); i++) { 102 | var layer = selection.objectAtIndex(i), 103 | isConstrained = layer.frame().constrainProportions(); 104 | 105 | layer.frame().constrainProportions = (isConstrained) ? 0 : 1; 106 | } 107 | 108 | context.document.reloadInspector(); 109 | } 110 | 111 | // Alignment methods 112 | var align = function(context, isHorizontal, isAlignToArtboard) { 113 | var selection = context.selection, 114 | document = context.document, 115 | layerFrame = selection.objectAtIndex(0).frame(), 116 | layerAbsolutePosition = _getAbsolutePosition(selection[0], true) 117 | playground = { 118 | startX: layerAbsolutePosition.x, 119 | startY: layerAbsolutePosition.y, 120 | endX: layerAbsolutePosition.x + layerFrame.width(), 121 | endY: layerAbsolutePosition.y + layerFrame.height() 122 | }, 123 | areAllWidthsOdd = true, 124 | areAllHeightsOdd = true, 125 | areAllWidthsEven = true, 126 | areAllHeightsEven = true, 127 | isAlignToArtboard = isAlignToArtboard || selection.count() == 1; 128 | 129 | // Aligning different layers 130 | if (!isAlignToArtboard) { 131 | // Looking for biggest playground area 132 | for (var i = 0; i < selection.count(); i++) { 133 | layerFrame = selection.objectAtIndex(i).frame(); 134 | layerAbsolutePosition = _getAbsolutePosition(selection[i], true); 135 | 136 | playground.startX = layerAbsolutePosition.x < playground.startX ? layerAbsolutePosition.x : playground.startX; 137 | playground.startY = layerAbsolutePosition.y < playground.startY ? layerAbsolutePosition.y : playground.startY; 138 | playground.endX = (layerAbsolutePosition.x + layerFrame.width() > playground.endX) ? (layerAbsolutePosition.x + layerFrame.width()) : playground.endX; 139 | playground.endY = (layerAbsolutePosition.y + layerFrame.height() > playground.endY) ? (layerAbsolutePosition.y + layerFrame.height()) : playground.endY; 140 | areAllWidthsOdd &= (layerFrame.width() % 2 !== 0); 141 | areAllHeightsOdd &= (layerFrame.height() % 2 !== 0); 142 | areAllWidthsEven &= (layerFrame.width() % 2 === 0); 143 | areAllHeightsEven &= (layerFrame.height() % 2 === 0); 144 | } 145 | 146 | playground.width = playground.endX - playground.startX; 147 | playground.height = playground.endY - playground.startY; 148 | // Synthesizing playground size when they're odd and some layers have even sizes 149 | // (if both playground's size and layers' are odd then math will do its magic) 150 | playground.width -= ((areAllWidthsOdd && playground.width % 2 === 0) || (areAllWidthsEven && playground.width % 2)) ? 1 : 0; 151 | playground.height -= ((areAllHeightsOdd && playground.height % 2 === 0) || (areAllHeightsEven && playground.height % 2)) ? 1 : 0; 152 | 153 | // Aligning each object to playground 154 | for (var i = 0; i < selection.count(); i++) { 155 | _align(selection.objectAtIndex(i), isHorizontal); 156 | } 157 | } else { // Aligning to artboard 158 | var artboardFrame = document.currentPage().currentArtboard().frame(); 159 | playground.startX = 0; 160 | playground.startY = 0; 161 | playground.endX = artboardFrame.width(); 162 | playground.endY = artboardFrame.height(); 163 | playground.width = artboardFrame.width(); 164 | playground.height = artboardFrame.height(); 165 | 166 | // Aligning 167 | for (var i = 0; i < selection.count(); i++) { 168 | _align(selection.objectAtIndex(i), isHorizontal); 169 | } 170 | } 171 | 172 | function _align(layer, isHorizontal) { 173 | var isOdd = false, 174 | layerFrame = layer.frame(), 175 | layerAbsolute = _getAbsolutePosition(layer), 176 | newPos; 177 | 178 | if (isHorizontal) { 179 | newPos = playground.startX + ((playground.width - layerFrame.width()) / 2) - layerAbsolute.x; 180 | 181 | if (!layerAbsolute.x) { 182 | layerFrame.setX(newPos); 183 | } else { 184 | layerFrame.setX(layerFrame.x() + newPos); 185 | } 186 | 187 | isOdd = (playground.width + layerFrame.width()) % 2 !== 0; 188 | } else { 189 | newPos = playground.startY + ((playground.height - layerFrame.height()) / 2) - layerAbsolute.y; 190 | 191 | if (!layerAbsolute.y) { 192 | layerFrame.setY(newPos); 193 | } else { 194 | layerFrame.setY(layerFrame.y() + newPos); 195 | } 196 | 197 | isOdd = (playground.height + layerFrame.height()) % 2 !== 0; 198 | } 199 | isOdd && document.showMessage("👻 Aligned to half pixels (odd numbers)"); 200 | } 201 | }; 202 | 203 | var align_horizontally = function(context) { 204 | align(context, true); 205 | }; 206 | var align_vertically = function(context) { 207 | align(context); 208 | }; 209 | var align_horizontally_to_artboard = function(context) { 210 | align(context, true, true); 211 | }; 212 | var align_vertically_to_artboard = function(context) { 213 | align(context, false, true); 214 | }; 215 | 216 | function _getAbsolutePosition(layer, isAnyway) { 217 | var x = 0, 218 | y = 0; 219 | 220 | if (layer.parentGroup() instanceof MSArtboardGroup == 0 || layer.parentGroup() instanceof MSPage == 0 || isAnyway) { 221 | while (layer instanceof MSArtboardGroup == 0 && layer.parentGroup() instanceof MSPage == 0) { 222 | if (layer instanceof MSArtboardGroup == 0 && layer.parentGroup() instanceof MSPage == 0) { 223 | x += layer.frame().x(); 224 | y += layer.frame().y(); 225 | } 226 | 227 | layer = layer.parentGroup(); 228 | } 229 | } 230 | 231 | return { 232 | x: x, 233 | y: y 234 | }; 235 | } 236 | 237 | var pasteAndReplace = function(context) { 238 | var doc = context.document, 239 | selection = context.selection[0], 240 | pastedLayer; 241 | 242 | if (context.selection.count() > 1) { 243 | doc.showMessage("Plz select just one layer"); 244 | } else { 245 | doc.currentHandler().pasteOverSelection(selection); 246 | pastedLayer = doc.selectedLayers(); 247 | 248 | doc.setSelectedLayers(MSLayerArray.arrayWithLayer(selection)); 249 | doc.currentHandler().delete(selection); 250 | 251 | doc.setSelectedLayers(pastedLayer); 252 | } 253 | }; 254 | 255 | var deleteAndKeepSelection = function(context) { 256 | var doc = context.document, 257 | selection = context.selection[0], 258 | pastedLayer, 259 | selectionParent = selection.parentGroup(), 260 | neighbors = selectionParent.layers().array(), 261 | sibling; 262 | 263 | for (var i = 0; i < neighbors.count(); i++) { 264 | // Looking for next (top) layer 265 | if (neighbors[i] == selection && i < neighbors.count() - 1) { 266 | sibling = neighbors[i + 1]; 267 | } 268 | } 269 | // Still looking for previous (below) layer 270 | if (!sibling && neighbors.count() > 1) { 271 | sibling = neighbors[neighbors.count() - 2] 272 | } 273 | 274 | doc.currentHandler().delete(selection); 275 | 276 | if (sibling) { 277 | doc.setSelectedLayers([ sibling ]); 278 | } else { 279 | doc.setSelectedLayers([ selectionParent ]); 280 | } 281 | }; -------------------------------------------------------------------------------- /readme-assets/demo1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozzik/push-and-shove/24d2b7768eab9e2e4171d2ec631ee1853ec8639e/readme-assets/demo1.gif -------------------------------------------------------------------------------- /readme-assets/demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozzik/push-and-shove/24d2b7768eab9e2e4171d2ec631ee1853ec8639e/readme-assets/demo2.gif -------------------------------------------------------------------------------- /readme-assets/demo3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozzik/push-and-shove/24d2b7768eab9e2e4171d2ec631ee1853ec8639e/readme-assets/demo3.gif --------------------------------------------------------------------------------