├── .appcast.xml ├── .gitignore ├── .npmrc ├── README.md ├── assets ├── icon.png └── ui │ ├── index.html │ ├── main.css │ └── main.js ├── documentation-assets ├── documentation-animation.gif ├── documentation-ui.png ├── grid-small.png ├── parabolic-small.png ├── radial-small.png └── random-small.png ├── package.json ├── src ├── geometry │ ├── Line.js │ ├── Oval.js │ ├── Point.js │ └── Triangle.js ├── main │ ├── ColorManager.js │ ├── FieldGenerator.js │ ├── Init.js │ ├── ShapeExtractor.js │ └── TriangleField.js ├── manifest.json └── util │ ├── Color.js │ ├── Intersection.js │ └── Math.js └── triangle-field.sketchplugin └── Contents ├── Resources ├── icon.png ├── main.js ├── main.js.map └── ui │ ├── index.html │ ├── main.css │ └── main.js └── Sketch ├── Init.js ├── Init.js.map └── manifest.json /.appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artifacts 2 | plugin.sketchplugin 3 | 4 | # npm 5 | node_modules 6 | .npm 7 | npm-debug.log 8 | 9 | # mac 10 | .DS_Store 11 | 12 | # WebStorm 13 | .idea 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Triangle Fields 2 | A sketch plugin for generating a [Delaunay Tessellation](https://en.wikipedia.org/wiki/Delaunay_triangulation) from a selected shape. 3 | 4 | ![UI](documentation-assets/documentation-animation.gif) 5 | 6 | ## Installation 7 | - [Download the plugin repository](https://github.com/0la0/triangle-fields/archive/master.zip) 8 | - Unzip the repo, and double-click the `triangle-fields.sketchplugin` file 9 | - Set your parameters, select a shape, click 'Generate', and off you go! 10 | 11 | ![UI](documentation-assets/documentation-ui.png) 12 | 13 | ## Generation Options 14 | 15 | * ### Random 16 | An evenly distributed random field. 17 | ![UI](documentation-assets/random-small.png) 18 | 19 | * ### Random-Parabolic: 20 | A random distribution with a higher probability of points being generated near the centroid of the selected shape. 21 | ![UI](documentation-assets/parabolic-small.png) 22 | 23 | * ### Grid 24 | Points evenly distributed on a cartesian plane. 25 | ![UI](documentation-assets/grid-small.png) 26 | 27 | * ### Radial 28 | Points evenly distributed on a polar coordinate system with the center being the centroid of the selected shape. 29 | ![UI](documentation-assets/radial-small.png) 30 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0la0/triangle-fields/c665416ba69c6113e6327c59c1611971d620c6c1/assets/icon.png -------------------------------------------------------------------------------- /assets/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Triangle Field 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 |
16 |
Number of Points
17 |
18 |
19 | 22 | 27 |
28 | 29 |
30 | 33 | 38 |
39 |
40 |
41 | 42 | 43 |
44 |
Point Distribution
45 |
46 |
47 | 55 | 56 | 57 |
58 |
59 | 66 | 67 | 68 |
69 |
70 | 77 | 78 | 79 |
80 |
81 | 88 | 89 | 90 |
91 |
92 |
93 | 94 | 95 |
96 |
Color Distribution
97 |
98 |
99 | 107 | 108 | 109 |
110 |
111 | 118 | 119 | 120 |
121 |
122 |
123 | 124 | 125 | 126 |
127 | 128 |
129 | 130 |
131 |
Rendering Options
132 |
133 |
134 | 142 | 143 | 144 |
145 |
146 |
147 | 154 | 155 | 156 |
157 |
158 | 161 | 166 |
167 |
168 | 169 | 192 |
193 |
194 | 195 | 196 |
197 |
Colors
198 |
199 |
200 | 201 |
202 |
203 | 204 |
205 |
206 | 207 |
208 |
209 |

Crunching the Numbers

