130 |
131 |
132 | {/* Choose colour */}
133 |
134 |
135 |
Starting Color
136 |
143 |
144 |
145 |
146 |
147 | {/* Generate hues or generate tints and/or shades */}
148 |
149 |
150 |
155 |
156 |
157 | {generateOption === generateOption1 ? (
158 |
159 |
160 |
Interval between each hue
161 |
162 |
163 |
169 |
170 |
171 |
172 |
173 | Generate tints for each hue
174 |
175 | {tintForHue ? (
176 |
177 |
Interval
178 |
179 |
185 |
186 |
187 | ) : null}
188 |
189 |
190 |
191 | Generate shades for each hue
192 |
193 | {shadeForHue ? (
194 |
195 |
Interval
196 |
197 |
203 |
204 |
205 | ) : null}
206 |
207 |
208 | ) : null}
209 |
210 | {generateOption === generateOption2 ? (
211 |
212 |
213 |
214 | Tints
215 |
216 | {tint ? (
217 |
218 |
Interval between each tint
219 |
220 |
226 |
227 |
228 | ) : null}
229 |
230 |
231 |
232 |
233 | Shades
234 |
235 | {shade ? (
236 |
237 |
Interval between each shade
238 |
239 |
245 |
246 |
247 | ) : null}
248 |
249 |
250 | ) : null}
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
265 |
266 |
269 |
270 |
271 |
272 |
273 | Developed and maintained at{' '}
274 |
275 | Flexcode Labs
276 |
277 |
278 |
Made in Tanzania
279 |
280 |
281 | )
282 | }
283 |
284 | export default render(Plugin)
285 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export const rgbToHSL = (r: number, g: number, b: number) => {
2 | // Find greatest and smallest channel values
3 | let cmin = Math.min(r, g, b),
4 | cmax = Math.max(r, g, b),
5 | delta = cmax - cmin,
6 | h = 0,
7 | s = 0,
8 | l = 0
9 |
10 | // Calculate hue
11 | // No difference
12 | if (delta == 0) h = 0
13 | // Red is max
14 | else if (cmax == r) h = ((g - b) / delta) % 6
15 | // Green is max
16 | else if (cmax == g) h = (b - r) / delta + 2
17 | // Blue is max
18 | else h = (r - g) / delta + 4
19 |
20 | h = Math.round(h * 60)
21 |
22 | // Make negative hues positive behind 360°
23 | if (h < 0) h += 360
24 |
25 | // Calculate lightness
26 | l = (cmax + cmin) / 2
27 |
28 | // Calculate saturation
29 | s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1))
30 |
31 | // Multiply l and s by 100
32 | s = +(s * 100).toFixed(1)
33 | l = +(l * 100).toFixed(1)
34 |
35 | return { h, s, l }
36 | }
37 |
38 | export const hexToHSL = (hex: string) => {
39 | const hexValue = hex.replace('#', '')
40 |
41 | let rgbFromHex = hexValue.match(/.{1,2}/g)
42 | let rgb: any = []
43 |
44 | if (rgbFromHex) {
45 | rgb = [
46 | parseInt(rgbFromHex[0], 16),
47 | parseInt(rgbFromHex[1], 16),
48 | parseInt(rgbFromHex[2], 16),
49 | ]
50 | }
51 |
52 | let r: number, g: number, b: number
53 |
54 | // Make r, g, and b fractions of 1
55 | r = rgb[0] / 255
56 | g = rgb[1] / 255
57 | b = rgb[2] / 255
58 |
59 | return rgbToHSL(r, g, b)
60 | }
61 |
62 | export const hslToRGB = (h: number, s: number, l: number) => {
63 | // Must be fractions of 1
64 | s /= 100
65 | l /= 100
66 |
67 | let c = (1 - Math.abs(2 * l - 1)) * s,
68 | x = c * (1 - Math.abs(((h / 60) % 2) - 1)),
69 | m = l - c / 2,
70 | r = 0,
71 | g = 0,
72 | b = 0
73 |
74 | if (0 <= h && h < 60) {
75 | r = c
76 | g = x
77 | b = 0
78 | } else if (60 <= h && h < 120) {
79 | r = x
80 | g = c
81 | b = 0
82 | } else if (120 <= h && h < 180) {
83 | r = 0
84 | g = c
85 | b = x
86 | } else if (180 <= h && h < 240) {
87 | r = 0
88 | g = x
89 | b = c
90 | } else if (240 <= h && h < 300) {
91 | r = x
92 | g = 0
93 | b = c
94 | } else if (300 <= h && h < 360) {
95 | r = c
96 | g = 0
97 | b = x
98 | }
99 | r = Math.round((r + m) * 255) / 255
100 | g = Math.round((g + m) * 255) / 255
101 | b = Math.round((b + m) * 255) / 255
102 |
103 | return { r, g, b }
104 | }
105 |
106 | export const getHues = (h: number, s: number, l: number, space: number) => {
107 | let positiveSum = h
108 | let negativeSum = h
109 | let hs: number[] = []
110 |
111 | while (negativeSum >= 0) {
112 | hs.push(negativeSum)
113 | negativeSum -= space
114 | }
115 |
116 | while (positiveSum <= 359) {
117 | hs.push(positiveSum)
118 | positiveSum += space
119 | }
120 |
121 | hs = hs.sort((a, b) => a - b)
122 |
123 | let uniqHs = hs.filter((h, index) => {
124 | return hs.indexOf(h) === index
125 | })
126 |
127 | interface Hues {
128 | h: number
129 | s: number
130 | l: number
131 | }
132 |
133 | let hues: Hues[] = []
134 | uniqHs.forEach((uniqH) => {
135 | hues.push({ h: uniqH, s: s, l: l })
136 | })
137 |
138 | return hues
139 | }
140 |
141 | export const getTints = (h: number, s: number, l: number, space: number) => {
142 | const tints = []
143 |
144 | while (l <= 100) {
145 | const tint = hslToRGB(h, s, l)
146 | tints.push(tint)
147 | l += space
148 | }
149 |
150 | return tints
151 | }
152 |
153 | export const getShades = (h: number, s: number, l: number, space: number) => {
154 | const shades = []
155 |
156 | while (l >= 0) {
157 | const shade = hslToRGB(h, s, l)
158 | shades.push(shade)
159 | l -= space
160 | }
161 |
162 | return shades
163 | }
164 |
165 | export interface Padding {
166 | top: number
167 | right: number
168 | bottom: number
169 | left: number
170 | }
171 |
172 | export class Container {
173 | name: string
174 | layoutMode: 'NONE' | 'HORIZONTAL' | 'VERTICAL'
175 | padding: Padding
176 | spacing: number
177 | primarySizingMode: 'FIXED' | 'AUTO'
178 | counterSizingMode: 'FIXED' | 'AUTO'
179 |
180 | constructor(
181 | name: string,
182 | layoutMode: 'NONE' | 'HORIZONTAL' | 'VERTICAL',
183 | padding: Padding,
184 | spacing: number,
185 | primarySizingMode: 'FIXED' | 'AUTO',
186 | counterSizingMode: 'FIXED' | 'AUTO'
187 | ) {
188 | this.name = name
189 | this.layoutMode = layoutMode
190 | this.padding = padding
191 | this.spacing = spacing
192 | this.primarySizingMode = primarySizingMode
193 | this.counterSizingMode = counterSizingMode
194 | }
195 |
196 | createContainer() {
197 | const container = figma.createFrame()
198 | container.name = this.name
199 | container.layoutMode = this.layoutMode
200 |
201 | container.paddingTop = this.padding.top
202 | container.paddingRight = this.padding.right
203 | container.paddingBottom = this.padding.bottom
204 | container.paddingLeft = this.padding.left
205 |
206 | container.itemSpacing = this.spacing
207 | container.primaryAxisSizingMode = this.primarySizingMode
208 | container.counterAxisSizingMode = this.counterSizingMode
209 | return container
210 | }
211 | }
212 |
213 | // Generate hues
214 | export const generateHues = (
215 | { h, s, l }: any,
216 | space: number,
217 | frameDirection: any,
218 | padding: Padding,
219 | spacing: number,
220 | size: number,
221 | tintForHue: boolean,
222 | shadeForHue: boolean,
223 | tintsForHuesAmount: number,
224 | shadesForHuesAmount: number
225 | ) => {
226 | const hues = getHues(h, s, l, space)
227 | const parentFrame = new Container(
228 | `Hues`,
229 | frameDirection,
230 | padding,
231 | spacing,
232 | 'AUTO',
233 | 'AUTO'
234 | ).createContainer()
235 |
236 | hues.forEach(({ h, l, s }) => {
237 | const hueRgb = hslToRGB(h, s, l)
238 |
239 | const hueCircle = () => {
240 | const hueNode = figma.createEllipse()
241 | hueNode.resize(size, size)
242 |
243 | const { r, g, b } = hueRgb
244 | hueNode.fills = [{ type: 'SOLID', color: { r, g, b } }]
245 |
246 | return hueNode
247 | }
248 |
249 | if (tintForHue && shadeForHue) {
250 | const tints = generateTints(
251 | { h, l, s },
252 | tintsForHuesAmount,
253 | frameDirection,
254 | padding,
255 | spacing,
256 | size
257 | )
258 |
259 | const shades = generateShades(
260 | { h, l, s },
261 | shadesForHuesAmount,
262 | frameDirection,
263 | padding,
264 | spacing,
265 | size
266 | )
267 |
268 | const container = new Container(
269 | `Hues`,
270 | frameDirection,
271 | padding,
272 | spacing,
273 | 'AUTO',
274 | 'AUTO'
275 | ).createContainer()
276 |
277 | container.appendChild(tints)
278 | container.appendChild(shades)
279 |
280 | parentFrame.layoutMode =
281 | frameDirection === 'VERTICAL' ? 'HORIZONTAL' : 'VERTICAL'
282 |
283 | parentFrame.appendChild(container)
284 | } else if (tintForHue) {
285 | const tints = generateTints(
286 | { h, l, s },
287 | tintsForHuesAmount,
288 | frameDirection,
289 | padding,
290 | spacing,
291 | size
292 | )
293 |
294 | const container = new Container(
295 | `Hues`,
296 | frameDirection === 'VERTICAL' ? 'HORIZONTAL' : 'VERTICAL',
297 | padding,
298 | spacing,
299 | 'AUTO',
300 | 'AUTO'
301 | ).createContainer()
302 |
303 | container.appendChild(tints)
304 |
305 | parentFrame.layoutMode =
306 | frameDirection === 'VERTICAL' ? 'HORIZONTAL' : 'VERTICAL'
307 | parentFrame.appendChild(container)
308 | } else if (shadeForHue) {
309 | const shades = generateShades(
310 | { h, l, s },
311 | shadesForHuesAmount,
312 | frameDirection,
313 | padding,
314 | spacing,
315 | size
316 | )
317 |
318 | const container = new Container(
319 | `Hues`,
320 | frameDirection === 'VERTICAL' ? 'HORIZONTAL' : 'VERTICAL',
321 | padding,
322 | spacing,
323 | 'AUTO',
324 | 'AUTO'
325 | ).createContainer()
326 | container.appendChild(shades)
327 |
328 | parentFrame.layoutMode =
329 | frameDirection === 'VERTICAL' ? 'HORIZONTAL' : 'VERTICAL'
330 | parentFrame.appendChild(container)
331 | } else {
332 | parentFrame.appendChild(hueCircle())
333 | }
334 | })
335 |
336 | return parentFrame
337 | }
338 |
339 | // Generate tints
340 | export const generateTints = (
341 | { h, s, l }: any,
342 | space: number,
343 | frameDirection: any,
344 | padding: Padding,
345 | spacing: number,
346 | size: number
347 | ): FrameNode => {
348 | const tints = getTints(h, s, l, space)
349 | const container = new Container(
350 | `Tints`,
351 | frameDirection,
352 | padding,
353 | spacing,
354 | 'AUTO',
355 | 'AUTO'
356 | ).createContainer()
357 |
358 | const reversedTints = tints.reverse()
359 |
360 | reversedTints.forEach((tint) => {
361 | const tintNode = figma.createEllipse()
362 | tintNode.resize(size, size)
363 |
364 | const { r, g, b } = tint
365 | tintNode.fills = [{ type: 'SOLID', color: { r, g, b } }]
366 |
367 | return container.appendChild(tintNode)
368 | })
369 |
370 | return container
371 | }
372 |
373 | // Generate shades
374 | export const generateShades = (
375 | { h, s, l }: any,
376 | space: number,
377 | frameDirection: any,
378 | padding: Padding,
379 | spacing: number,
380 | size: number
381 | ): FrameNode => {
382 | const shades = getShades(h, s, l, space)
383 | const container = new Container(
384 | `Shades`,
385 | frameDirection,
386 | padding,
387 | spacing,
388 | 'AUTO',
389 | 'AUTO'
390 | ).createContainer()
391 |
392 | shades.forEach((shade) => {
393 | const shadeNode = figma.createEllipse()
394 | shadeNode.resize(size, size)
395 |
396 | const { r, g, b } = shade
397 | shadeNode.fills = [{ type: 'SOLID', color: { r, g, b } }]
398 |
399 | container.appendChild(shadeNode)
400 | })
401 |
402 | return container
403 | }
404 |
405 | // select generated frame
406 | export const selectFrame = (node: FrameNode) => {
407 | const selectFrame: FrameNode[] = []
408 | selectFrame.push(node)
409 |
410 | figma.currentPage.selection = selectFrame
411 | figma.viewport.scrollAndZoomIntoView(selectFrame)
412 | }
413 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@create-figma-plugin/tsconfig",
3 | "compilerOptions": {
4 | "typeRoots": [
5 | "node_modules/@figma",
6 | "node_modules/@types"
7 | ]
8 | },
9 | "include": [
10 | "src/**/*.ts",
11 | "src/**/*.tsx"
12 | ]
13 | }
--------------------------------------------------------------------------------