├── .gitignore ├── README.md ├── manifest.json ├── package-lock.json ├── package.json ├── rangi-showcase.gif ├── rangi-showcase.mp4 ├── src ├── main.ts ├── types.ts ├── ui.css ├── ui.tsx └── utils │ └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | *.css.d.ts 4 | build/ 5 | node_modules/ 6 | manifest.json 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rangi 2 | 3 | A Figma plugin to help generate hues, tints, and/or shades 4 | 5 | 7 | 8 | ## Features 9 | 10 | - [x] Generate hues from a given color. 11 | 12 | - [x] Generate tints from a given color 13 | 14 | - [x] Generate shades from a given color 15 | 16 | ## How it works 17 | 18 | ### Generating Hues 19 | 20 | rangi takes a color input and an interval value. It converts the color to HSL(Hue, Saturation, Light) value and plus or minus the interval value from the H value until it's either greater or equal to 0 or less or equal to 359. The max is set to 359 because some color values would give a black color when they hit 360. 21 | 22 | ### Generating Tints 23 | 24 | To generate Tints, rangi takes the initial color, converts it to HSL(Hue, Saturation, Light) value adds the interval value to the L value until the L value is less or equal to 100. 25 | 26 | ### Generating Shades 27 | 28 | Shades generation is similar to tints generation except instead of adding the interval value to the L value, rangi substracts it until the L value is less or equal to 0. 29 | 30 | ## Todo & Roadmap 31 | 32 | If the plugin gains traction, the plan is to do the following: 33 | 34 | - [ ] Refactor code 35 | - [ ] Add ability for users to configure the generated frames, things like size, frame orientation, shape, naming, etc. 36 | 37 | ## Assets & Links 38 | 39 | UI design of the plugin: 40 | https://www.figma.com/community/file/1157058671138458897 41 | 42 | Plugin in Figma community: 43 | https://www.figma.com/community/plugin/1153959136228678599 44 | 45 | Built and maintained with ❤ by [Flexcode Labs](https://flexocelabs.com) 46 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": "1.0.0", 3 | "editorType": ["figma"], 4 | "id": "1153959136228678599", 5 | "name": "rangi", 6 | "main": "build/main.js", 7 | "ui": "build/ui.js", 8 | "networkAccess": { 9 | "allowedDomains": ["none"], 10 | "reasoning": "" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rangi", 3 | "version": "1.1.1", 4 | "description": "A Figma plugin to help generate hues, tints, and/or shades", 5 | "author": "Flexcode Labs", 6 | "license": "Figma Community Resource License", 7 | "scripts": { 8 | "build": "build-figma-plugin --typecheck --minify", 9 | "watch": "build-figma-plugin --typecheck --watch" 10 | }, 11 | "dependencies": { 12 | "@create-figma-plugin/ui": "^2.1.3", 13 | "@create-figma-plugin/utilities": "^2.1.3", 14 | "preact": "^10" 15 | }, 16 | "devDependencies": { 17 | "@create-figma-plugin/build": "^2.1.3", 18 | "@create-figma-plugin/tsconfig": "^2.1.3", 19 | "@figma/plugin-typings": "1.50.0", 20 | "typescript": "^4" 21 | }, 22 | "figma-plugin": { 23 | "editorType": [ 24 | "figma" 25 | ], 26 | "id": "1153959136228678599", 27 | "name": "rangi", 28 | "api": "1.0.0", 29 | "main": "src/main.ts", 30 | "ui": "src/ui.tsx", 31 | "networkAccess": { 32 | "allowedDomains": [ 33 | "none" 34 | ], 35 | "reasoning": "" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rangi-showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexcodelabs/rangi-figma/c0039a12ffbfcfc41202eabe077473ce83496be9/rangi-showcase.gif -------------------------------------------------------------------------------- /rangi-showcase.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flexcodelabs/rangi-figma/c0039a12ffbfcfc41202eabe077473ce83496be9/rangi-showcase.mp4 -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { once, showUI } from '@create-figma-plugin/utilities' 2 | import { CancelHandler, GenerateHandler, InputValues } from './types' 3 | import { 4 | hexToHSL, 5 | Padding, 6 | generateHues, 7 | generateTints, 8 | generateShades, 9 | Container, 10 | selectFrame, 11 | } from './utils' 12 | 13 | export default function () { 14 | once('GENERATE', function (inputValues: InputValues) { 15 | const circleSize = 120 16 | const circleSpace = 30 17 | const frameDirection = 'HORIZONTAL' 18 | 19 | const { 20 | colorCode, 21 | hue, 22 | hueInterval, 23 | tintForHue, 24 | tintForHueInterval, 25 | shadeForHue, 26 | shadeForHueInterval, 27 | tint, 28 | tintInterval, 29 | shade, 30 | shadeInterval, 31 | } = inputValues 32 | 33 | const color = hexToHSL(colorCode) 34 | 35 | const framePadding: Padding = { 36 | top: 50, 37 | right: 50, 38 | bottom: 50, 39 | left: 50, 40 | } 41 | 42 | if (hue && tint && shade) { 43 | const hues = generateHues( 44 | color, 45 | hueInterval, 46 | frameDirection, 47 | framePadding, 48 | circleSpace, 49 | circleSize, 50 | tintForHue, 51 | shadeForHue, 52 | tintForHueInterval, 53 | shadeForHueInterval 54 | ) 55 | const tints = generateTints( 56 | color, 57 | tintInterval, 58 | frameDirection, 59 | framePadding, 60 | circleSpace, 61 | circleSize 62 | ) 63 | 64 | const shades = generateShades( 65 | color, 66 | shadeInterval, 67 | frameDirection, 68 | framePadding, 69 | circleSpace, 70 | circleSize 71 | ) 72 | 73 | const parentFrame = new Container( 74 | `Parent Frame`, 75 | frameDirection, 76 | framePadding, 77 | 70, 78 | 'AUTO', 79 | 'AUTO' 80 | ).createContainer() 81 | 82 | parentFrame.appendChild(hues) 83 | parentFrame.appendChild(tints) 84 | parentFrame.appendChild(shades) 85 | selectFrame(parentFrame) 86 | figma.closePlugin('Hues, tints and shades generated') 87 | } else if (hue && tint) { 88 | const hues = generateHues( 89 | color, 90 | hueInterval, 91 | frameDirection, 92 | framePadding, 93 | circleSpace, 94 | circleSize, 95 | tintForHue, 96 | shadeForHue, 97 | tintForHueInterval, 98 | shadeForHueInterval 99 | ) 100 | const tints = generateTints( 101 | color, 102 | tintInterval, 103 | frameDirection, 104 | framePadding, 105 | circleSpace, 106 | circleSize 107 | ) 108 | 109 | const parentFrame = new Container( 110 | `Parent Frame`, 111 | frameDirection, 112 | framePadding, 113 | 70, 114 | 'AUTO', 115 | 'AUTO' 116 | ).createContainer() 117 | 118 | parentFrame.appendChild(hues) 119 | parentFrame.appendChild(tints) 120 | selectFrame(parentFrame) 121 | figma.closePlugin('Hues and tints generated') 122 | } else if (hue && shade) { 123 | const hues = generateHues( 124 | color, 125 | hueInterval, 126 | frameDirection, 127 | framePadding, 128 | circleSpace, 129 | circleSize, 130 | tintForHue, 131 | shadeForHue, 132 | tintForHueInterval, 133 | shadeForHueInterval 134 | ) 135 | 136 | const shades = generateShades( 137 | color, 138 | shadeInterval, 139 | frameDirection, 140 | framePadding, 141 | circleSpace, 142 | circleSize 143 | ) 144 | 145 | const parentFrame = new Container( 146 | `Parent Frame`, 147 | frameDirection, 148 | framePadding, 149 | 70, 150 | 'AUTO', 151 | 'AUTO' 152 | ).createContainer() 153 | 154 | parentFrame.appendChild(hues) 155 | parentFrame.appendChild(shades) 156 | selectFrame(parentFrame) 157 | figma.closePlugin('Hues and shades generated') 158 | } else if (tint && shade) { 159 | const tints = generateTints( 160 | color, 161 | tintInterval, 162 | frameDirection, 163 | framePadding, 164 | circleSpace, 165 | circleSize 166 | ) 167 | 168 | const shades = generateShades( 169 | color, 170 | shadeInterval, 171 | frameDirection, 172 | framePadding, 173 | circleSpace, 174 | circleSize 175 | ) 176 | 177 | const parentFrame = new Container( 178 | `Parent Frame`, 179 | frameDirection, 180 | framePadding, 181 | 70, 182 | 'AUTO', 183 | 'AUTO' 184 | ).createContainer() 185 | 186 | parentFrame.appendChild(tints) 187 | parentFrame.appendChild(shades) 188 | selectFrame(parentFrame) 189 | figma.closePlugin('Tints and shades generated') 190 | } else if (hue) { 191 | const hues = generateHues( 192 | color, 193 | hueInterval, 194 | frameDirection, 195 | framePadding, 196 | circleSpace, 197 | circleSize, 198 | tintForHue, 199 | shadeForHue, 200 | tintForHueInterval, 201 | shadeForHueInterval 202 | ) 203 | selectFrame(hues) 204 | figma.closePlugin('Hues generated') 205 | } else if (tint) { 206 | const tints = generateTints( 207 | color, 208 | tintInterval, 209 | frameDirection, 210 | framePadding, 211 | circleSpace, 212 | circleSize 213 | ) 214 | selectFrame(tints) 215 | figma.closePlugin('Tints generated') 216 | } else if (shade) { 217 | const shades = generateShades( 218 | color, 219 | shadeInterval, 220 | frameDirection, 221 | framePadding, 222 | circleSpace, 223 | circleSize 224 | ) 225 | 226 | selectFrame(shades) 227 | figma.closePlugin('Shades generated') 228 | } else { 229 | figma.closePlugin('No option selected') 230 | } 231 | }) 232 | once('CANCEL', function () { 233 | figma.closePlugin('okay, bye ✌🏾') 234 | }) 235 | showUI({ 236 | width: 400, 237 | height: 335, 238 | }) 239 | } 240 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { EventHandler } from '@create-figma-plugin/utilities' 2 | 3 | export interface GenerateHandler extends EventHandler { 4 | name: 'GENERATE' 5 | handler: (inputValues: InputValues | any) => void 6 | } 7 | 8 | export interface CancelHandler extends EventHandler { 9 | name: 'CANCEL' 10 | handler: () => void 11 | } 12 | 13 | export interface InputValues { 14 | colorCode: string 15 | hue: boolean 16 | hueInterval: number 17 | tintForHue: boolean 18 | tintForHueInterval: number 19 | shadeForHue: boolean 20 | shadeForHueInterval: number 21 | tint: boolean 22 | tintInterval: number 23 | shade: boolean 24 | shadeInterval: number 25 | } 26 | -------------------------------------------------------------------------------- /src/ui.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | .container { 8 | height: 100%; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: space-between; 12 | } 13 | 14 | .section { 15 | padding: 10px; 16 | display: flex; 17 | flex-direction: column; 18 | gap: 10px; 19 | } 20 | 21 | .section_2 { 22 | gap: 0; 23 | } 24 | 25 | .section_title { 26 | padding: 0 8px; 27 | font-size: 11px; 28 | font-weight: 600; 29 | line-height: 16px; 30 | } 31 | 32 | .color_input { 33 | line-height: 16px; 34 | } 35 | 36 | .setting_container { 37 | width: 100%; 38 | height: 32px; 39 | padding: 0 8px; 40 | display: flex; 41 | align-items: center; 42 | justify-content: space-between; 43 | gap: 20px; 44 | flex: 1 0; 45 | } 46 | 47 | .numeric_input { 48 | width: 50px; 49 | text-align: center; 50 | } 51 | 52 | .right_content { 53 | display: flex; 54 | align-items: center; 55 | justify-content: space-between; 56 | gap: 10px; 57 | } 58 | 59 | .right_content_2 { 60 | width: 250px; 61 | } 62 | 63 | .action_container { 64 | width: 100%; 65 | display: flex; 66 | flex-direction: row-reverse; 67 | align-items: center; 68 | justify-content: center; 69 | gap: 20px; 70 | } 71 | 72 | .button { 73 | border-radius: 3px !important; 74 | padding-right: 30px !important; 75 | padding-left: 30px !important; 76 | } 77 | 78 | .footer_container { 79 | text-align: center; 80 | margin-bottom: 5px; 81 | } 82 | 83 | .flexcode { 84 | margin-bottom: 5px; 85 | } 86 | 87 | .tanzania { 88 | color: var(--figma-color-text-tertiary); 89 | } 90 | 91 | .w-100{ 92 | width: 100%; 93 | } -------------------------------------------------------------------------------- /src/ui.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Checkbox, 4 | Divider, 5 | Dropdown, 6 | DropdownOption, 7 | Link, 8 | render, 9 | Text, 10 | TextboxColor, 11 | TextboxNumeric, 12 | } from '@create-figma-plugin/ui' 13 | import { emit } from '@create-figma-plugin/utilities' 14 | import { Fragment, h, JSX } from 'preact' 15 | import { useCallback, useState } from 'preact/hooks' 16 | import { CancelHandler, GenerateHandler, InputValues } from './types' 17 | import '!./ui.css' 18 | 19 | const Plugin = () => { 20 | const generateOption1 = 'Generate Hues' 21 | const generateOption2 = 'Generate Tints or Shades' 22 | 23 | const [color, setColor] = useState('F221B0') 24 | const [generateOption, setGenerateOption] = useState(generateOption1) 25 | const [hue, setHue] = useState(true) 26 | const [hueInterval, setHueInterval] = useState('10') 27 | const [tintForHue, setTintForHue] = useState(false) 28 | const [tintForHueInterval, setTintForHueInterval] = useState('10') 29 | const [shadeForHue, setShadeForHue] = useState(false) 30 | const [shadeForHueInterval, setShadeForHueInterval] = useState('10') 31 | const [tint, setTint] = useState(false) 32 | const [tintInterval, setTintInterval] = useState('10') 33 | const [shade, setShade] = useState(false) 34 | const [shadeInterval, setShadeInterval] = useState('10') 35 | 36 | const generateOptionsList: Array = [ 37 | { value: generateOption1 }, 38 | { value: generateOption2 }, 39 | ] 40 | 41 | const handleHexColorInput = (event: JSX.TargetedEvent) => { 42 | const newHexColor = event.currentTarget.value 43 | setColor(newHexColor) 44 | } 45 | 46 | const handleGenerateOptionChange = ( 47 | event: JSX.TargetedEvent 48 | ) => { 49 | const newOptionValue = event.currentTarget.value 50 | setGenerateOption(newOptionValue) 51 | setTintForHue(false) 52 | setShadeForHue(false) 53 | setTint(false) 54 | setShade(false) 55 | setHue(!hue) 56 | } 57 | 58 | const handleHueInterval = (event: JSX.TargetedEvent) => { 59 | const newHueInterval = event.currentTarget.value 60 | setHueInterval(newHueInterval) 61 | } 62 | 63 | const handleTintForHue = (event: JSX.TargetedEvent) => { 64 | const newTintForHue = event.currentTarget.checked 65 | setTintForHue(newTintForHue) 66 | } 67 | 68 | const handleTintForHueInterval = ( 69 | event: JSX.TargetedEvent 70 | ) => { 71 | const newTintForHueInterval = event.currentTarget.value 72 | setTintForHueInterval(newTintForHueInterval) 73 | } 74 | 75 | const handleShadeForHue = (event: JSX.TargetedEvent) => { 76 | const newShadeForHue = event.currentTarget.checked 77 | setShadeForHue(newShadeForHue) 78 | } 79 | 80 | const handleShadeForHueInterval = ( 81 | event: JSX.TargetedEvent 82 | ) => { 83 | const newShadeForHueInterval = event.currentTarget.value 84 | setShadeForHueInterval(newShadeForHueInterval) 85 | } 86 | 87 | const handleTint = (event: JSX.TargetedEvent) => { 88 | const newTint = event.currentTarget.checked 89 | setTint(newTint) 90 | } 91 | 92 | const handleTintInterval = (event: JSX.TargetedEvent) => { 93 | const newTintInterval = event.currentTarget.value 94 | setTintInterval(newTintInterval) 95 | } 96 | 97 | const handleShade = (event: JSX.TargetedEvent) => { 98 | const newShade = event.currentTarget.checked 99 | setShade(newShade) 100 | } 101 | 102 | const handleShadeInterval = (event: JSX.TargetedEvent) => { 103 | const newShadeInterval = event.currentTarget.value 104 | setShadeInterval(newShadeInterval) 105 | } 106 | 107 | // Add all input values to an object 108 | const inputValues: InputValues = { 109 | colorCode: color, 110 | hue, 111 | hueInterval: Number(hueInterval), 112 | tintForHue, 113 | tintForHueInterval: Number(tintForHueInterval), 114 | shadeForHue, 115 | shadeForHueInterval: Number(shadeForHueInterval), 116 | tint, 117 | tintInterval: Number(tintInterval), 118 | shade, 119 | shadeInterval: Number(shadeInterval), 120 | } 121 | 122 | // Action button handles 123 | const handleGenerateClick = useCallback(() => { 124 | emit('GENERATE', inputValues) 125 | }, [inputValues]) 126 | const handleCancelClick = useCallback(() => emit('CANCEL'), []) 127 | 128 | return ( 129 |
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 | } --------------------------------------------------------------------------------