210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 | 218 | 219 | -------------------------------------------------------------------------------- /assets/ui/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | --color-background: #ECECEC; 3 | --color-font: #515151; 4 | --color-primary: #4892DE; 5 | --color-primary-light: #C1DDFA; 6 | --color-secondary: #BBCCFF; 7 | --color-tertiary: #E97171; 8 | --color-grey-dark: #333; 9 | --color-black: #121212; 10 | --color-grey: #515151; 11 | --color-white: #FFF; 12 | --color-grey-light: #AAA; 13 | --color-grey-lightest: #CCC; 14 | --animation-time: 0.2s; 15 | --animation-time-slow: 0.3s; 16 | --icon-size: 20px; 17 | } 18 | 19 | html, body { 20 | width: 100%; 21 | height: 100%; 22 | margin: 0; 23 | padding: 0; 24 | position: relative; 25 | font-family: 'Roboto', Helvetica, Arial, sans-serif; 26 | font-size: 18px; 27 | font-weight: 200; 28 | background-color: var(--color-background); 29 | color: var(--color-font); 30 | user-select: none; 31 | } 32 | 33 | h1 { 34 | margin: 0; 35 | padding: 0; 36 | } 37 | 38 | p { 39 | margin: 0; 40 | padding: 0; 41 | } 42 | 43 | input { 44 | background: none; 45 | background-color: var(--color-white); 46 | border: none; 47 | border-bottom: 1px solid var(--color-primary-light); 48 | padding: 2px; 49 | margin: 0 8px; 50 | color: var(--color-font); 51 | width: 90px; 52 | font-size: 18px; 53 | outline-style: none; 54 | font-weight: 200; 55 | transition: font-weight var(--antimation-time) ease; 56 | } 57 | input[type="number"]::-webkit-outer-spin-button, 58 | input[type="number"]::-webkit-inner-spin-button { 59 | -webkit-appearance: none; 60 | margin: 0; 61 | } 62 | input[type="number"] { 63 | -moz-appearance: textfield; 64 | position: relative; 65 | } 66 | input:focus { 67 | border-bottom: 1px solid var(--color-primary); 68 | font-weight: 500; 69 | } 70 | 71 | button { 72 | border: none; 73 | outline: none; 74 | background-color: Transparent; 75 | background-repeat:no-repeat; 76 | cursor: pointer; 77 | padding: 8px; 78 | border-radius: 4px; 79 | color: var(--color-white); 80 | background-color: var(--color-primary); 81 | box-shadow: 0px 1px 4px var(--color-grey-dark); 82 | transition: box-shadow var(--animation-time) ease; 83 | } 84 | 85 | .columns { 86 | display: flex; 87 | flex-direction: row; 88 | justify-content: flex-start; 89 | align-items: flex-start; 90 | } 91 | .column { 92 | display: flex; 93 | flex-direction: column; 94 | justify-content: flex-start; 95 | align-items: flex-start; 96 | } 97 | 98 | .card { 99 | border: none; 100 | background-color: var(--color-white); 101 | box-shadow: 0px 1px 4px var(--color-grey-dark); 102 | border-radius: 4px; 103 | margin: 8px; 104 | width: 325px; 105 | position: relative; 106 | } 107 | 108 | .card-label { 109 | padding: 8px; 110 | font-size: 1.2em; 111 | color: var(--color-black); 112 | background-color: var(--color-secondary); 113 | } 114 | 115 | .card-content { 116 | padding: 8px; 117 | } 118 | 119 | .row { 120 | display: flex; 121 | flex-direction: row; 122 | justify-content: flex-start; 123 | margin: 8px 0; 124 | } 125 | 126 | .row-space-between { 127 | display: flex; 128 | flex-direction: row; 129 | justify-content: space-between; 130 | } 131 | 132 | .loader { 133 | display: none; 134 | font-size: 1.4em; 135 | } 136 | 137 | .loader-active { 138 | position: absolute; 139 | left: 0; 140 | top: 0; 141 | right: 0; 142 | bottom: 0; 143 | color: var(--color-white); 144 | background-color: rgba(1, 1, 1, 0.5); 145 | display: flex; 146 | flex-direction: column; 147 | align-items: center; 148 | justify-content: space-around; 149 | } 150 | 151 | .loader-icon-row { 152 | display: flex; 153 | flex-direction: row; 154 | justify-content: space-between; 155 | align-items: center; 156 | padding: 0 32px; 157 | margin: 8px 0; 158 | } 159 | 160 | .loader-icon { 161 | width: 16px; 162 | height: 16px; 163 | animation: loader-icon--animate 1.4s infinite ease-in-out, loader-icon-rotate 8s infinite linear; 164 | opacity: 0; 165 | } 166 | 167 | .loader-icon-1 { 168 | width: 0; 169 | height: 0; 170 | border-top: 35px solid #69F16D; 171 | border-right: 30px solid transparent; 172 | transform: rotateZ(20deg); 173 | } 174 | 175 | .loader-icon-2 { 176 | width: 0; 177 | height: 0; 178 | border-left: 30px solid transparent; 179 | border-right: 15px solid transparent; 180 | border-top: 20px solid #6CD2EA; 181 | transform: rotateZ(-10deg); 182 | } 183 | 184 | .loader-icon-3 { 185 | width: 0; 186 | height: 0; 187 | border-bottom: 30px solid #F169ED; 188 | border-right: 25px solid transparent; 189 | transform: rotateZ(28deg); 190 | } 191 | 192 | @keyframes loader-icon--animate { 193 | 0% { 194 | opacity: 0; 195 | } 196 | 50% { 197 | opacity: 1; 198 | } 199 | 100% { 200 | opacity: 0; 201 | } 202 | } 203 | 204 | @keyframes loader-icon-rotate { 205 | 0% { 206 | transform: rotateZ(0deg); 207 | } 208 | 100% { 209 | transform: rotateZ(360deg); 210 | } 211 | } 212 | 213 | .sublabel { 214 | font-style: italic; 215 | font-size: 0.8em; 216 | } 217 | 218 | .row { 219 | display: flex; 220 | flex-direction: row; 221 | justify-content: flex-start; 222 | align-items: center; 223 | margin: 4px 0; 224 | } 225 | 226 | .shape-param { 227 | transform: scaleY(0); 228 | opacity: 0; 229 | transform-origin: center; 230 | margin: 0 8px; 231 | transition: transform var(--animation-time-slow) ease, opacity var(--animation-time) ease; 232 | } 233 | 234 | .shape-param-active { 235 | opacity: 1; 236 | transform: scaleY(1); 237 | } 238 | 239 | .color-container { 240 | display: flex; 241 | flex-direction: row; 242 | justify-content: flex-start; 243 | align-items: center; 244 | margin: 4px 0; 245 | } 246 | .color-row { 247 | display: flex; 248 | flex-direction: row; 249 | justify-content: flex-start; 250 | align-items: center; 251 | } 252 | 253 | .color-preview { 254 | width: 30px; 255 | height: 30px; 256 | border-radius: 15px; 257 | margin: 0 4px; 258 | } 259 | .color-input { 260 | width: 80px; 261 | } 262 | .color-input-invalid { 263 | border: 2px solid red; 264 | border-radius: 4px; 265 | } 266 | 267 | .checkbox-label { 268 | position: relative; 269 | display: block; 270 | height: 16px; 271 | width: 32px; 272 | background-color: var(--color-grey-light); 273 | border-radius: 16px; 274 | cursor: pointer; 275 | transition: background-color var(--animation-time) ease; 276 | } 277 | 278 | .checkbox-input:checked ~ .checkbox-label { 279 | background-color: var(--color-primary-light); 280 | } 281 | 282 | .checkbox-label { 283 | margin: 0 8px; 284 | } 285 | 286 | .checkbox-label:after { 287 | position: absolute; 288 | left: 0; 289 | top: -2px; 290 | display: block; 291 | width: var(--icon-size); 292 | height: var(--icon-size); 293 | border-radius: 50%; 294 | background-color: var(--color-white); 295 | content: ''; 296 | box-shadow: 0px 0px 4px var(--color-grey-dark); 297 | transition: left var(--animation-time) ease, background-color var(--animation-time) ease, box-shadow var(--animation-time) ease; 298 | } 299 | 300 | .checkbox-input:checked ~ label:after { 301 | left: 12px; 302 | background-color: var(--color-primary); 303 | box-shadow: 0px 0px 1px var(--color-grey-dark); 304 | } 305 | 306 | .input-hidden { 307 | display: none; 308 | } 309 | 310 | .radio-label { 311 | position: relative; 312 | display: block; 313 | height: var(--icon-size); 314 | width: var(--icon-size); 315 | border-radius: 50%; 316 | margin: 0 4px; 317 | cursor: pointer; 318 | transition: background-color var(--animation-time) ease; 319 | } 320 | 321 | .radio-input ~ .radio-label { 322 | border: 2px solid var(--color-grey-light); 323 | transition: border-color var(--animation-time) ease; 324 | } 325 | .radio-input:checked ~ .radio-label { 326 | border-color: var(--color-primary); 327 | } 328 | 329 | .radio-label:after { 330 | position: absolute; 331 | left: 0; 332 | top: 0; 333 | display: block; 334 | width: var(--icon-size); 335 | height: var(--icon-size); 336 | border-radius: 50%; 337 | content: ''; 338 | } 339 | 340 | .radio-input ~ label:after { 341 | transform: scale(0); 342 | transition: transform var(--animation-time-slow) ease; 343 | } 344 | 345 | .radio-input:checked ~ label:after { 346 | background-color: var(--color-primary); 347 | transform: scale(0.8); 348 | transform-origin: center; 349 | } 350 | 351 | .fab { 352 | width: 36px; 353 | height: 36px; 354 | border-radius: 50%; 355 | position: absolute; 356 | bottom: 0; 357 | right: 0; 358 | margin: 8px; 359 | display: block; 360 | } 361 | .fab:hover { 362 | box-shadow: 0px 2px 8px var(--color-grey-dark); 363 | } 364 | .fab:before { 365 | content: ''; 366 | background-color: var(--color-white); 367 | width: 22px; 368 | height: 2px; 369 | border-radius: 2px; 370 | position: absolute; 371 | display: block; 372 | top: 50%; 373 | left: 50%; 374 | transform: translate(-50%) rotateZ(90deg); 375 | } 376 | .fab:after { 377 | content: ''; 378 | background-color: var(--color-white); 379 | width: 22px; 380 | height: 2px; 381 | border-radius: 2px; 382 | position: absolute; 383 | display: block; 384 | top: 50%; 385 | left: 50%; 386 | transform: translate(-50%); 387 | } 388 | 389 | .generate-button { 390 | margin: 8px; 391 | width: 325px; 392 | font-size: 1.2em; 393 | } 394 | 395 | .remove-color-button { 396 | opacity: 0; 397 | position: relative; 398 | background-color: var(--color-tertiary); 399 | transition: opacity var(--animation-time) ease-in-out; 400 | width: 25px; 401 | height: 25px; 402 | margin: -4px 0 0 0; 403 | } 404 | .remove-color-button:before { 405 | width: 18px; 406 | transform: translate(-50%) rotateZ(45deg); 407 | } 408 | .remove-color-button:after { 409 | width: 18px; 410 | transform: translate(-50%) rotateZ(-45deg); 411 | } 412 | .color-container:hover > .remove-color-button { 413 | opacity: 1; 414 | } 415 | -------------------------------------------------------------------------------- /assets/ui/main.js: -------------------------------------------------------------------------------- 1 | const domIds = [ 2 | 'numEdgePoints', 3 | 'numFieldPoints', 4 | 'points', 5 | 'lines', 6 | 'triangles', 7 | 'generate', 8 | 'loader', 9 | 'pointRadius', 10 | 'pointRadiusContainer', 11 | 'lineWidth', 12 | 'lineWidthContainer', 13 | 'distributionRandom', 14 | 'distributionParabolic', 15 | 'distributionGrid', 16 | 'distributionRadial', 17 | 'colorContainer', 18 | 'addColor', 19 | 'colorDistributionDiscrete', 20 | 'colorDistributionContinuous' 21 | ]; 22 | const dom = {}; 23 | const params = { 24 | numEdgePoints: 20, 25 | numFieldPoints: 20, 26 | renderPoints: false, 27 | renderLines: false, 28 | renderTriangles: true, 29 | distribution: 'random', 30 | lineWidth: 4, 31 | pointRadius: 5, 32 | colors: [], 33 | colorDistribution: 'continuous' 34 | }; 35 | const LOADER_ACTIVE = 'loader-active'; 36 | const SHAPE_PARAM_ACTIVE = 'shape-param-active'; 37 | const TIME_DELAY = 50; 38 | const HEX_REGEX = /[0-9A-F]{6}$/; 39 | let colorPickerCount = 0; 40 | 41 | function callPlugin(actionName) { 42 | if (!actionName) { 43 | throw new Error('missing action name') 44 | } 45 | try { 46 | const payload = JSON.stringify([].slice.call(arguments)); 47 | console.log(payload); 48 | window['__skpm_sketchBridge'].callNative(payload); 49 | } catch(error) { 50 | closeLoader(); 51 | } 52 | } 53 | 54 | function isValidHexColor(hexValue) { 55 | if (!hexValue || typeof hexValue !== 'string') { return false } 56 | return HEX_REGEX.test(hexValue.toUpperCase()); 57 | } 58 | 59 | function getRandomColorComponent() { 60 | const str = Number(Math.floor(256 * Math.random())).toString(16).toUpperCase(); 61 | const leftPad = str.length > 1 ? '' : '0'; 62 | return `${leftPad}${str}`; 63 | } 64 | 65 | function getRandomColor() { 66 | return `${getRandomColorComponent()}${getRandomColorComponent()}${getRandomColorComponent()}`; 67 | } 68 | 69 | function addColorPicker(suppressDelete) { 70 | const id = `input${++colorPickerCount}`; 71 | const colorString = getRandomColor(); 72 | const inputElement = document.createElement('input'); 73 | const preview = document.createElement('div'); 74 | const label = document.createElement('label'); 75 | const container = document.createElement('div'); 76 | const closeButton = document.createElement('button'); 77 | const colorRow = document.createElement('div'); 78 | 79 | inputElement.addEventListener('change', event => { 80 | const hexString = event.target.value; 81 | const isValid = isValidHexColor(hexString); 82 | if (isValid) { 83 | preview.style.setProperty('background-color', `#${hexString}`); 84 | inputElement.classList.remove('color-input-invalid'); 85 | } else { 86 | inputElement.classList.add('color-input-invalid'); 87 | } 88 | }); 89 | closeButton.addEventListener('click', () => dom.colorContainer.removeChild(colorRow)); 90 | preview.classList.add('color-preview'); 91 | preview.style.setProperty('background-color', `#${colorString}`); 92 | inputElement.setAttribute('type', 'text'); 93 | inputElement.setAttribute('value', colorString); 94 | inputElement.setAttribute('id', id); 95 | inputElement.classList.add('color-input'); 96 | label.setAttribute('for', id); 97 | closeButton.classList.add('fab'); 98 | closeButton.classList.add('remove-color-button'); 99 | container.classList.add('color-container'); 100 | colorRow.classList.add('color-row'); 101 | container.appendChild(preview); 102 | container.appendChild(inputElement); 103 | container.appendChild(label); 104 | colorRow.appendChild(container); 105 | if (!suppressDelete) { 106 | container.appendChild(closeButton); 107 | } 108 | dom.colorContainer.appendChild(colorRow); 109 | } 110 | 111 | function getAllColors() { 112 | const elements = dom.colorContainer.getElementsByClassName('color-input'); 113 | return Array.prototype.slice.call(elements) 114 | .map(ele => ele.value); 115 | } 116 | 117 | function handleDistributionChange(event) { 118 | if (!event.target.checked) { return; } 119 | params.distribution = event.target.value; 120 | } 121 | 122 | function handleColorDistributionChange(event) { 123 | if (!event.target.checked) { return; } 124 | params.colorDistribution = event.target.value; 125 | } 126 | 127 | function init() { 128 | window.closeLoader = () => setTimeout(() => dom.loader.classList.remove(LOADER_ACTIVE), TIME_DELAY); 129 | domIds.forEach(key => dom[key] = document.getElementById(key)); 130 | dom.numEdgePoints.addEventListener('change', event => params.numEdgePoints = parseInt(event.target.value, 10)); 131 | dom.numFieldPoints.addEventListener('change', event => params.numFieldPoints = parseInt(event.target.value, 10)); 132 | // TODO: reimplement when we know about ovals in Sketch 52 133 | // dom.points.addEventListener('change', event => { 134 | // const val = event.target.checked; 135 | // params.renderPoints = val; 136 | // val ? 137 | // dom.pointRadiusContainer.classList.add(SHAPE_PARAM_ACTIVE) : 138 | // dom.pointRadiusContainer.classList.remove(SHAPE_PARAM_ACTIVE); 139 | // }); 140 | // dom.pointRadius.addEventListener('change', event => params.pointRadius = parseInt(event.target.value, 10)); 141 | dom.lines.addEventListener('change', event => { 142 | const val = event.target.checked; 143 | params.renderLines = val; 144 | val ? 145 | dom.lineWidthContainer.classList.add(SHAPE_PARAM_ACTIVE) : 146 | dom.lineWidthContainer.classList.remove(SHAPE_PARAM_ACTIVE); 147 | }); 148 | dom.lineWidth.addEventListener('change', event => params.lineWidth = parseInt(event.target.value, 10)); 149 | dom.triangles.addEventListener('change', event => params.renderTriangles = event.target.checked); 150 | 151 | dom.distributionRandom.addEventListener('change', handleDistributionChange); 152 | dom.distributionParabolic.addEventListener('change', handleDistributionChange); 153 | dom.distributionGrid.addEventListener('change', handleDistributionChange); 154 | dom.distributionRadial.addEventListener('change', handleDistributionChange); 155 | dom.colorDistributionDiscrete.addEventListener('change', handleColorDistributionChange); 156 | dom.colorDistributionContinuous.addEventListener('change', handleColorDistributionChange); 157 | dom.generate.addEventListener('click', () => { 158 | if (!params.renderPoints && !params.renderLines && !params.renderTriangles) { return; } 159 | const colors = getAllColors(); 160 | const colorsAreValid = colors.every(isValidHexColor); 161 | if (!colorsAreValid) { return; } 162 | params.colors = colors; 163 | dom.loader.classList.add(LOADER_ACTIVE); 164 | setTimeout(() => callPlugin('GENERATE_FIELD', JSON.stringify(params)), TIME_DELAY); 165 | }); 166 | dom.addColor.addEventListener('click', () => addColorPicker()); 167 | addColorPicker(true); 168 | addColorPicker(true); 169 | } 170 | document.addEventListener('DOMContentLoaded', init); 171 | -------------------------------------------------------------------------------- /documentation-assets/documentation-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0la0/triangle-fields/c665416ba69c6113e6327c59c1611971d620c6c1/documentation-assets/documentation-animation.gif -------------------------------------------------------------------------------- /documentation-assets/documentation-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0la0/triangle-fields/c665416ba69c6113e6327c59c1611971d620c6c1/documentation-assets/documentation-ui.png -------------------------------------------------------------------------------- /documentation-assets/grid-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0la0/triangle-fields/c665416ba69c6113e6327c59c1611971d620c6c1/documentation-assets/grid-small.png -------------------------------------------------------------------------------- /documentation-assets/parabolic-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0la0/triangle-fields/c665416ba69c6113e6327c59c1611971d620c6c1/documentation-assets/parabolic-small.png -------------------------------------------------------------------------------- /documentation-assets/radial-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0la0/triangle-fields/c665416ba69c6113e6327c59c1611971d620c6c1/documentation-assets/radial-small.png -------------------------------------------------------------------------------- /documentation-assets/random-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0la0/triangle-fields/c665416ba69c6113e6327c59c1611971d620c6c1/documentation-assets/random-small.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "triangle-field", 3 | "version": "2.0.1", 4 | "engines": { 5 | "sketch": ">=3.0" 6 | }, 7 | "skpm": { 8 | "name": "triangle-field", 9 | "manifest": "src/manifest.json", 10 | "main": "triangle-field.sketchplugin", 11 | "assets": [ 12 | "assets/**/*" 13 | ], 14 | "resources": [ 15 | "assets/ui/main.js" 16 | ] 17 | }, 18 | "scripts": { 19 | "build": "skpm-build", 20 | "watch": "skpm-build --watch", 21 | "start": "skpm-build --watch --run", 22 | "postinstall": "npm run build && skpm-link", 23 | "clean": "rm -rf triangle-field.sketchplugin" 24 | }, 25 | "devDependencies": { 26 | "@skpm/builder": "^0.5.2" 27 | }, 28 | "author": "Luke Anderson", 29 | "dependencies": { 30 | "cdt2d": "^1.0.0", 31 | "clean-pslg": "^1.1.2", 32 | "sketch-module-web-view": "^1.2.3" 33 | }, 34 | "repository": "https://github.com/0la0/triangle-fields", 35 | "description": "Generate Delaunay triangle tessellation inside a shape." 36 | } 37 | -------------------------------------------------------------------------------- /src/geometry/Line.js: -------------------------------------------------------------------------------- 1 | import colorManager from '../main/ColorManager'; 2 | 3 | function createLine(p1, p2, thickness, name) { 4 | const path = NSBezierPath.bezierPath(); 5 | path.moveToPoint(NSMakePoint(p1.getX(), p1.getY())); 6 | path.lineToPoint(NSMakePoint(p2.getX(), p2.getY())); 7 | 8 | const shape = MSShapeGroup.layerWithPath(MSPath.pathWithBezierPath(path)); 9 | const border = shape.style().addStylePartOfType(1); 10 | border.color = MSColor.colorWithRGBADictionary(colorManager.getRandomColor()); 11 | border.thickness = thickness; 12 | shape.name = name; 13 | return shape; 14 | } 15 | 16 | export default class Line { 17 | constructor(p1, p2, thickness, name) { 18 | this.p1 = p1; 19 | this.p2 = p2; 20 | this.thickness = thickness; 21 | this.name = name; 22 | this.id = [ ...this.p1.toArray(), ...this.p2.toArray() ] 23 | .sort((a, b) => a - b) 24 | .reduce((acc, num) => `${acc}${num}`, ''); 25 | } 26 | 27 | setName(name) { 28 | this.name = name; 29 | return this; 30 | } 31 | 32 | getId() { 33 | return this.id; 34 | } 35 | 36 | getShape() { 37 | return createLine(this.p1, this.p2, this.thickness, this.name); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/geometry/Oval.js: -------------------------------------------------------------------------------- 1 | import colorManager from '../main/ColorManager'; 2 | 3 | function createOval(centerPoint, radius, name) { 4 | const ovalShape = MSOvalShape.alloc().init(); 5 | ovalShape.frame = MSRect.rectWithRect(NSMakeRect(centerPoint.getX(), centerPoint.getY(), radius, radius)); 6 | const shapeGroup = MSShapeGroup.shapeWithPath(ovalShape); 7 | const fill = shapeGroup.style().addStylePartOfType(0); 8 | fill.color = MSColor.colorWithRGBADictionary(colorManager.getRandomColor()); 9 | shapeGroup.name = name || 'Point'; 10 | return shapeGroup; 11 | } 12 | 13 | export default class Oval { 14 | constructor(center, radius, name) { 15 | this.center = center.clone().addScalar(-radius / 2); 16 | this.radius = radius; 17 | this.name = name; 18 | } 19 | 20 | getShape() { 21 | return createOval(this.center, this.radius, this.name); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/geometry/Point.js: -------------------------------------------------------------------------------- 1 | 2 | export default class Point { 3 | constructor(x, y) { 4 | this.x = x || 0; 5 | this.y = y || 0; 6 | } 7 | 8 | getX() { 9 | return this.x; 10 | } 11 | 12 | getY() { 13 | return this.y; 14 | } 15 | 16 | add(p) { 17 | return new Point( 18 | this.x + p.x, 19 | this.y + p.y 20 | ); 21 | } 22 | 23 | multScalar(scalar) { 24 | return new Point( 25 | this.x * scalar, 26 | this.y * scalar 27 | ); 28 | } 29 | 30 | addScalar(scalar) { 31 | return new Point( 32 | this.x + scalar, 33 | this.y + scalar 34 | ); 35 | } 36 | 37 | toArray() { 38 | return [ this.x, this.y ]; 39 | } 40 | 41 | clone() { 42 | return new Point(this.x, this.y); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/geometry/Triangle.js: -------------------------------------------------------------------------------- 1 | import colorManager from '../main/ColorManager'; 2 | 3 | function createTriangle(p1, p2, p3, name) { 4 | const path = NSBezierPath.bezierPath(); 5 | path.moveToPoint(NSMakePoint(p1.getX(), p1.getY())); 6 | path.lineToPoint(NSMakePoint(p2.getX(), p2.getY())); 7 | path.lineToPoint(NSMakePoint(p3.getX(), p3.getY())); 8 | path.closePath(); 9 | const shape = MSShapeGroup.layerWithPath(MSPath.pathWithBezierPath(path)); 10 | shape.name = name; 11 | // const border = shape.style().addStylePartOfType(1); 12 | // border.color = MSColor.colorWithRGBADictionary(getRandomColor()); 13 | // border.thickness = 1; 14 | const fill = shape.style().addStylePartOfType(0); // `0` constant indicates that we need a `fill` part to be created 15 | fill.color = MSColor.colorWithRGBADictionary(colorManager.getRandomColor()); 16 | return shape; 17 | } 18 | 19 | export default class Triangle { 20 | constructor(p1, p2, p3, name) { 21 | this.p1 = p1; 22 | this.p2 = p2; 23 | this.p3 = p3; 24 | this.name = name; 25 | } 26 | 27 | getShape() { 28 | return createTriangle(this.p1, this.p2, this.p3, this.name); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/ColorManager.js: -------------------------------------------------------------------------------- 1 | import Color from '../util/Color'; 2 | 3 | class ColorManager { 4 | constructor(colors) { 5 | this.colors = []; 6 | } 7 | 8 | setFromHexList(hexList) { 9 | this.colors = hexList.map(Color.fromHex); 10 | } 11 | 12 | setGenerationMethod(generationMethod) { 13 | this.generationMethod = generationMethod; 14 | } 15 | 16 | getRandomColorIndex() { 17 | return Math.floor(this.colors.length * Math.random()); 18 | } 19 | 20 | generateColorFromContinuousSpace() { 21 | if (this.colors.length < 2) { 22 | return this.colors[0]; 23 | } 24 | const index1 = this.getRandomColorIndex(); 25 | let index2 = this.getRandomColorIndex(); 26 | while(index1 === index2) { 27 | index2 = this.getRandomColorIndex(); 28 | } 29 | return this.colors[index1].interpolateWith(this.colors[index2]); 30 | } 31 | 32 | generateColorFromDiscreteSpace() { 33 | return this.colors[this.getRandomColorIndex()]; 34 | } 35 | 36 | getRandomColor() { 37 | return this.generationMethod === 'continuous' ? 38 | this.generateColorFromContinuousSpace() : 39 | this.generateColorFromDiscreteSpace(); 40 | } 41 | } 42 | 43 | const instance = new ColorManager(); 44 | export default instance; 45 | -------------------------------------------------------------------------------- /src/main/FieldGenerator.js: -------------------------------------------------------------------------------- 1 | import Point from '../geometry/Point'; 2 | import { getRandomNum, getCentroid } from '../util/Math'; 3 | import { pointIsInsidePolygon } from '../util/Intersection'; 4 | 5 | function getBounds(points) { 6 | const dimensions = { 7 | minX: Number.MAX_VALUE, 8 | maxX: Number.MIN_VALUE, 9 | minY: Number.MAX_VALUE, 10 | maxY: Number.MIN_VALUE, 11 | }; 12 | const fieldDimensions = points.reduce((dims, point) => { 13 | const x = point.getX(); 14 | const y = point.getY(); 15 | if (x < dims.minX) { dims.minX = x; } 16 | if (x > dims.maxX) { dims.maxX = x; } 17 | if (y < dims.minY) { dims.minY = y; } 18 | if (y > dims.maxY) { dims.maxY = y; } 19 | return dims; 20 | }, dimensions); 21 | fieldDimensions.rangeX = fieldDimensions.maxX - fieldDimensions.minX; 22 | fieldDimensions.rangeY = fieldDimensions.maxY - fieldDimensions.minY; 23 | return fieldDimensions; 24 | } 25 | 26 | function createRandomField(numFieldPoints, edgePoints) { 27 | const centroid = getCentroid(edgePoints); 28 | const bounds = getBounds(edgePoints); 29 | let points = []; 30 | while (points.length < numFieldPoints) { 31 | const potentialPoint = new Point( 32 | getRandomNum(bounds.rangeX) + bounds.minX, 33 | getRandomNum(bounds.rangeY) + bounds.minY, 34 | ); 35 | if (pointIsInsidePolygon(edgePoints, potentialPoint)) { 36 | points.push(potentialPoint); 37 | } 38 | } 39 | return { points }; 40 | } 41 | 42 | function createGaussianField(numFieldPoints, edgePoints) { 43 | const centroid = getCentroid(edgePoints); 44 | const bounds = getBounds(edgePoints); 45 | const maxRadius = Math.max( 46 | bounds.maxX - centroid.x, 47 | centroid.x - bounds.minX, 48 | bounds.maxY - centroid.x, 49 | centroid.y - bounds.minY, 50 | ); 51 | let points = []; 52 | while (points.length < numFieldPoints) { 53 | const angle = 2 * Math.PI * Math.random(); 54 | const radius = maxRadius * Math.pow(Math.random(), 1.5); 55 | const potentialPoint = new Point( 56 | radius * Math.cos(angle) + centroid.x, 57 | radius * Math.sin(angle) + centroid.y, 58 | ); 59 | if (pointIsInsidePolygon(edgePoints, potentialPoint)) { 60 | points.push(potentialPoint); 61 | } 62 | } 63 | return { points }; 64 | } 65 | 66 | function createSquareField(numFieldPoints, edgePoints) { 67 | const bounds = getBounds(edgePoints); 68 | const numRows = 10; 69 | const numColumns = 10; 70 | const xStride = bounds.rangeX / numColumns; 71 | const yStride = bounds.rangeY / numRows; 72 | let points = []; 73 | for (let i = 0; i < numColumns; i++) { 74 | for (let j = 0; j < numRows; j++) { 75 | const potentialPoint = new Point( 76 | i * xStride + bounds.minX, 77 | j * yStride + bounds.minY, 78 | ); 79 | if (pointIsInsidePolygon(edgePoints, potentialPoint)) { 80 | points.push(potentialPoint); 81 | } 82 | } 83 | } 84 | return { points }; 85 | } 86 | 87 | function createRadialField(numFieldPoints, edgePoints) { 88 | const centroid = getCentroid(edgePoints); 89 | const bounds = getBounds(edgePoints); 90 | const maxRadius = Math.max( 91 | bounds.maxX - centroid.x, 92 | centroid.x - bounds.minX, 93 | bounds.maxY - centroid.x, 94 | centroid.y - bounds.minY, 95 | ); 96 | const TWO_PI = 2 * Math.PI; 97 | const numSectors = 8; 98 | const numSegments = 5; 99 | const thetaStride = TWO_PI / numSectors; 100 | const radiusStride = maxRadius / numSegments; 101 | let points = [ centroid.clone(), ]; 102 | for (let t = 0; t < numSectors; t++) { 103 | for (let r = 1; r < numSegments; r++) { 104 | const theta = t * thetaStride; 105 | const radius = r * radiusStride; 106 | const potentialPoint = new Point( 107 | radius * Math.cos(theta) + centroid.x, 108 | radius * Math.sin(theta) + centroid.y 109 | ); 110 | if (pointIsInsidePolygon(edgePoints, potentialPoint)) { 111 | points.push(potentialPoint); 112 | } 113 | } 114 | } 115 | return { points }; 116 | } 117 | 118 | const strategy = { 119 | random: createRandomField, 120 | parabolic: createGaussianField, 121 | grid: createSquareField, 122 | radial: createRadialField, 123 | }; 124 | 125 | function distributionStrategy(key) { 126 | return strategy[key] || createRandomField; 127 | } 128 | 129 | export default distributionStrategy; 130 | -------------------------------------------------------------------------------- /src/main/Init.js: -------------------------------------------------------------------------------- 1 | import { UI } from 'sketch'; 2 | import BrowserWindow from 'sketch-module-web-view'; 3 | import createTriangleField from './TriangleField'; 4 | 5 | const UI_WIDTH = 684; 6 | const UI_PATH = './ui/index.html'; 7 | const GENERATE_FIELD = 'GENERATE_FIELD'; 8 | const CLOSE_LOADER = 'closeLoader()'; 9 | const WEBVIEW_ID = 'triangle-field-ui'; 10 | 11 | export default function init(context) { 12 | const options = { 13 | identifier: WEBVIEW_ID, 14 | width: UI_WIDTH, 15 | show: false, 16 | }; 17 | let browserWindow = new BrowserWindow(options); 18 | const closeLoader = () => browserWindow.webContents.executeJavaScript(CLOSE_LOADER); 19 | 20 | browserWindow.webContents.on(GENERATE_FIELD, dto => { 21 | let params; 22 | try { 23 | params = JSON.parse(dto); 24 | } catch(error) { 25 | console.log('ERROR', error); 26 | context.document.showMessage('Error, check logs'); 27 | return; 28 | } 29 | const selection = NSDocumentController.sharedDocumentController().currentDocument().selectedLayers().layers(); 30 | if (selection.count() < 1) { 31 | context.document.showMessage('Select a shape!'); 32 | closeLoader(); 33 | return; 34 | } 35 | const sketchObject = selection.firstObject(); 36 | const page = NSDocumentController.sharedDocumentController().currentDocument().currentPage(); 37 | createTriangleField(page, sketchObject, params); 38 | closeLoader(); 39 | }); 40 | 41 | browserWindow.on('closed', () => browserWindow = null); 42 | browserWindow.once('ready-to-show', () => browserWindow.show()); 43 | browserWindow.loadURL(UI_PATH); 44 | } 45 | -------------------------------------------------------------------------------- /src/main/ShapeExtractor.js: -------------------------------------------------------------------------------- 1 | import Point from '../geometry/Point'; 2 | 3 | // TODO: figure out how to sequence all "pointAtIndex" into this 4 | export default function getPointsFromShape(shape, numFieldPoints) { 5 | const absoluteRect = shape.absoluteRect(); 6 | const frame = shape.frame(); 7 | const deltaX = absoluteRect.x() - frame.x(); 8 | const deltaY = absoluteRect.y() - frame.y(); 9 | const path = shape.pathInFrameWithTransforms(); 10 | const numPoints = path.elementCount(); 11 | const bezierPath = NSBezierPath.bezierPathWithPath(path); 12 | const length = Math.floor(bezierPath.length()); 13 | const stride = length / numFieldPoints; 14 | return new Array(numFieldPoints).fill(null) 15 | .map((n, i) => { 16 | const length = Math.floor(i * stride); 17 | const { x, y } = bezierPath.pointOnPathAtLength(length); 18 | return new Point(deltaX + x, deltaY + y); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/TriangleField.js: -------------------------------------------------------------------------------- 1 | import { Group } from 'sketch/dom'; 2 | import cdt2d from 'cdt2d'; 3 | import cleanPSLG from 'clean-pslg'; 4 | import distributionStrategy from './FieldGenerator'; 5 | import getPointsFromShape from './ShapeExtractor'; 6 | import Triangle from '../geometry/Triangle'; 7 | import Oval from '../geometry/Oval'; 8 | import Line from '../geometry/Line'; 9 | import colorManager from './ColorManager'; 10 | 11 | export default function createTriangleField(page, shape, params) { 12 | const { 13 | numEdgePoints, 14 | numFieldPoints, 15 | renderPoints, 16 | renderLines, 17 | renderTriangles, 18 | distribution, 19 | lineWidth, 20 | pointRadius, 21 | colors, 22 | colorDistribution, 23 | } = params; 24 | 25 | const edgePoints = getPointsFromShape(shape, numEdgePoints); 26 | const distributionFn = distributionStrategy(distribution); 27 | const pointField = distributionFn(numFieldPoints, edgePoints); 28 | const allPoints = edgePoints.concat(pointField.points); 29 | const pointArray = allPoints.map(point => point.toArray()); 30 | const edgeIndices = edgePoints.map((point, index, arr) => [ index, (index + 1) % arr.length ]); 31 | cleanPSLG(pointArray, edgeIndices) 32 | const triangleIndices = cdt2d(pointArray, edgeIndices, { exterior: false }); 33 | 34 | const trianglePoints = triangleIndices.map(([i0, i1, i2]) => ({ 35 | p0: allPoints[i0], 36 | p1: allPoints[i1], 37 | p2: allPoints[i2], 38 | })); 39 | 40 | const parentGroup = new Group({ 41 | parent: page, 42 | name: 'triangle field' 43 | }); 44 | 45 | colorManager.setFromHexList(colors); 46 | colorManager.setGenerationMethod(colorDistribution); 47 | 48 | if (renderTriangles) { 49 | const triangleLayers = trianglePoints 50 | .map(({ p0, p1, p2}, index) => new Triangle(p0, p1, p2, `Triangle-${index}`)) 51 | .map(triangle => triangle.getShape()); 52 | const triangleGroup = new Group({ 53 | parent: parentGroup, 54 | name: 'triangles', 55 | layers: triangleLayers, 56 | }); 57 | triangleGroup.adjustToFit(); 58 | } 59 | 60 | if (renderLines) { 61 | const lineLayers = trianglePoints 62 | .map(({ p0, p1, p2 }) => { 63 | const line0 = new Line(p0, p1, lineWidth, ''); 64 | const line1 = new Line(p1, p2, lineWidth, ''); 65 | const line2 = new Line(p2, p0, lineWidth, ''); 66 | return [ line0, line1, line2 ]; 67 | }) 68 | .reduce((uniqueList, triangleLines) => { 69 | triangleLines.forEach(line => { 70 | if (!uniqueList.some(_line => _line.getId() === line.getId())) { 71 | uniqueList.push(line); 72 | } 73 | }); 74 | return uniqueList; 75 | }, []) 76 | .map((line, index) => line.setName(`Line-${index}`).getShape()); 77 | const lineGroup = new Group({ 78 | parent: parentGroup, 79 | name: 'lines', 80 | layers: lineLayers, 81 | }); 82 | lineGroup.adjustToFit(); 83 | } 84 | 85 | if (renderPoints) { 86 | const pointLayers = allPoints 87 | .map((p, index) => new Oval(p, pointRadius, `Point-${index}`)) 88 | .map(oval => oval.getShape()); 89 | const pointGroup = new Group({ 90 | parent: parentGroup, 91 | name: 'points', 92 | layers: pointLayers, 93 | }); 94 | pointGroup.adjustToFit(); 95 | } 96 | 97 | parentGroup.adjustToFit(); 98 | } 99 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "compatibleVersion": 3, 3 | "bundleVersion": 1, 4 | "version": "2.0.1", 5 | "compatibleVersion": "52.0", 6 | "icon": "icon.png", 7 | "description": "Generate Delaunay triangle tessellation inside a shape.", 8 | "commands": [ 9 | { 10 | "name": "triangle-fields", 11 | "identifier": "triangle-fields-identifier", 12 | "script": "./main/Init.js" 13 | } 14 | ], 15 | "menu": { 16 | "title": "triangle-fields", 17 | "items": [ "triangle-fields-identifier" ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/util/Color.js: -------------------------------------------------------------------------------- 1 | 2 | // https://en.wikipedia.org/wiki/Smoothstep 3 | function smoothstep(x) { 4 | return 6 * Math.pow(x, 5) - 15 * Math.pow(x, 4) + 10 * Math.pow(x, 3); 5 | } 6 | 7 | function getValueBetweenTwoPoints(y1, y2, percent) { 8 | const range = y2 - y1; 9 | return y1 + range * percent; 10 | } 11 | 12 | export default class Color { 13 | constructor(r, g, b) { 14 | this.r = r; 15 | this.g = g; 16 | this.b = b; 17 | this.a = 1; 18 | } 19 | 20 | interpolateWith(color) { 21 | const percent = smoothstep(Math.random()); 22 | const r = getValueBetweenTwoPoints(this.r, color.r, percent); 23 | const g = getValueBetweenTwoPoints(this.g, color.g, percent); 24 | const b = getValueBetweenTwoPoints(this.b, color.b, percent); 25 | return new Color(r, g, b); 26 | } 27 | 28 | static fromHex(hexValue) { 29 | try { 30 | const r = parseInt(hexValue.substring(0, 2), 16) / 255; 31 | const g = parseInt(hexValue.substring(2, 4), 16) / 255; 32 | const b = parseInt(hexValue.substring(4, 6), 16) / 255; 33 | return new Color(r, g, b); 34 | } 35 | catch(error) { 36 | console.log('error', error); 37 | return new Color(1, 0, 0); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/util/Intersection.js: -------------------------------------------------------------------------------- 1 | import Point from '../geometry/Point'; 2 | 3 | const ORIENTATION = { 4 | COLINEAR: 'COLINEAR', 5 | CLOCKWISE: 'CLOCKWISE', 6 | COUNTERCLOCKWISE: 'COUNTERCLOCKWISE' 7 | }; 8 | 9 | function isOnSegment(p, q, r) { 10 | return q.x <= Math.max(p.x, r.x) && 11 | q.x >= Math.min(p.x, r.x) && 12 | q.y <= Math.max(p.y, r.y) && 13 | q.y >= Math.min(p.y, r.y); 14 | } 15 | 16 | function getOrientation(p, q, r) { 17 | const orientation = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y); 18 | if (orientation === 0) { 19 | return ORIENTATION.COLINEAR; 20 | } 21 | return (orientation > 0) ? ORIENTATION.CLOCKWISE : ORIENTATION.COUNTERCLOCKWISE; 22 | } 23 | 24 | function linesDoIntersect(p1, q1, p2, q2) { 25 | const o1 = getOrientation(p1, q1, p2); 26 | const o2 = getOrientation(p1, q1, q2); 27 | const o3 = getOrientation(p2, q2, p1); 28 | const o4 = getOrientation(p2, q2, q1); 29 | if (o1 !== o2 && o3 !== o4) { 30 | return true; 31 | } 32 | if (o1 === ORIENTATION.COLINEAR && isOnSegment(p1, p2, q1)) { return true; } 33 | if (o2 === ORIENTATION.COLINEAR && isOnSegment(p1, q2, q1)) { return true; } 34 | if (o3 === ORIENTATION.COLINEAR && isOnSegment(p2, p1, q2)) { return true; } 35 | if (o4 === ORIENTATION.COLINEAR && isOnSegment(p2, q1, q2)) { return true; } 36 | return false; 37 | } 38 | 39 | export function pointIsInsidePolygon(polygon, p) { 40 | if (polygon.length < 3) { 41 | throw new Error('Polygon must have at least 3 points'); 42 | } 43 | const n = polygon.length; 44 | const extreme = new Point(Number.MAX_VALUE, p.y); 45 | let count = 0; 46 | for (let i = 0; i < n; i++) { 47 | const nextIndex = (i + 1) % n; 48 | // p-etreme intersects with polygon[i]-polygon[nextIndex] 49 | if (linesDoIntersect(polygon[i], polygon[nextIndex], p, extreme)) { 50 | // p is colinear with i-next and it lies on segment 51 | if (getOrientation(polygon[i], p, polygon[nextIndex]) === ORIENTATION.COLINEAR) { 52 | return isOnSegment(polygon[i], p, polygon[nextIndex]); 53 | } 54 | count++; 55 | } 56 | } 57 | return count % 2 == 1; // odd number of intersections 58 | } 59 | -------------------------------------------------------------------------------- /src/util/Math.js: -------------------------------------------------------------------------------- 1 | import Point from '../geometry/Point'; 2 | 3 | function getPosNeg() { 4 | return Math.random() < 0.5 ? -1 : 1; 5 | } 6 | 7 | export function getRandomNum(mult) { 8 | return mult * Math.random(); 9 | } 10 | 11 | export function getCentroid(points) { 12 | if (!points || !points.length) { return new Point(0, 0); } 13 | const sum = points.reduce((s, p) => s.add(p), new Point(0, 0)); 14 | return sum.multScalar(1 / points.length); 15 | } 16 | -------------------------------------------------------------------------------- /triangle-field.sketchplugin/Contents/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0la0/triangle-fields/c665416ba69c6113e6327c59c1611971d620c6c1/triangle-field.sketchplugin/Contents/Resources/icon.png -------------------------------------------------------------------------------- /triangle-field.sketchplugin/Contents/Resources/main.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 40 | /******/ } 41 | /******/ }; 42 | /******/ 43 | /******/ // define __esModule on exports 44 | /******/ __webpack_require__.r = function(exports) { 45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 47 | /******/ } 48 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 49 | /******/ }; 50 | /******/ 51 | /******/ // create a fake namespace object 52 | /******/ // mode & 1: value is a module id, require it 53 | /******/ // mode & 2: merge all properties of value into the ns 54 | /******/ // mode & 4: return value when already ns object 55 | /******/ // mode & 8|1: behave like require 56 | /******/ __webpack_require__.t = function(value, mode) { 57 | /******/ if(mode & 1) value = __webpack_require__(value); 58 | /******/ if(mode & 8) return value; 59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 60 | /******/ var ns = Object.create(null); 61 | /******/ __webpack_require__.r(ns); 62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 64 | /******/ return ns; 65 | /******/ }; 66 | /******/ 67 | /******/ // getDefaultExport function for compatibility with non-harmony modules 68 | /******/ __webpack_require__.n = function(module) { 69 | /******/ var getter = module && module.__esModule ? 70 | /******/ function getDefault() { return module['default']; } : 71 | /******/ function getModuleExports() { return module; }; 72 | /******/ __webpack_require__.d(getter, 'a', getter); 73 | /******/ return getter; 74 | /******/ }; 75 | /******/ 76 | /******/ // Object.prototype.hasOwnProperty.call 77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 78 | /******/ 79 | /******/ // __webpack_public_path__ 80 | /******/ __webpack_require__.p = ""; 81 | /******/ 82 | /******/ 83 | /******/ // Load entry module and return exports 84 | /******/ return __webpack_require__(__webpack_require__.s = "./assets/ui/main.js"); 85 | /******/ }) 86 | /************************************************************************/ 87 | /******/ ({ 88 | 89 | /***/ "./assets/ui/main.js": 90 | /*!***************************!*\ 91 | !*** ./assets/ui/main.js ***! 92 | \***************************/ 93 | /*! no static exports found */ 94 | /***/ (function(module, exports) { 95 | 96 | var domIds = ['numEdgePoints', 'numFieldPoints', 'points', 'lines', 'triangles', 'generate', 'loader', 'pointRadius', 'pointRadiusContainer', 'lineWidth', 'lineWidthContainer', 'distributionRandom', 'distributionParabolic', 'distributionGrid', 'distributionRadial', 'colorContainer', 'addColor', 'colorDistributionDiscrete', 'colorDistributionContinuous']; 97 | var dom = {}; 98 | var params = { 99 | numEdgePoints: 20, 100 | numFieldPoints: 20, 101 | renderPoints: false, 102 | renderLines: false, 103 | renderTriangles: true, 104 | distribution: 'random', 105 | lineWidth: 4, 106 | pointRadius: 5, 107 | colors: [], 108 | colorDistribution: 'continuous' 109 | }; 110 | var LOADER_ACTIVE = 'loader-active'; 111 | var SHAPE_PARAM_ACTIVE = 'shape-param-active'; 112 | var TIME_DELAY = 50; 113 | var HEX_REGEX = /[0-9A-F]{6}$/; 114 | var colorPickerCount = 0; 115 | 116 | function callPlugin(actionName) { 117 | if (!actionName) { 118 | throw new Error('missing action name'); 119 | } 120 | 121 | try { 122 | var payload = JSON.stringify([].slice.call(arguments)); 123 | console.log(payload); 124 | window['__skpm_sketchBridge'].callNative(payload); 125 | } catch (error) { 126 | closeLoader(); 127 | } 128 | } 129 | 130 | function isValidHexColor(hexValue) { 131 | if (!hexValue || typeof hexValue !== 'string') { 132 | return false; 133 | } 134 | 135 | return HEX_REGEX.test(hexValue.toUpperCase()); 136 | } 137 | 138 | function getRandomColorComponent() { 139 | var str = Number(Math.floor(256 * Math.random())).toString(16).toUpperCase(); 140 | var leftPad = str.length > 1 ? '' : '0'; 141 | return "".concat(leftPad).concat(str); 142 | } 143 | 144 | function getRandomColor() { 145 | return "".concat(getRandomColorComponent()).concat(getRandomColorComponent()).concat(getRandomColorComponent()); 146 | } 147 | 148 | function addColorPicker(suppressDelete) { 149 | var id = "input".concat(++colorPickerCount); 150 | var colorString = getRandomColor(); 151 | var inputElement = document.createElement('input'); 152 | var preview = document.createElement('div'); 153 | var label = document.createElement('label'); 154 | var container = document.createElement('div'); 155 | var closeButton = document.createElement('button'); 156 | var colorRow = document.createElement('div'); 157 | inputElement.addEventListener('change', function (event) { 158 | var hexString = event.target.value; 159 | var isValid = isValidHexColor(hexString); 160 | 161 | if (isValid) { 162 | preview.style.setProperty('background-color', "#".concat(hexString)); 163 | inputElement.classList.remove('color-input-invalid'); 164 | } else { 165 | inputElement.classList.add('color-input-invalid'); 166 | } 167 | }); 168 | closeButton.addEventListener('click', function () { 169 | return dom.colorContainer.removeChild(colorRow); 170 | }); 171 | preview.classList.add('color-preview'); 172 | preview.style.setProperty('background-color', "#".concat(colorString)); 173 | inputElement.setAttribute('type', 'text'); 174 | inputElement.setAttribute('value', colorString); 175 | inputElement.setAttribute('id', id); 176 | inputElement.classList.add('color-input'); 177 | label.setAttribute('for', id); 178 | closeButton.classList.add('fab'); 179 | closeButton.classList.add('remove-color-button'); 180 | container.classList.add('color-container'); 181 | colorRow.classList.add('color-row'); 182 | container.appendChild(preview); 183 | container.appendChild(inputElement); 184 | container.appendChild(label); 185 | colorRow.appendChild(container); 186 | 187 | if (!suppressDelete) { 188 | container.appendChild(closeButton); 189 | } 190 | 191 | dom.colorContainer.appendChild(colorRow); 192 | } 193 | 194 | function getAllColors() { 195 | var elements = dom.colorContainer.getElementsByClassName('color-input'); 196 | return Array.prototype.slice.call(elements).map(function (ele) { 197 | return ele.value; 198 | }); 199 | } 200 | 201 | function handleDistributionChange(event) { 202 | if (!event.target.checked) { 203 | return; 204 | } 205 | 206 | params.distribution = event.target.value; 207 | } 208 | 209 | function handleColorDistributionChange(event) { 210 | if (!event.target.checked) { 211 | return; 212 | } 213 | 214 | params.colorDistribution = event.target.value; 215 | } 216 | 217 | function init() { 218 | window.closeLoader = function () { 219 | return setTimeout(function () { 220 | return dom.loader.classList.remove(LOADER_ACTIVE); 221 | }, TIME_DELAY); 222 | }; 223 | 224 | domIds.forEach(function (key) { 225 | return dom[key] = document.getElementById(key); 226 | }); 227 | dom.numEdgePoints.addEventListener('change', function (event) { 228 | return params.numEdgePoints = parseInt(event.target.value, 10); 229 | }); 230 | dom.numFieldPoints.addEventListener('change', function (event) { 231 | return params.numFieldPoints = parseInt(event.target.value, 10); 232 | }); // TODO: reimplement when we know about ovals in Sketch 52 233 | // dom.points.addEventListener('change', event => { 234 | // const val = event.target.checked; 235 | // params.renderPoints = val; 236 | // val ? 237 | // dom.pointRadiusContainer.classList.add(SHAPE_PARAM_ACTIVE) : 238 | // dom.pointRadiusContainer.classList.remove(SHAPE_PARAM_ACTIVE); 239 | // }); 240 | // dom.pointRadius.addEventListener('change', event => params.pointRadius = parseInt(event.target.value, 10)); 241 | 242 | dom.lines.addEventListener('change', function (event) { 243 | var val = event.target.checked; 244 | params.renderLines = val; 245 | val ? dom.lineWidthContainer.classList.add(SHAPE_PARAM_ACTIVE) : dom.lineWidthContainer.classList.remove(SHAPE_PARAM_ACTIVE); 246 | }); 247 | dom.lineWidth.addEventListener('change', function (event) { 248 | return params.lineWidth = parseInt(event.target.value, 10); 249 | }); 250 | dom.triangles.addEventListener('change', function (event) { 251 | return params.renderTriangles = event.target.checked; 252 | }); 253 | dom.distributionRandom.addEventListener('change', handleDistributionChange); 254 | dom.distributionParabolic.addEventListener('change', handleDistributionChange); 255 | dom.distributionGrid.addEventListener('change', handleDistributionChange); 256 | dom.distributionRadial.addEventListener('change', handleDistributionChange); 257 | dom.colorDistributionDiscrete.addEventListener('change', handleColorDistributionChange); 258 | dom.colorDistributionContinuous.addEventListener('change', handleColorDistributionChange); 259 | dom.generate.addEventListener('click', function () { 260 | if (!params.renderPoints && !params.renderLines && !params.renderTriangles) { 261 | return; 262 | } 263 | 264 | var colors = getAllColors(); 265 | var colorsAreValid = colors.every(isValidHexColor); 266 | 267 | if (!colorsAreValid) { 268 | return; 269 | } 270 | 271 | params.colors = colors; 272 | dom.loader.classList.add(LOADER_ACTIVE); 273 | setTimeout(function () { 274 | return callPlugin('GENERATE_FIELD', JSON.stringify(params)); 275 | }, TIME_DELAY); 276 | }); 277 | dom.addColor.addEventListener('click', function () { 278 | return addColorPicker(); 279 | }); 280 | addColorPicker(true); 281 | addColorPicker(true); 282 | } 283 | 284 | document.addEventListener('DOMContentLoaded', init); 285 | 286 | /***/ }) 287 | 288 | /******/ }); 289 | //# sourceMappingURL=main.js.map -------------------------------------------------------------------------------- /triangle-field.sketchplugin/Contents/Resources/main.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./assets/ui/main.js"],"names":["domIds","dom","params","numEdgePoints","numFieldPoints","renderPoints","renderLines","renderTriangles","distribution","lineWidth","pointRadius","colors","colorDistribution","LOADER_ACTIVE","SHAPE_PARAM_ACTIVE","TIME_DELAY","HEX_REGEX","colorPickerCount","callPlugin","actionName","Error","payload","JSON","stringify","slice","call","arguments","console","log","window","callNative","error","closeLoader","isValidHexColor","hexValue","test","toUpperCase","getRandomColorComponent","str","Number","Math","floor","random","toString","leftPad","length","getRandomColor","addColorPicker","suppressDelete","id","colorString","inputElement","document","createElement","preview","label","container","closeButton","colorRow","addEventListener","event","hexString","target","value","isValid","style","setProperty","classList","remove","add","colorContainer","removeChild","setAttribute","appendChild","getAllColors","elements","getElementsByClassName","Array","prototype","map","ele","handleDistributionChange","checked","handleColorDistributionChange","init","setTimeout","loader","forEach","key","getElementById","parseInt","lines","val","lineWidthContainer","triangles","distributionRandom","distributionParabolic","distributionGrid","distributionRadial","colorDistributionDiscrete","colorDistributionContinuous","generate","colorsAreValid","every","addColor"],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,kDAA0C,gCAAgC;AAC1E;AACA;;AAEA;AACA;AACA;AACA,gEAAwD,kBAAkB;AAC1E;AACA,yDAAiD,cAAc;AAC/D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iDAAyC,iCAAiC;AAC1E,wHAAgH,mBAAmB,EAAE;AACrI;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;;AAGA;AACA;;;;;;;;;;;;AClFA,IAAMA,MAAM,GAAG,CACb,eADa,EAEb,gBAFa,EAGb,QAHa,EAIb,OAJa,EAKb,WALa,EAMb,UANa,EAOb,QAPa,EAQb,aARa,EASb,sBATa,EAUb,WAVa,EAWb,oBAXa,EAYb,oBAZa,EAab,uBAba,EAcb,kBAda,EAeb,oBAfa,EAgBb,gBAhBa,EAiBb,UAjBa,EAkBb,2BAlBa,EAmBb,6BAnBa,CAAf;AAqBA,IAAMC,GAAG,GAAG,EAAZ;AACA,IAAMC,MAAM,GAAG;AACbC,eAAa,EAAE,EADF;AAEbC,gBAAc,EAAE,EAFH;AAGbC,cAAY,EAAE,KAHD;AAIbC,aAAW,EAAE,KAJA;AAKbC,iBAAe,EAAE,IALJ;AAMbC,cAAY,EAAE,QAND;AAObC,WAAS,EAAE,CAPE;AAQbC,aAAW,EAAE,CARA;AASbC,QAAM,EAAE,EATK;AAUbC,mBAAiB,EAAE;AAVN,CAAf;AAYA,IAAMC,aAAa,GAAG,eAAtB;AACA,IAAMC,kBAAkB,GAAG,oBAA3B;AACA,IAAMC,UAAU,GAAG,EAAnB;AACA,IAAMC,SAAS,GAAG,cAAlB;AACA,IAAIC,gBAAgB,GAAG,CAAvB;;AAEA,SAASC,UAAT,CAAoBC,UAApB,EAAgC;AAC9B,MAAI,CAACA,UAAL,EAAiB;AACf,UAAM,IAAIC,KAAJ,CAAU,qBAAV,CAAN;AACD;;AACD,MAAI;AACF,QAAMC,OAAO,GAAGC,IAAI,CAACC,SAAL,CAAe,GAAGC,KAAH,CAASC,IAAT,CAAcC,SAAd,CAAf,CAAhB;AACAC,WAAO,CAACC,GAAR,CAAYP,OAAZ;AACAQ,UAAM,CAAC,qBAAD,CAAN,CAA8BC,UAA9B,CAAyCT,OAAzC;AACD,GAJD,CAIE,OAAMU,KAAN,EAAa;AACbC,eAAW;AACZ;AACF;;AAED,SAASC,eAAT,CAAyBC,QAAzB,EAAmC;AACjC,MAAI,CAACA,QAAD,IAAa,OAAOA,QAAP,KAAoB,QAArC,EAA+C;AAAE,WAAO,KAAP;AAAc;;AAC/D,SAAOlB,SAAS,CAACmB,IAAV,CAAeD,QAAQ,CAACE,WAAT,EAAf,CAAP;AACD;;AAED,SAASC,uBAAT,GAAmC;AACjC,MAAMC,GAAG,GAAGC,MAAM,CAACC,IAAI,CAACC,KAAL,CAAW,MAAMD,IAAI,CAACE,MAAL,EAAjB,CAAD,CAAN,CAAwCC,QAAxC,CAAiD,EAAjD,EAAqDP,WAArD,EAAZ;AACA,MAAMQ,OAAO,GAAGN,GAAG,CAACO,MAAJ,GAAa,CAAb,GAAiB,EAAjB,GAAsB,GAAtC;AACA,mBAAUD,OAAV,SAAoBN,GAApB;AACD;;AAED,SAASQ,cAAT,GAA0B;AACxB,mBAAUT,uBAAuB,EAAjC,SAAsCA,uBAAuB,EAA7D,SAAkEA,uBAAuB,EAAzF;AACD;;AAED,SAASU,cAAT,CAAwBC,cAAxB,EAAwC;AACtC,MAAMC,EAAE,kBAAW,EAAEhC,gBAAb,CAAR;AACA,MAAMiC,WAAW,GAAGJ,cAAc,EAAlC;AACA,MAAMK,YAAY,GAAGC,QAAQ,CAACC,aAAT,CAAuB,OAAvB,CAArB;AACA,MAAMC,OAAO,GAAGF,QAAQ,CAACC,aAAT,CAAuB,KAAvB,CAAhB;AACA,MAAME,KAAK,GAAGH,QAAQ,CAACC,aAAT,CAAuB,OAAvB,CAAd;AACA,MAAMG,SAAS,GAAGJ,QAAQ,CAACC,aAAT,CAAuB,KAAvB,CAAlB;AACA,MAAMI,WAAW,GAAGL,QAAQ,CAACC,aAAT,CAAuB,QAAvB,CAApB;AACA,MAAMK,QAAQ,GAAGN,QAAQ,CAACC,aAAT,CAAuB,KAAvB,CAAjB;AAEAF,cAAY,CAACQ,gBAAb,CAA8B,QAA9B,EAAwC,UAAAC,KAAK,EAAI;AAC/C,QAAMC,SAAS,GAAGD,KAAK,CAACE,MAAN,CAAaC,KAA/B;AACA,QAAMC,OAAO,GAAG/B,eAAe,CAAC4B,SAAD,CAA/B;;AACA,QAAIG,OAAJ,EAAa;AACXV,aAAO,CAACW,KAAR,CAAcC,WAAd,CAA0B,kBAA1B,aAAkDL,SAAlD;AACAV,kBAAY,CAACgB,SAAb,CAAuBC,MAAvB,CAA8B,qBAA9B;AACD,KAHD,MAGO;AACLjB,kBAAY,CAACgB,SAAb,CAAuBE,GAAvB,CAA2B,qBAA3B;AACD;AACF,GATD;AAUAZ,aAAW,CAACE,gBAAZ,CAA6B,OAA7B,EAAsC;AAAA,WAAM1D,GAAG,CAACqE,cAAJ,CAAmBC,WAAnB,CAA+Bb,QAA/B,CAAN;AAAA,GAAtC;AACAJ,SAAO,CAACa,SAAR,CAAkBE,GAAlB,CAAsB,eAAtB;AACAf,SAAO,CAACW,KAAR,CAAcC,WAAd,CAA0B,kBAA1B,aAAkDhB,WAAlD;AACAC,cAAY,CAACqB,YAAb,CAA0B,MAA1B,EAAkC,MAAlC;AACArB,cAAY,CAACqB,YAAb,CAA0B,OAA1B,EAAmCtB,WAAnC;AACAC,cAAY,CAACqB,YAAb,CAA0B,IAA1B,EAAgCvB,EAAhC;AACAE,cAAY,CAACgB,SAAb,CAAuBE,GAAvB,CAA2B,aAA3B;AACAd,OAAK,CAACiB,YAAN,CAAmB,KAAnB,EAA0BvB,EAA1B;AACAQ,aAAW,CAACU,SAAZ,CAAsBE,GAAtB,CAA0B,KAA1B;AACAZ,aAAW,CAACU,SAAZ,CAAsBE,GAAtB,CAA0B,qBAA1B;AACAb,WAAS,CAACW,SAAV,CAAoBE,GAApB,CAAwB,iBAAxB;AACAX,UAAQ,CAACS,SAAT,CAAmBE,GAAnB,CAAuB,WAAvB;AACAb,WAAS,CAACiB,WAAV,CAAsBnB,OAAtB;AACAE,WAAS,CAACiB,WAAV,CAAsBtB,YAAtB;AACAK,WAAS,CAACiB,WAAV,CAAsBlB,KAAtB;AACAG,UAAQ,CAACe,WAAT,CAAqBjB,SAArB;;AACA,MAAI,CAACR,cAAL,EAAqB;AACnBQ,aAAS,CAACiB,WAAV,CAAsBhB,WAAtB;AACD;;AACDxD,KAAG,CAACqE,cAAJ,CAAmBG,WAAnB,CAA+Bf,QAA/B;AACD;;AAED,SAASgB,YAAT,GAAwB;AACtB,MAAMC,QAAQ,GAAG1E,GAAG,CAACqE,cAAJ,CAAmBM,sBAAnB,CAA0C,aAA1C,CAAjB;AACA,SAAOC,KAAK,CAACC,SAAN,CAAgBtD,KAAhB,CAAsBC,IAAtB,CAA2BkD,QAA3B,EACJI,GADI,CACA,UAAAC,GAAG;AAAA,WAAIA,GAAG,CAACjB,KAAR;AAAA,GADH,CAAP;AAED;;AAED,SAASkB,wBAAT,CAAkCrB,KAAlC,EAAyC;AACvC,MAAI,CAACA,KAAK,CAACE,MAAN,CAAaoB,OAAlB,EAA2B;AAAE;AAAS;;AACtChF,QAAM,CAACM,YAAP,GAAsBoD,KAAK,CAACE,MAAN,CAAaC,KAAnC;AACD;;AAED,SAASoB,6BAAT,CAAuCvB,KAAvC,EAA8C;AAC5C,MAAI,CAACA,KAAK,CAACE,MAAN,CAAaoB,OAAlB,EAA2B;AAAE;AAAS;;AACtChF,QAAM,CAACU,iBAAP,GAA2BgD,KAAK,CAACE,MAAN,CAAaC,KAAxC;AACD;;AAED,SAASqB,IAAT,GAAgB;AACdvD,QAAM,CAACG,WAAP,GAAqB;AAAA,WAAMqD,UAAU,CAAC;AAAA,aAAMpF,GAAG,CAACqF,MAAJ,CAAWnB,SAAX,CAAqBC,MAArB,CAA4BvD,aAA5B,CAAN;AAAA,KAAD,EAAmDE,UAAnD,CAAhB;AAAA,GAArB;;AACAf,QAAM,CAACuF,OAAP,CAAe,UAAAC,GAAG;AAAA,WAAIvF,GAAG,CAACuF,GAAD,CAAH,GAAWpC,QAAQ,CAACqC,cAAT,CAAwBD,GAAxB,CAAf;AAAA,GAAlB;AACAvF,KAAG,CAACE,aAAJ,CAAkBwD,gBAAlB,CAAmC,QAAnC,EAA6C,UAAAC,KAAK;AAAA,WAAI1D,MAAM,CAACC,aAAP,GAAuBuF,QAAQ,CAAC9B,KAAK,CAACE,MAAN,CAAaC,KAAd,EAAqB,EAArB,CAAnC;AAAA,GAAlD;AACA9D,KAAG,CAACG,cAAJ,CAAmBuD,gBAAnB,CAAoC,QAApC,EAA8C,UAAAC,KAAK;AAAA,WAAI1D,MAAM,CAACE,cAAP,GAAwBsF,QAAQ,CAAC9B,KAAK,CAACE,MAAN,CAAaC,KAAd,EAAqB,EAArB,CAApC;AAAA,GAAnD,EAJc,CAKd;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AACA9D,KAAG,CAAC0F,KAAJ,CAAUhC,gBAAV,CAA2B,QAA3B,EAAqC,UAAAC,KAAK,EAAI;AAC5C,QAAMgC,GAAG,GAAGhC,KAAK,CAACE,MAAN,CAAaoB,OAAzB;AACAhF,UAAM,CAACI,WAAP,GAAqBsF,GAArB;AACAA,OAAG,GACD3F,GAAG,CAAC4F,kBAAJ,CAAuB1B,SAAvB,CAAiCE,GAAjC,CAAqCvD,kBAArC,CADC,GAEDb,GAAG,CAAC4F,kBAAJ,CAAuB1B,SAAvB,CAAiCC,MAAjC,CAAwCtD,kBAAxC,CAFF;AAGD,GAND;AAOAb,KAAG,CAACQ,SAAJ,CAAckD,gBAAd,CAA+B,QAA/B,EAAyC,UAAAC,KAAK;AAAA,WAAI1D,MAAM,CAACO,SAAP,GAAmBiF,QAAQ,CAAC9B,KAAK,CAACE,MAAN,CAAaC,KAAd,EAAqB,EAArB,CAA/B;AAAA,GAA9C;AACA9D,KAAG,CAAC6F,SAAJ,CAAcnC,gBAAd,CAA+B,QAA/B,EAAyC,UAAAC,KAAK;AAAA,WAAI1D,MAAM,CAACK,eAAP,GAAyBqD,KAAK,CAACE,MAAN,CAAaoB,OAA1C;AAAA,GAA9C;AAEAjF,KAAG,CAAC8F,kBAAJ,CAAuBpC,gBAAvB,CAAwC,QAAxC,EAAkDsB,wBAAlD;AACAhF,KAAG,CAAC+F,qBAAJ,CAA0BrC,gBAA1B,CAA2C,QAA3C,EAAqDsB,wBAArD;AACAhF,KAAG,CAACgG,gBAAJ,CAAqBtC,gBAArB,CAAsC,QAAtC,EAAgDsB,wBAAhD;AACAhF,KAAG,CAACiG,kBAAJ,CAAuBvC,gBAAvB,CAAwC,QAAxC,EAAkDsB,wBAAlD;AACAhF,KAAG,CAACkG,yBAAJ,CAA8BxC,gBAA9B,CAA+C,QAA/C,EAAyDwB,6BAAzD;AACAlF,KAAG,CAACmG,2BAAJ,CAAgCzC,gBAAhC,CAAiD,QAAjD,EAA2DwB,6BAA3D;AACAlF,KAAG,CAACoG,QAAJ,CAAa1C,gBAAb,CAA8B,OAA9B,EAAuC,YAAM;AAC3C,QAAI,CAACzD,MAAM,CAACG,YAAR,IAAwB,CAACH,MAAM,CAACI,WAAhC,IAA+C,CAACJ,MAAM,CAACK,eAA3D,EAA4E;AAAE;AAAS;;AACvF,QAAMI,MAAM,GAAG+D,YAAY,EAA3B;AACA,QAAM4B,cAAc,GAAG3F,MAAM,CAAC4F,KAAP,CAAatE,eAAb,CAAvB;;AACA,QAAI,CAACqE,cAAL,EAAqB;AAAE;AAAS;;AAChCpG,UAAM,CAACS,MAAP,GAAgBA,MAAhB;AACAV,OAAG,CAACqF,MAAJ,CAAWnB,SAAX,CAAqBE,GAArB,CAAyBxD,aAAzB;AACAwE,cAAU,CAAC;AAAA,aAAMnE,UAAU,CAAC,gBAAD,EAAmBI,IAAI,CAACC,SAAL,CAAerB,MAAf,CAAnB,CAAhB;AAAA,KAAD,EAA6Da,UAA7D,CAAV;AACD,GARD;AASAd,KAAG,CAACuG,QAAJ,CAAa7C,gBAAb,CAA8B,OAA9B,EAAuC;AAAA,WAAMZ,cAAc,EAApB;AAAA,GAAvC;AACAA,gBAAc,CAAC,IAAD,CAAd;AACAA,gBAAc,CAAC,IAAD,CAAd;AACD;;AACDK,QAAQ,CAACO,gBAAT,CAA0B,kBAA1B,EAA8CyB,IAA9C,E","file":"main.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./assets/ui/main.js\");\n","const domIds = [\n 'numEdgePoints',\n 'numFieldPoints',\n 'points',\n 'lines',\n 'triangles',\n 'generate',\n 'loader',\n 'pointRadius',\n 'pointRadiusContainer',\n 'lineWidth',\n 'lineWidthContainer',\n 'distributionRandom',\n 'distributionParabolic',\n 'distributionGrid',\n 'distributionRadial',\n 'colorContainer',\n 'addColor',\n 'colorDistributionDiscrete',\n 'colorDistributionContinuous'\n];\nconst dom = {};\nconst params = {\n numEdgePoints: 20,\n numFieldPoints: 20,\n renderPoints: false,\n renderLines: false,\n renderTriangles: true,\n distribution: 'random',\n lineWidth: 4,\n pointRadius: 5,\n colors: [],\n colorDistribution: 'continuous'\n};\nconst LOADER_ACTIVE = 'loader-active';\nconst SHAPE_PARAM_ACTIVE = 'shape-param-active';\nconst TIME_DELAY = 50;\nconst HEX_REGEX = /[0-9A-F]{6}$/;\nlet colorPickerCount = 0;\n\nfunction callPlugin(actionName) {\n if (!actionName) {\n throw new Error('missing action name')\n }\n try {\n const payload = JSON.stringify([].slice.call(arguments));\n console.log(payload);\n window['__skpm_sketchBridge'].callNative(payload);\n } catch(error) {\n closeLoader();\n }\n}\n\nfunction isValidHexColor(hexValue) {\n if (!hexValue || typeof hexValue !== 'string') { return false }\n return HEX_REGEX.test(hexValue.toUpperCase());\n}\n\nfunction getRandomColorComponent() {\n const str = Number(Math.floor(256 * Math.random())).toString(16).toUpperCase();\n const leftPad = str.length > 1 ? '' : '0';\n return `${leftPad}${str}`;\n}\n\nfunction getRandomColor() {\n return `${getRandomColorComponent()}${getRandomColorComponent()}${getRandomColorComponent()}`;\n}\n\nfunction addColorPicker(suppressDelete) {\n const id = `input${++colorPickerCount}`;\n const colorString = getRandomColor();\n const inputElement = document.createElement('input');\n const preview = document.createElement('div');\n const label = document.createElement('label');\n const container = document.createElement('div');\n const closeButton = document.createElement('button');\n const colorRow = document.createElement('div');\n\n inputElement.addEventListener('change', event => {\n const hexString = event.target.value;\n const isValid = isValidHexColor(hexString);\n if (isValid) {\n preview.style.setProperty('background-color', `#${hexString}`);\n inputElement.classList.remove('color-input-invalid');\n } else {\n inputElement.classList.add('color-input-invalid');\n }\n });\n closeButton.addEventListener('click', () => dom.colorContainer.removeChild(colorRow));\n preview.classList.add('color-preview');\n preview.style.setProperty('background-color', `#${colorString}`);\n inputElement.setAttribute('type', 'text');\n inputElement.setAttribute('value', colorString);\n inputElement.setAttribute('id', id);\n inputElement.classList.add('color-input');\n label.setAttribute('for', id);\n closeButton.classList.add('fab');\n closeButton.classList.add('remove-color-button');\n container.classList.add('color-container');\n colorRow.classList.add('color-row');\n container.appendChild(preview);\n container.appendChild(inputElement);\n container.appendChild(label);\n colorRow.appendChild(container);\n if (!suppressDelete) {\n container.appendChild(closeButton);\n }\n dom.colorContainer.appendChild(colorRow);\n}\n\nfunction getAllColors() {\n const elements = dom.colorContainer.getElementsByClassName('color-input');\n return Array.prototype.slice.call(elements)\n .map(ele => ele.value);\n}\n\nfunction handleDistributionChange(event) {\n if (!event.target.checked) { return; }\n params.distribution = event.target.value;\n}\n\nfunction handleColorDistributionChange(event) {\n if (!event.target.checked) { return; }\n params.colorDistribution = event.target.value;\n}\n\nfunction init() {\n window.closeLoader = () => setTimeout(() => dom.loader.classList.remove(LOADER_ACTIVE), TIME_DELAY);\n domIds.forEach(key => dom[key] = document.getElementById(key));\n dom.numEdgePoints.addEventListener('change', event => params.numEdgePoints = parseInt(event.target.value, 10));\n dom.numFieldPoints.addEventListener('change', event => params.numFieldPoints = parseInt(event.target.value, 10));\n // TODO: reimplement when we know about ovals in Sketch 52\n // dom.points.addEventListener('change', event => {\n // const val = event.target.checked;\n // params.renderPoints = val;\n // val ?\n // dom.pointRadiusContainer.classList.add(SHAPE_PARAM_ACTIVE) :\n // dom.pointRadiusContainer.classList.remove(SHAPE_PARAM_ACTIVE);\n // });\n // dom.pointRadius.addEventListener('change', event => params.pointRadius = parseInt(event.target.value, 10));\n dom.lines.addEventListener('change', event => {\n const val = event.target.checked;\n params.renderLines = val;\n val ?\n dom.lineWidthContainer.classList.add(SHAPE_PARAM_ACTIVE) :\n dom.lineWidthContainer.classList.remove(SHAPE_PARAM_ACTIVE);\n });\n dom.lineWidth.addEventListener('change', event => params.lineWidth = parseInt(event.target.value, 10));\n dom.triangles.addEventListener('change', event => params.renderTriangles = event.target.checked);\n\n dom.distributionRandom.addEventListener('change', handleDistributionChange);\n dom.distributionParabolic.addEventListener('change', handleDistributionChange);\n dom.distributionGrid.addEventListener('change', handleDistributionChange);\n dom.distributionRadial.addEventListener('change', handleDistributionChange);\n dom.colorDistributionDiscrete.addEventListener('change', handleColorDistributionChange);\n dom.colorDistributionContinuous.addEventListener('change', handleColorDistributionChange);\n dom.generate.addEventListener('click', () => {\n if (!params.renderPoints && !params.renderLines && !params.renderTriangles) { return; }\n const colors = getAllColors();\n const colorsAreValid = colors.every(isValidHexColor);\n if (!colorsAreValid) { return; }\n params.colors = colors;\n dom.loader.classList.add(LOADER_ACTIVE);\n setTimeout(() => callPlugin('GENERATE_FIELD', JSON.stringify(params)), TIME_DELAY);\n });\n dom.addColor.addEventListener('click', () => addColorPicker());\n addColorPicker(true);\n addColorPicker(true);\n}\ndocument.addEventListener('DOMContentLoaded', init);\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /triangle-field.sketchplugin/Contents/Resources/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Triangle Field 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 |
16 |
Number of Points
17 |
18 |
19 | 22 | 27 |
28 | 29 |
30 | 33 | 38 |
39 |
40 |
41 | 42 | 43 |
44 |
Point Distribution
45 |
46 |
47 | 55 | 56 | 57 |
58 |
59 | 66 | 67 | 68 |
69 |
70 | 77 | 78 | 79 |
80 |
81 | 88 | 89 | 90 |
91 |
92 |
93 | 94 | 95 |
96 |
Color Distribution
97 |
98 |
99 | 107 | 108 | 109 |
110 |
111 | 118 | 119 | 120 |
121 |
122 |
123 | 124 | 125 | 126 |
127 | 128 |
129 | 130 |
131 |
Rendering Options
132 |
133 |
134 | 142 | 143 | 144 |
145 |
146 |
147 | 154 | 155 | 156 |
157 |
158 | 161 | 166 |
167 |
168 | 169 | 192 |
193 |
194 | 195 | 196 |
197 |
Colors
198 |
199 |
200 | 201 |
202 |
203 | 204 |
205 |
206 | 207 |
208 |
209 |

