├── image.png ├── README.md └── typographicScale.sketchplugin /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/automat/sketch-plugin-typographic-scale/HEAD/image.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##Sketch-Plugin: Typographic Scale ✍ 2 | 3 | This plugin generates a typographic scale from selected text layers.
4 | The produced scales are meant to serve as general starting points for building your own scales.
5 | (Based on: http://alistapart.com/article/more-meaningful-typography) 6 | 7 | ![Shot](/image.png) 8 | 9 | ###Usage 10 | 11 | Select a textlayer, multiple textlayers, mixed layers containing at least 12 | one textlayer or groups (only root will be processed), hit the plugin. 13 | 14 | ###Options 15 | 16 | Option | Description 17 | ------------ | ------------- 18 | Type Scale | The scale to be used 19 | Scale Range | The scale range to be applied 20 | Type Return | Either floats or integers 21 | Layer Suffix | (optional) em and (or) % suffix appended to layer name, based on return type 22 | 23 | 24 | -------------------------------------------------------------------------------- /typographicScale.sketchplugin: -------------------------------------------------------------------------------- 1 | // Typographic Scale (v0.1) - Creates a typographic scale from a TextLayer using its fontSize as the base scale 2 | 3 | (function () { 4 | var PLUGIN_ID = 'PLUGIN_TYPOGRAPHIC_SCALE', 5 | PLUGIN_NAME = 'Typographic Scale ✍'; 6 | 7 | function warn(msg) { 8 | doc.showMessage_(PLUGIN_NAME + ' : ' + msg); 9 | } 10 | 11 | var numElements = selection.count(); 12 | if (numElements == 0) { 13 | warn('Nothing selected.'); 14 | return; 15 | } else if (numElements == 1 && selection[0].class() != MSTextLayer && 16 | selection[0].class() != MSLayerGroup) { 17 | warn('Selected element is not of type TextLayer.'); 18 | return; 19 | } 20 | 21 | var elements = []; 22 | var i; 23 | 24 | (function () { 25 | var enumerator, item, class_; 26 | var layers, numLayers, item_; 27 | 28 | enumerator = selection.objectEnumerator(); 29 | while (item = enumerator.nextObject()) { 30 | class_ = item.class(); 31 | if (class_ == MSTextLayer) { 32 | elements.push(item); 33 | } else if (class_ == MSLayerGroup) { 34 | //just 1st level 35 | layers = item.layers().array(); 36 | numLayers = layers.count(); 37 | i = -1; 38 | while (++i < numLayers) { 39 | item_ = layers[i]; 40 | if (item_.class() == MSTextLayer) { 41 | elements.push(item_); 42 | } 43 | } 44 | } 45 | } 46 | numElements = elements.length; 47 | })(); 48 | 49 | if (numElements == 0) { 50 | warn('None of the selected elements is or has children of type TextLayer.'); 51 | return; 52 | } 53 | 54 | function MakeDict(objects, keys) { 55 | return NSDictionary.dictionaryWithObjects_forKeys(objects, keys); 56 | } 57 | 58 | var PRESET_KEY_ORDERED = { 59 | Scale: ['Minor Second', 60 | 'Major Second', 61 | 'Minor Third', 62 | 'Major Third', 63 | 'Perfect Fourth', 64 | 'Augmented Fourth', 65 | 'Perfect Fifth', 66 | 'Golden Ratio'], 67 | Range: ['x2', 'x4', 'x8'], 68 | All: ['Scale', 'Range', 'ReturnType', 'SuffixEm', 'SuffixPer'] 69 | }, 70 | PRESET = MakeDict( 71 | [MakeDict( 72 | [ 73 | [0.772, 0.823, 0.878, 0.937, 1, 1.067, 1.138, 1.215, 1.296], 74 | [0.624, 0.702, 0.79, 0.889, 1, 1.125, 1.266, 1.424, 1.602], 75 | [0.482, 0.579, 0.694, 0.833, 1, 1.2, 1.44, 1.728, 2.074], 76 | [0.41, 0.512, 0.64, 0.8, 1, 1.25, 1.563, 1.953, 2.441], 77 | [0.317, 0.422, 0.563, 0.75, 1, 1.333, 1.777, 2.369, 3.157], 78 | [0.25, 0.354, 0.5, 0.707, 1, 1.414, 1.999, 2.879, 3.998], 79 | [0.198, 0.296, 0.444, 0.667, 1, 1.5, 2.25, 3.375, 5.063], 80 | [0.146, 0.236, 0.382, 0.618, 1, 1.618, 2.618, 4.236, 6.854] 81 | ], 82 | PRESET_KEY_ORDERED.Scale), 83 | MakeDict( 84 | [2, 4, 8], 85 | PRESET_KEY_ORDERED.Range), 86 | ['Float', 'Integer'], false, false], 87 | PRESET_KEY_ORDERED.All 88 | ); 89 | 90 | 91 | var defaults = NSUserDefaults.standardUserDefaults(), 92 | pluginValues; 93 | 94 | if (!defaults.objectForKey(PLUGIN_ID)) { 95 | defaults.setObject_forKey_( 96 | MakeDict( 97 | ['Major Third', 'x8', 'Float', false, false], 98 | ['Scale', 'Range', 'ReturnType', 'SuffixEm', 'SuffixPer']), 99 | PLUGIN_ID); 100 | } 101 | 102 | // mutableCopy nor NSMutableDictionary.dictionaryWithDictionary return a mutable copy 103 | // this will work for now 104 | pluginValues = NSMutableDictionary.alloc().init(); 105 | pluginValues.setDictionary_(defaults.objectForKey(PLUGIN_ID)); 106 | 107 | var typeScale, 108 | typeScaleNum; 109 | var funcScale, 110 | funcSuffix; 111 | 112 | (function () { 113 | // TODO : do it the proper cocoa way 114 | var viewWidth = 270, 115 | viewHeight = 100, 116 | labelWidth = 80, 117 | compWidth = viewWidth - labelWidth, 118 | compWidth_2 = compWidth * 0.5; 119 | 120 | var view = NSView.alloc().initWithFrame_(NSMakeRect(0, 0, viewWidth, viewHeight)); 121 | 122 | function createLabel(x, y, name) { 123 | var label = NSTextField.alloc().initWithFrame_(NSMakeRect(x, y, labelWidth, 25)); 124 | label.setEditable_(false); 125 | label.setSelectable_(false); 126 | label.setBezeled_(false); 127 | label.setDrawsBackground_(false); 128 | label.setFont(NSFont.systemFontOfSize_(11)); 129 | label.setStringValue_(name); 130 | return label; 131 | } 132 | 133 | function createSelect(x, y, width, values, initialState) { 134 | var select = NSPopUpButton.alloc().initWithFrame_(NSMakeRect(x, y + 5, width, 25)); 135 | select.addItemsWithTitles_(values); 136 | select.setFont(NSFont.systemFontOfSize_(11)); 137 | select.selectItemWithTitle_(initialState); 138 | 139 | return select; 140 | } 141 | 142 | function createCheckBox(x, y, name, initialState) { 143 | var btn = NSButton.alloc().initWithFrame_(NSMakeRect(x, y + 9, compWidth_2, 18)); 144 | btn.setButtonType_(NSSwitchButton); 145 | btn.setState_(initialState || NSOffState); 146 | btn.setTitle_(name); 147 | return btn; 148 | } 149 | 150 | var offset = viewHeight - 32; 151 | 152 | var scaleLabel = createLabel(0, offset, 'Type Scale'), 153 | scaleSelect = createSelect(labelWidth, offset, 130, PRESET_KEY_ORDERED['Scale'], pluginValues['Scale']), 154 | rangeSelect = createSelect(labelWidth + 130, offset, 60, PRESET_KEY_ORDERED['Range'], pluginValues['Range']); 155 | 156 | offset -= 32; 157 | 158 | var returnLabel = createLabel(0, offset, 'Type Return'), 159 | returnSelect = createSelect(labelWidth, offset, compWidth, PRESET['ReturnType'], pluginValues['ReturnType']); 160 | 161 | offset -= 31; 162 | 163 | var suffixLabel = createLabel(0, offset, 'Layer Suffix'), 164 | suffixCheckEm = createCheckBox(labelWidth, offset, 'em', pluginValues['SuffixEm']), 165 | suffixCheckPer = createCheckBox(labelWidth + compWidth_2 - 25, offset, 'percentage', pluginValues['SuffixPer']); 166 | 167 | var alert = NSAlert.alloc().init(); 168 | alert.setMessageText_(PLUGIN_NAME); 169 | alert.setAccessoryView_(view); 170 | 171 | view.addSubview_(scaleLabel); 172 | view.addSubview_(scaleSelect); 173 | view.addSubview_(rangeSelect); 174 | view.addSubview_(returnLabel); 175 | view.addSubview_(returnSelect); 176 | view.addSubview_(suffixLabel); 177 | view.addSubview_(suffixCheckEm); 178 | view.addSubview_(suffixCheckPer); 179 | 180 | alert.runModal(); 181 | 182 | var scale_ = scaleSelect.titleOfSelectedItem(), 183 | range = rangeSelect.titleOfSelectedItem(), 184 | return_ = returnSelect.titleOfSelectedItem(), 185 | suffixEm = suffixCheckEm.state(), 186 | suffixPer = suffixCheckPer.state(); 187 | 188 | pluginValues.setValue_forKey_(scale_, 'Scale'); 189 | pluginValues.setValue_forKey_(range, 'Range'); 190 | pluginValues.setValue_forKey_(return_, 'ReturnType'); 191 | pluginValues.setValue_forKey_(suffixEm, 'SuffixEm'); 192 | pluginValues.setValue_forKey_(suffixPer, 'SuffixPer'); 193 | 194 | defaults.setObject_forKey_( 195 | pluginValues, PLUGIN_ID 196 | ); 197 | defaults.synchronize(); 198 | 199 | var isReturnTypeFloat = return_ == PRESET['ReturnType'][0]; 200 | 201 | funcScale = new Function('base,scale', 'return ' + (isReturnTypeFloat ? 'base * scale' : '~~(base * scale + 0.5)')); 202 | funcSuffix = new Function('base,scale', 'return ' + 203 | (function () { 204 | var em, per; 205 | em = per = "'_' + "; 206 | if (isReturnTypeFloat) { 207 | em += "scale + 'em'"; 208 | per += "scale * 100 + '%'" 209 | } else { 210 | var val = "scale / base * 100"; 211 | em += val + "/ 100 + 'em'"; 212 | per += val + " + '%'"; 213 | } 214 | return suffixEm && suffixPer ? (em + '+' + per) : 215 | suffixEm ? em : suffixPer ? per : 216 | "''"; 217 | })()); 218 | 219 | 220 | range = PRESET['Range'][range]; 221 | typeScale = PRESET['Scale'][scale_]; 222 | 223 | var start = (typeScale.count() - 1) / 2 - range / 2, 224 | count = range + 1; 225 | 226 | typeScale = typeScale.subarrayWithRange_(NSMakeRange(start, count)); 227 | typeScaleNum = typeScale.count(); 228 | })(); 229 | 230 | 231 | (function () { 232 | var element, baseScale, baseName, baseOffset, scale; 233 | var elements_, element_, elementScale_; 234 | var offset, frame; 235 | 236 | var j = -1; 237 | while (++j < numElements) { 238 | element = elements[j]; 239 | 240 | baseScale = element.fontSize = funcScale(element.fontSize(), 1); 241 | baseName = element.name(); 242 | baseOffset = element.frame().y(); 243 | 244 | elements_ = new Array(typeScaleNum); 245 | 246 | i = -1; 247 | while (++i < typeScaleNum) { 248 | scale = typeScale[i]; 249 | 250 | element_ = element.duplicate(); 251 | elementScale_ = element_.fontSize = funcScale(baseScale, scale); 252 | element_.setName(baseName + funcSuffix(baseScale, elementScale_)); 253 | 254 | baseOffset -= scale >= 1 ? 0 : element_.frame().height(); 255 | elements_[i] = element_; 256 | } 257 | 258 | offset = 0; 259 | i = -1; 260 | while (++i < typeScaleNum) { 261 | frame = elements_[i].frame(); 262 | frame.y = baseOffset + offset; 263 | offset += frame.height(); 264 | } 265 | 266 | element.parentGroup().removeLayer(element); 267 | } 268 | })(); 269 | })(); 270 | 271 | --------------------------------------------------------------------------------