├── 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 | 
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 |
--------------------------------------------------------------------------------