Crunching the Numbers

210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 | 218 | 219 | -------------------------------------------------------------------------------- /triangle-field.sketchplugin/Contents/Resources/ui/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | --color-background: #ECECEC; 3 | --color-font: #515151; 4 | --color-primary: #4892DE; 5 | --color-primary-light: #C1DDFA; 6 | --color-secondary: #BBCCFF; 7 | --color-tertiary: #E97171; 8 | --color-grey-dark: #333; 9 | --color-black: #121212; 10 | --color-grey: #515151; 11 | --color-white: #FFF; 12 | --color-grey-light: #AAA; 13 | --color-grey-lightest: #CCC; 14 | --animation-time: 0.2s; 15 | --animation-time-slow: 0.3s; 16 | --icon-size: 20px; 17 | } 18 | 19 | html, body { 20 | width: 100%; 21 | height: 100%; 22 | margin: 0; 23 | padding: 0; 24 | position: relative; 25 | font-family: 'Roboto', Helvetica, Arial, sans-serif; 26 | font-size: 18px; 27 | font-weight: 200; 28 | background-color: var(--color-background); 29 | color: var(--color-font); 30 | user-select: none; 31 | } 32 | 33 | h1 { 34 | margin: 0; 35 | padding: 0; 36 | } 37 | 38 | p { 39 | margin: 0; 40 | padding: 0; 41 | } 42 | 43 | input { 44 | background: none; 45 | background-color: var(--color-white); 46 | border: none; 47 | border-bottom: 1px solid var(--color-primary-light); 48 | padding: 2px; 49 | margin: 0 8px; 50 | color: var(--color-font); 51 | width: 90px; 52 | font-size: 18px; 53 | outline-style: none; 54 | font-weight: 200; 55 | transition: font-weight var(--antimation-time) ease; 56 | } 57 | input[type="number"]::-webkit-outer-spin-button, 58 | input[type="number"]::-webkit-inner-spin-button { 59 | -webkit-appearance: none; 60 | margin: 0; 61 | } 62 | input[type="number"] { 63 | -moz-appearance: textfield; 64 | position: relative; 65 | } 66 | input:focus { 67 | border-bottom: 1px solid var(--color-primary); 68 | font-weight: 500; 69 | } 70 | 71 | button { 72 | border: none; 73 | outline: none; 74 | background-color: Transparent; 75 | background-repeat:no-repeat; 76 | cursor: pointer; 77 | padding: 8px; 78 | border-radius: 4px; 79 | color: var(--color-white); 80 | background-color: var(--color-primary); 81 | box-shadow: 0px 1px 4px var(--color-grey-dark); 82 | transition: box-shadow var(--animation-time) ease; 83 | } 84 | 85 | .columns { 86 | display: flex; 87 | flex-direction: row; 88 | justify-content: flex-start; 89 | align-items: flex-start; 90 | } 91 | .column { 92 | display: flex; 93 | flex-direction: column; 94 | justify-content: flex-start; 95 | align-items: flex-start; 96 | } 97 | 98 | .card { 99 | border: none; 100 | background-color: var(--color-white); 101 | box-shadow: 0px 1px 4px var(--color-grey-dark); 102 | border-radius: 4px; 103 | margin: 8px; 104 | width: 325px; 105 | position: relative; 106 | } 107 | 108 | .card-label { 109 | padding: 8px; 110 | font-size: 1.2em; 111 | color: var(--color-black); 112 | background-color: var(--color-secondary); 113 | } 114 | 115 | .card-content { 116 | padding: 8px; 117 | } 118 | 119 | .row { 120 | display: flex; 121 | flex-direction: row; 122 | justify-content: flex-start; 123 | margin: 8px 0; 124 | } 125 | 126 | .row-space-between { 127 | display: flex; 128 | flex-direction: row; 129 | justify-content: space-between; 130 | } 131 | 132 | .loader { 133 | display: none; 134 | font-size: 1.4em; 135 | } 136 | 137 | .loader-active { 138 | position: absolute; 139 | left: 0; 140 | top: 0; 141 | right: 0; 142 | bottom: 0; 143 | color: var(--color-white); 144 | background-color: rgba(1, 1, 1, 0.5); 145 | display: flex; 146 | flex-direction: column; 147 | align-items: center; 148 | justify-content: space-around; 149 | } 150 | 151 | .loader-icon-row { 152 | display: flex; 153 | flex-direction: row; 154 | justify-content: space-between; 155 | align-items: center; 156 | padding: 0 32px; 157 | margin: 8px 0; 158 | } 159 | 160 | .loader-icon { 161 | width: 16px; 162 | height: 16px; 163 | animation: loader-icon--animate 1.4s infinite ease-in-out, loader-icon-rotate 8s infinite linear; 164 | opacity: 0; 165 | } 166 | 167 | .loader-icon-1 { 168 | width: 0; 169 | height: 0; 170 | border-top: 35px solid #69F16D; 171 | border-right: 30px solid transparent; 172 | transform: rotateZ(20deg); 173 | } 174 | 175 | .loader-icon-2 { 176 | width: 0; 177 | height: 0; 178 | border-left: 30px solid transparent; 179 | border-right: 15px solid transparent; 180 | border-top: 20px solid #6CD2EA; 181 | transform: rotateZ(-10deg); 182 | } 183 | 184 | .loader-icon-3 { 185 | width: 0; 186 | height: 0; 187 | border-bottom: 30px solid #F169ED; 188 | border-right: 25px solid transparent; 189 | transform: rotateZ(28deg); 190 | } 191 | 192 | @keyframes loader-icon--animate { 193 | 0% { 194 | opacity: 0; 195 | } 196 | 50% { 197 | opacity: 1; 198 | } 199 | 100% { 200 | opacity: 0; 201 | } 202 | } 203 | 204 | @keyframes loader-icon-rotate { 205 | 0% { 206 | transform: rotateZ(0deg); 207 | } 208 | 100% { 209 | transform: rotateZ(360deg); 210 | } 211 | } 212 | 213 | .sublabel { 214 | font-style: italic; 215 | font-size: 0.8em; 216 | } 217 | 218 | .row { 219 | display: flex; 220 | flex-direction: row; 221 | justify-content: flex-start; 222 | align-items: center; 223 | margin: 4px 0; 224 | } 225 | 226 | .shape-param { 227 | transform: scaleY(0); 228 | opacity: 0; 229 | transform-origin: center; 230 | margin: 0 8px; 231 | transition: transform var(--animation-time-slow) ease, opacity var(--animation-time) ease; 232 | } 233 | 234 | .shape-param-active { 235 | opacity: 1; 236 | transform: scaleY(1); 237 | } 238 | 239 | .color-container { 240 | display: flex; 241 | flex-direction: row; 242 | justify-content: flex-start; 243 | align-items: center; 244 | margin: 4px 0; 245 | } 246 | .color-row { 247 | display: flex; 248 | flex-direction: row; 249 | justify-content: flex-start; 250 | align-items: center; 251 | } 252 | 253 | .color-preview { 254 | width: 30px; 255 | height: 30px; 256 | border-radius: 15px; 257 | margin: 0 4px; 258 | } 259 | .color-input { 260 | width: 80px; 261 | } 262 | .color-input-invalid { 263 | border: 2px solid red; 264 | border-radius: 4px; 265 | } 266 | 267 | .checkbox-label { 268 | position: relative; 269 | display: block; 270 | height: 16px; 271 | width: 32px; 272 | background-color: var(--color-grey-light); 273 | border-radius: 16px; 274 | cursor: pointer; 275 | transition: background-color var(--animation-time) ease; 276 | } 277 | 278 | .checkbox-input:checked ~ .checkbox-label { 279 | background-color: var(--color-primary-light); 280 | } 281 | 282 | .checkbox-label { 283 | margin: 0 8px; 284 | } 285 | 286 | .checkbox-label:after { 287 | position: absolute; 288 | left: 0; 289 | top: -2px; 290 | display: block; 291 | width: var(--icon-size); 292 | height: var(--icon-size); 293 | border-radius: 50%; 294 | background-color: var(--color-white); 295 | content: ''; 296 | box-shadow: 0px 0px 4px var(--color-grey-dark); 297 | transition: left var(--animation-time) ease, background-color var(--animation-time) ease, box-shadow var(--animation-time) ease; 298 | } 299 | 300 | .checkbox-input:checked ~ label:after { 301 | left: 12px; 302 | background-color: var(--color-primary); 303 | box-shadow: 0px 0px 1px var(--color-grey-dark); 304 | } 305 | 306 | .input-hidden { 307 | display: none; 308 | } 309 | 310 | .radio-label { 311 | position: relative; 312 | display: block; 313 | height: var(--icon-size); 314 | width: var(--icon-size); 315 | border-radius: 50%; 316 | margin: 0 4px; 317 | cursor: pointer; 318 | transition: background-color var(--animation-time) ease; 319 | } 320 | 321 | .radio-input ~ .radio-label { 322 | border: 2px solid var(--color-grey-light); 323 | transition: border-color var(--animation-time) ease; 324 | } 325 | .radio-input:checked ~ .radio-label { 326 | border-color: var(--color-primary); 327 | } 328 | 329 | .radio-label:after { 330 | position: absolute; 331 | left: 0; 332 | top: 0; 333 | display: block; 334 | width: var(--icon-size); 335 | height: var(--icon-size); 336 | border-radius: 50%; 337 | content: ''; 338 | } 339 | 340 | .radio-input ~ label:after { 341 | transform: scale(0); 342 | transition: transform var(--animation-time-slow) ease; 343 | } 344 | 345 | .radio-input:checked ~ label:after { 346 | background-color: var(--color-primary); 347 | transform: scale(0.8); 348 | transform-origin: center; 349 | } 350 | 351 | .fab { 352 | width: 36px; 353 | height: 36px; 354 | border-radius: 50%; 355 | position: absolute; 356 | bottom: 0; 357 | right: 0; 358 | margin: 8px; 359 | display: block; 360 | } 361 | .fab:hover { 362 | box-shadow: 0px 2px 8px var(--color-grey-dark); 363 | } 364 | .fab:before { 365 | content: ''; 366 | background-color: var(--color-white); 367 | width: 22px; 368 | height: 2px; 369 | border-radius: 2px; 370 | position: absolute; 371 | display: block; 372 | top: 50%; 373 | left: 50%; 374 | transform: translate(-50%) rotateZ(90deg); 375 | } 376 | .fab:after { 377 | content: ''; 378 | background-color: var(--color-white); 379 | width: 22px; 380 | height: 2px; 381 | border-radius: 2px; 382 | position: absolute; 383 | display: block; 384 | top: 50%; 385 | left: 50%; 386 | transform: translate(-50%); 387 | } 388 | 389 | .generate-button { 390 | margin: 8px; 391 | width: 325px; 392 | font-size: 1.2em; 393 | } 394 | 395 | .remove-color-button { 396 | opacity: 0; 397 | position: relative; 398 | background-color: var(--color-tertiary); 399 | transition: opacity var(--animation-time) ease-in-out; 400 | width: 25px; 401 | height: 25px; 402 | margin: -4px 0 0 0; 403 | } 404 | .remove-color-button:before { 405 | width: 18px; 406 | transform: translate(-50%) rotateZ(45deg); 407 | } 408 | .remove-color-button:after { 409 | width: 18px; 410 | transform: translate(-50%) rotateZ(-45deg); 411 | } 412 | .color-container:hover > .remove-color-button { 413 | opacity: 1; 414 | } 415 | -------------------------------------------------------------------------------- /triangle-field.sketchplugin/Contents/Resources/ui/main.js: -------------------------------------------------------------------------------- 1 | const domIds = [ 2 | 'numEdgePoints', 3 | 'numFieldPoints', 4 | 'points', 5 | 'lines', 6 | 'triangles', 7 | 'generate', 8 | 'loader', 9 | 'pointRadius', 10 | 'pointRadiusContainer', 11 | 'lineWidth', 12 | 'lineWidthContainer', 13 | 'distributionRandom', 14 | 'distributionParabolic', 15 | 'distributionGrid', 16 | 'distributionRadial', 17 | 'colorContainer', 18 | 'addColor', 19 | 'colorDistributionDiscrete', 20 | 'colorDistributionContinuous' 21 | ]; 22 | const dom = {}; 23 | const params = { 24 | numEdgePoints: 20, 25 | numFieldPoints: 20, 26 | renderPoints: false, 27 | renderLines: false, 28 | renderTriangles: true, 29 | distribution: 'random', 30 | lineWidth: 4, 31 | pointRadius: 5, 32 | colors: [], 33 | colorDistribution: 'continuous' 34 | }; 35 | const LOADER_ACTIVE = 'loader-active'; 36 | const SHAPE_PARAM_ACTIVE = 'shape-param-active'; 37 | const TIME_DELAY = 50; 38 | const HEX_REGEX = /[0-9A-F]{6}$/; 39 | let colorPickerCount = 0; 40 | 41 | function callPlugin(actionName) { 42 | if (!actionName) { 43 | throw new Error('missing action name') 44 | } 45 | try { 46 | const payload = JSON.stringify([].slice.call(arguments)); 47 | console.log(payload); 48 | window['__skpm_sketchBridge'].callNative(payload); 49 | } catch(error) { 50 | closeLoader(); 51 | } 52 | } 53 | 54 | function isValidHexColor(hexValue) { 55 | if (!hexValue || typeof hexValue !== 'string') { return false } 56 | return HEX_REGEX.test(hexValue.toUpperCase()); 57 | } 58 | 59 | function getRandomColorComponent() { 60 | const str = Number(Math.floor(256 * Math.random())).toString(16).toUpperCase(); 61 | const leftPad = str.length > 1 ? '' : '0'; 62 | return `${leftPad}${str}`; 63 | } 64 | 65 | function getRandomColor() { 66 | return `${getRandomColorComponent()}${getRandomColorComponent()}${getRandomColorComponent()}`; 67 | } 68 | 69 | function addColorPicker(suppressDelete) { 70 | const id = `input${++colorPickerCount}`; 71 | const colorString = getRandomColor(); 72 | const inputElement = document.createElement('input'); 73 | const preview = document.createElement('div'); 74 | const label = document.createElement('label'); 75 | const container = document.createElement('div'); 76 | const closeButton = document.createElement('button'); 77 | const colorRow = document.createElement('div'); 78 | 79 | inputElement.addEventListener('change', event => { 80 | const hexString = event.target.value; 81 | const isValid = isValidHexColor(hexString); 82 | if (isValid) { 83 | preview.style.setProperty('background-color', `#${hexString}`); 84 | inputElement.classList.remove('color-input-invalid'); 85 | } else { 86 | inputElement.classList.add('color-input-invalid'); 87 | } 88 | }); 89 | closeButton.addEventListener('click', () => dom.colorContainer.removeChild(colorRow)); 90 | preview.classList.add('color-preview'); 91 | preview.style.setProperty('background-color', `#${colorString}`); 92 | inputElement.setAttribute('type', 'text'); 93 | inputElement.setAttribute('value', colorString); 94 | inputElement.setAttribute('id', id); 95 | inputElement.classList.add('color-input'); 96 | label.setAttribute('for', id); 97 | closeButton.classList.add('fab'); 98 | closeButton.classList.add('remove-color-button'); 99 | container.classList.add('color-container'); 100 | colorRow.classList.add('color-row'); 101 | container.appendChild(preview); 102 | container.appendChild(inputElement); 103 | container.appendChild(label); 104 | colorRow.appendChild(container); 105 | if (!suppressDelete) { 106 | container.appendChild(closeButton); 107 | } 108 | dom.colorContainer.appendChild(colorRow); 109 | } 110 | 111 | function getAllColors() { 112 | const elements = dom.colorContainer.getElementsByClassName('color-input'); 113 | return Array.prototype.slice.call(elements) 114 | .map(ele => ele.value); 115 | } 116 | 117 | function handleDistributionChange(event) { 118 | if (!event.target.checked) { return; } 119 | params.distribution = event.target.value; 120 | } 121 | 122 | function handleColorDistributionChange(event) { 123 | if (!event.target.checked) { return; } 124 | params.colorDistribution = event.target.value; 125 | } 126 | 127 | function init() { 128 | window.closeLoader = () => setTimeout(() => dom.loader.classList.remove(LOADER_ACTIVE), TIME_DELAY); 129 | domIds.forEach(key => dom[key] = document.getElementById(key)); 130 | dom.numEdgePoints.addEventListener('change', event => params.numEdgePoints = parseInt(event.target.value, 10)); 131 | dom.numFieldPoints.addEventListener('change', event => params.numFieldPoints = parseInt(event.target.value, 10)); 132 | // TODO: reimplement when we know about ovals in Sketch 52 133 | // dom.points.addEventListener('change', event => { 134 | // const val = event.target.checked; 135 | // params.renderPoints = val; 136 | // val ? 137 | // dom.pointRadiusContainer.classList.add(SHAPE_PARAM_ACTIVE) : 138 | // dom.pointRadiusContainer.classList.remove(SHAPE_PARAM_ACTIVE); 139 | // }); 140 | // dom.pointRadius.addEventListener('change', event => params.pointRadius = parseInt(event.target.value, 10)); 141 | dom.lines.addEventListener('change', event => { 142 | const val = event.target.checked; 143 | params.renderLines = val; 144 | val ? 145 | dom.lineWidthContainer.classList.add(SHAPE_PARAM_ACTIVE) : 146 | dom.lineWidthContainer.classList.remove(SHAPE_PARAM_ACTIVE); 147 | }); 148 | dom.lineWidth.addEventListener('change', event => params.lineWidth = parseInt(event.target.value, 10)); 149 | dom.triangles.addEventListener('change', event => params.renderTriangles = event.target.checked); 150 | 151 | dom.distributionRandom.addEventListener('change', handleDistributionChange); 152 | dom.distributionParabolic.addEventListener('change', handleDistributionChange); 153 | dom.distributionGrid.addEventListener('change', handleDistributionChange); 154 | dom.distributionRadial.addEventListener('change', handleDistributionChange); 155 | dom.colorDistributionDiscrete.addEventListener('change', handleColorDistributionChange); 156 | dom.colorDistributionContinuous.addEventListener('change', handleColorDistributionChange); 157 | dom.generate.addEventListener('click', () => { 158 | if (!params.renderPoints && !params.renderLines && !params.renderTriangles) { return; } 159 | const colors = getAllColors(); 160 | const colorsAreValid = colors.every(isValidHexColor); 161 | if (!colorsAreValid) { return; } 162 | params.colors = colors; 163 | dom.loader.classList.add(LOADER_ACTIVE); 164 | setTimeout(() => callPlugin('GENERATE_FIELD', JSON.stringify(params)), TIME_DELAY); 165 | }); 166 | dom.addColor.addEventListener('click', () => addColorPicker()); 167 | addColorPicker(true); 168 | addColorPicker(true); 169 | } 170 | document.addEventListener('DOMContentLoaded', init); 171 | -------------------------------------------------------------------------------- /triangle-field.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "compatibleVersion": "52.0", 3 | "bundleVersion": 1, 4 | "version": "2.0.1", 5 | "icon": "icon.png", 6 | "description": "Generate Delaunay triangle tessellation inside a shape.", 7 | "commands": [ 8 | { 9 | "name": "triangle-fields", 10 | "identifier": "triangle-fields-identifier", 11 | "script": "Init.js" 12 | } 13 | ], 14 | "menu": { 15 | "title": "triangle-fields", 16 | "items": [ 17 | "triangle-fields-identifier" 18 | ] 19 | }, 20 | "name": "triangle-field", 21 | "identifier": "triangle-field", 22 | "disableCocoaScriptPreprocessor": true, 23 | "appcast": "https://raw.githubusercontent.com/0la0/triangle-fields/master/.appcast.xml", 24 | "author": "Luke Anderson" 25 | } --------------------------------------------------------------------------------