├── .gitignore ├── README.md ├── dist └── plugin.js ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── plugin.js ├── renderer.js ├── server.js ├── ui.html └── util.js └── test └── sketch.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # figma-plugin-websockets 2 | 3 | A proof of concept connecting a Node.js server to Figma via local websockets. This allows for certain applications like live-coding JavaScript and piping the results to a Figma renderer, allowing for some interesting generative workflows. 4 | 5 | See [this teaser video](https://twitter.com/mattdesl/status/1661877697533059074). 6 | 7 | How it works: 8 | 9 | 1. A Node.js server is opened at a specific port on localhost. 10 | 2. A Figma plugin creates a new UI thread that connects a WebSocket to that localhost port. 11 | 3. The Node server watches for file changes to a given file, and sends the code to the Figma plugin's main thread to evaluate it as a rendered set of objects (frames, rectangles, shapes). 12 | 13 | This creates a two-way communication and allows for other interesting features. The plugin UI is currently hidden, but you could imagine it to include buttons such as "Export Metadata" (random seed, code) or "Export GIF/MP4" (spawning an `ffmpeg` process locally). 14 | 15 | ## Setup 16 | 17 | ### 1. Clone this repository and run the server 18 | 19 | When you first clone this repo, you will need to install the dependencies: 20 | 21 | ```sh 22 | cd figma-plugin-websockets 23 | 24 | # install deps 25 | npm install 26 | ``` 27 | 28 | To start developing, enter the following command on any JS file: 29 | 30 | ```sh 31 | node . test/sketch.js 32 | ``` 33 | 34 | You should see `Listening on port 19407` in your terminal. 35 | 36 | ### 2. Add the plugin into Figma 37 | 38 | In a new Figma document _using the desktop application_, open the Quick Actions bar with `Command + P` (macOS) or `Control + P` (Windows), and start typing `Import Plugin`. Choose the `Import Plugin from Manifest...` item and find the `manifest.json` in the repository folder you cloned in Step 1. 39 | 40 | ### 3. Run the plugin 41 | 42 | Once imported into Figma, it will appear in your Plugins panel under the `Development` drop-down, with the name of `LocalWebsockets`. In a fresh Figma document, click the Run button next to this plugin to create a new websocket-connected frame in your document. 43 | 44 | Now you can make changes to `test/sketch.js` or whatever file you are editing, and it will be reflected in the Figma document as actual vector objects (rectangles, ellipses, etc). You can click the 🎲 button to re-roll the random seed. Click the "X" to quit the plugin. 45 | 46 | ### 4. (Optional) Develop the Plugin 47 | 48 | If you want to further modify or develop the plugin, you'll need to run the build watcher in another terminal tab: 49 | 50 | ```sh 51 | npm run plugin:dev 52 | ``` 53 | 54 | Now you can edit the `src/plugin.js` file and it will create a new `dist/plugin.js` file. When you modify the plugin, you will need to close and re-run it in Figma to see the new changes. 55 | 56 | ## Limitations 57 | 58 | The main trouble is that evaluating the script has to happen in the main thread, which exposes some problems around debugging (error logging, lack of source maps, memory leaks) and security. 59 | 60 | ## Security Implications 61 | 62 | There may be some security implications by opening a websocket on localhost and communicating with Figma in this way. Feel free to open an issue if you think you could describe a scenario that might lead to a problem for the user. 63 | 64 | In addition to potential risks of exposing your local system to Figma, there may be a case where you are running some untrusted code in your user script (whether directly or without realizing it, such as a third-party dependency you have imported), and it does something nefarious with the `figma` global variable that exists due to the script running in Figma's main thread. 65 | 66 | ## License 67 | 68 | All source here is MIT licensed. 69 | -------------------------------------------------------------------------------- /dist/plugin.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | var __create = Object.create; 3 | var __defProp = Object.defineProperty; 4 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 5 | var __getOwnPropNames = Object.getOwnPropertyNames; 6 | var __getProtoOf = Object.getPrototypeOf; 7 | var __hasOwnProp = Object.prototype.hasOwnProperty; 8 | var __commonJS = (cb, mod) => function __require() { 9 | return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; 10 | }; 11 | var __copyProps = (to, from, except, desc) => { 12 | if (from && typeof from === "object" || typeof from === "function") { 13 | for (let key of __getOwnPropNames(from)) 14 | if (!__hasOwnProp.call(to, key) && key !== except) 15 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 16 | } 17 | return to; 18 | }; 19 | var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( 20 | // If the importer is in node compatibility mode or this is not an ESM 21 | // file that has been converted to a CommonJS file using a Babel- 22 | // compatible transform (i.e. "__esModule" has not been set), then set 23 | // "default" to the CommonJS "module.exports" for node compatibility. 24 | isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, 25 | mod 26 | )); 27 | 28 | // node_modules/canvas-sketch-util/lib/css-color-names.json 29 | var require_css_color_names = __commonJS({ 30 | "node_modules/canvas-sketch-util/lib/css-color-names.json"(exports, module) { 31 | module.exports = { 32 | aliceblue: "#f0f8ff", 33 | antiquewhite: "#faebd7", 34 | aqua: "#00ffff", 35 | aquamarine: "#7fffd4", 36 | azure: "#f0ffff", 37 | beige: "#f5f5dc", 38 | bisque: "#ffe4c4", 39 | black: "#000000", 40 | blanchedalmond: "#ffebcd", 41 | blue: "#0000ff", 42 | blueviolet: "#8a2be2", 43 | brown: "#a52a2a", 44 | burlywood: "#deb887", 45 | cadetblue: "#5f9ea0", 46 | chartreuse: "#7fff00", 47 | chocolate: "#d2691e", 48 | coral: "#ff7f50", 49 | cornflowerblue: "#6495ed", 50 | cornsilk: "#fff8dc", 51 | crimson: "#dc143c", 52 | cyan: "#00ffff", 53 | darkblue: "#00008b", 54 | darkcyan: "#008b8b", 55 | darkgoldenrod: "#b8860b", 56 | darkgray: "#a9a9a9", 57 | darkgreen: "#006400", 58 | darkgrey: "#a9a9a9", 59 | darkkhaki: "#bdb76b", 60 | darkmagenta: "#8b008b", 61 | darkolivegreen: "#556b2f", 62 | darkorange: "#ff8c00", 63 | darkorchid: "#9932cc", 64 | darkred: "#8b0000", 65 | darksalmon: "#e9967a", 66 | darkseagreen: "#8fbc8f", 67 | darkslateblue: "#483d8b", 68 | darkslategray: "#2f4f4f", 69 | darkslategrey: "#2f4f4f", 70 | darkturquoise: "#00ced1", 71 | darkviolet: "#9400d3", 72 | deeppink: "#ff1493", 73 | deepskyblue: "#00bfff", 74 | dimgray: "#696969", 75 | dimgrey: "#696969", 76 | dodgerblue: "#1e90ff", 77 | firebrick: "#b22222", 78 | floralwhite: "#fffaf0", 79 | forestgreen: "#228b22", 80 | fuchsia: "#ff00ff", 81 | gainsboro: "#dcdcdc", 82 | ghostwhite: "#f8f8ff", 83 | gold: "#ffd700", 84 | goldenrod: "#daa520", 85 | gray: "#808080", 86 | green: "#008000", 87 | greenyellow: "#adff2f", 88 | grey: "#808080", 89 | honeydew: "#f0fff0", 90 | hotpink: "#ff69b4", 91 | indianred: "#cd5c5c", 92 | indigo: "#4b0082", 93 | ivory: "#fffff0", 94 | khaki: "#f0e68c", 95 | lavender: "#e6e6fa", 96 | lavenderblush: "#fff0f5", 97 | lawngreen: "#7cfc00", 98 | lemonchiffon: "#fffacd", 99 | lightblue: "#add8e6", 100 | lightcoral: "#f08080", 101 | lightcyan: "#e0ffff", 102 | lightgoldenrodyellow: "#fafad2", 103 | lightgray: "#d3d3d3", 104 | lightgreen: "#90ee90", 105 | lightgrey: "#d3d3d3", 106 | lightpink: "#ffb6c1", 107 | lightsalmon: "#ffa07a", 108 | lightseagreen: "#20b2aa", 109 | lightskyblue: "#87cefa", 110 | lightslategray: "#778899", 111 | lightslategrey: "#778899", 112 | lightsteelblue: "#b0c4de", 113 | lightyellow: "#ffffe0", 114 | lime: "#00ff00", 115 | limegreen: "#32cd32", 116 | linen: "#faf0e6", 117 | magenta: "#ff00ff", 118 | maroon: "#800000", 119 | mediumaquamarine: "#66cdaa", 120 | mediumblue: "#0000cd", 121 | mediumorchid: "#ba55d3", 122 | mediumpurple: "#9370db", 123 | mediumseagreen: "#3cb371", 124 | mediumslateblue: "#7b68ee", 125 | mediumspringgreen: "#00fa9a", 126 | mediumturquoise: "#48d1cc", 127 | mediumvioletred: "#c71585", 128 | midnightblue: "#191970", 129 | mintcream: "#f5fffa", 130 | mistyrose: "#ffe4e1", 131 | moccasin: "#ffe4b5", 132 | navajowhite: "#ffdead", 133 | navy: "#000080", 134 | oldlace: "#fdf5e6", 135 | olive: "#808000", 136 | olivedrab: "#6b8e23", 137 | orange: "#ffa500", 138 | orangered: "#ff4500", 139 | orchid: "#da70d6", 140 | palegoldenrod: "#eee8aa", 141 | palegreen: "#98fb98", 142 | paleturquoise: "#afeeee", 143 | palevioletred: "#db7093", 144 | papayawhip: "#ffefd5", 145 | peachpuff: "#ffdab9", 146 | peru: "#cd853f", 147 | pink: "#ffc0cb", 148 | plum: "#dda0dd", 149 | powderblue: "#b0e0e6", 150 | purple: "#800080", 151 | rebeccapurple: "#663399", 152 | red: "#ff0000", 153 | rosybrown: "#bc8f8f", 154 | royalblue: "#4169e1", 155 | saddlebrown: "#8b4513", 156 | salmon: "#fa8072", 157 | sandybrown: "#f4a460", 158 | seagreen: "#2e8b57", 159 | seashell: "#fff5ee", 160 | sienna: "#a0522d", 161 | silver: "#c0c0c0", 162 | skyblue: "#87ceeb", 163 | slateblue: "#6a5acd", 164 | slategray: "#708090", 165 | slategrey: "#708090", 166 | snow: "#fffafa", 167 | springgreen: "#00ff7f", 168 | steelblue: "#4682b4", 169 | tan: "#d2b48c", 170 | teal: "#008080", 171 | thistle: "#d8bfd8", 172 | tomato: "#ff6347", 173 | turquoise: "#40e0d0", 174 | violet: "#ee82ee", 175 | wheat: "#f5deb3", 176 | white: "#ffffff", 177 | whitesmoke: "#f5f5f5", 178 | yellow: "#ffff00", 179 | yellowgreen: "#9acd32" 180 | }; 181 | } 182 | }); 183 | 184 | // node_modules/float-hsl2rgb/index.js 185 | var require_float_hsl2rgb = __commonJS({ 186 | "node_modules/float-hsl2rgb/index.js"(exports, module) { 187 | module.exports = hsl2rgb; 188 | function hsl2rgb(hsl) { 189 | var h = hsl[0], s = hsl[1], l = hsl[2], t1, t2, t3, rgb, val; 190 | if (s === 0) { 191 | val = l; 192 | return [val, val, val]; 193 | } 194 | if (l < 0.5) { 195 | t2 = l * (1 + s); 196 | } else { 197 | t2 = l + s - l * s; 198 | } 199 | t1 = 2 * l - t2; 200 | rgb = [0, 0, 0]; 201 | for (var i = 0; i < 3; i++) { 202 | t3 = h + 1 / 3 * -(i - 1); 203 | if (t3 < 0) { 204 | t3++; 205 | } 206 | if (t3 > 1) { 207 | t3--; 208 | } 209 | if (6 * t3 < 1) { 210 | val = t1 + (t2 - t1) * 6 * t3; 211 | } else if (2 * t3 < 1) { 212 | val = t2; 213 | } else if (3 * t3 < 2) { 214 | val = t1 + (t2 - t1) * (2 / 3 - t3) * 6; 215 | } else { 216 | val = t1; 217 | } 218 | rgb[i] = val; 219 | } 220 | return rgb; 221 | } 222 | } 223 | }); 224 | 225 | // node_modules/float-rgb2hsl/index.js 226 | var require_float_rgb2hsl = __commonJS({ 227 | "node_modules/float-rgb2hsl/index.js"(exports, module) { 228 | module.exports = rgb2hsl; 229 | function rgb2hsl(rgb) { 230 | var r = rgb[0], g = rgb[1], b = rgb[2], min = Math.min(r, g, b), max = Math.max(r, g, b), delta = max - min, h, s, l; 231 | if (max === min) { 232 | h = 0; 233 | } else if (r === max) { 234 | h = (g - b) / delta; 235 | } else if (g === max) { 236 | h = 2 + (b - r) / delta; 237 | } else if (b === max) { 238 | h = 4 + (r - g) / delta; 239 | } 240 | h = Math.min(h * 60, 360); 241 | if (h < 0) { 242 | h += 360; 243 | } 244 | l = (min + max) / 2; 245 | if (max === min) { 246 | s = 0; 247 | } else if (l <= 0.5) { 248 | s = delta / (max + min); 249 | } else { 250 | s = delta / (2 - max - min); 251 | } 252 | return [h / 360, s, l]; 253 | } 254 | } 255 | }); 256 | 257 | // node_modules/canvas-sketch-util/lib/wrap.js 258 | var require_wrap = __commonJS({ 259 | "node_modules/canvas-sketch-util/lib/wrap.js"(exports, module) { 260 | module.exports = wrap; 261 | function wrap(value, from, to) { 262 | if (typeof from !== "number" || typeof to !== "number") { 263 | throw new TypeError('Must specify "to" and "from" arguments as numbers'); 264 | } 265 | if (from > to) { 266 | var t = from; 267 | from = to; 268 | to = t; 269 | } 270 | var cycle = to - from; 271 | if (cycle === 0) { 272 | return to; 273 | } 274 | return value - cycle * Math.floor((value - from) / cycle); 275 | } 276 | } 277 | }); 278 | 279 | // node_modules/canvas-sketch-util/lib/hsl.js 280 | var require_hsl = __commonJS({ 281 | "node_modules/canvas-sketch-util/lib/hsl.js"(exports, module) { 282 | var floatHSL2RGB = require_float_hsl2rgb(); 283 | var floatRGB2HSL = require_float_rgb2hsl(); 284 | var wrap = require_wrap(); 285 | module.exports.RGBAToHSLA = RGBAToHSLA; 286 | function RGBAToHSLA(rgba) { 287 | var floatHSL = floatRGB2HSL([rgba[0] / 255, rgba[1] / 255, rgba[2] / 255]); 288 | return [ 289 | Math.max(0, Math.min(360, Math.round(floatHSL[0] * 360))), 290 | Math.max(0, Math.min(100, Math.round(floatHSL[1] * 100))), 291 | Math.max(0, Math.min(100, Math.round(floatHSL[2] * 100))), 292 | rgba[3] 293 | ]; 294 | } 295 | module.exports.HSLAToRGBA = HSLAToRGBA; 296 | function HSLAToRGBA(hsla) { 297 | var hue = wrap(hsla[0], 0, 360); 298 | var floatRGB = floatHSL2RGB([hue / 360, hsla[1] / 100, hsla[2] / 100]); 299 | return [ 300 | Math.max(0, Math.min(255, Math.round(floatRGB[0] * 255))), 301 | Math.max(0, Math.min(255, Math.round(floatRGB[1] * 255))), 302 | Math.max(0, Math.min(255, Math.round(floatRGB[2] * 255))), 303 | hsla[3] 304 | ]; 305 | } 306 | } 307 | }); 308 | 309 | // node_modules/canvas-sketch-util/lib/hex-to-rgba.js 310 | var require_hex_to_rgba = __commonJS({ 311 | "node_modules/canvas-sketch-util/lib/hex-to-rgba.js"(exports, module) { 312 | module.exports = hexToRGBA; 313 | function hexToRGBA(str) { 314 | if (typeof str !== "string") { 315 | throw new TypeError("Hex code parsing must be performed on a string parameter"); 316 | } 317 | str = str.toLowerCase(); 318 | if (!/^#[a-f0-9]+$/.test(str)) { 319 | return null; 320 | } 321 | var hex = str.replace(/^#/, ""); 322 | var alpha = 1; 323 | if (hex.length === 8) { 324 | alpha = parseInt(hex.slice(6, 8), 16) / 255; 325 | hex = hex.slice(0, 6); 326 | } 327 | if (hex.length === 4) { 328 | alpha = parseInt(hex.slice(3, 4).repeat(2), 16) / 255; 329 | hex = hex.slice(0, 3); 330 | } 331 | if (hex.length === 3) { 332 | hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; 333 | } 334 | var num = parseInt(hex, 16); 335 | var red = num >> 16; 336 | var green = num >> 8 & 255; 337 | var blue = num & 255; 338 | return [red, green, blue, alpha]; 339 | } 340 | } 341 | }); 342 | 343 | // node_modules/canvas-sketch-util/lib/rgba-to-hex.js 344 | var require_rgba_to_hex = __commonJS({ 345 | "node_modules/canvas-sketch-util/lib/rgba-to-hex.js"(exports, module) { 346 | module.exports = rgbaToHex; 347 | function rgbaToHex(rgba) { 348 | if (!rgba || !Array.isArray(rgba)) { 349 | throw new TypeError("Must specify an array to convert into a hex code"); 350 | } 351 | var r = Math.max(0, Math.min(255, Math.round(rgba[0] || 0))); 352 | var g = Math.max(0, Math.min(255, Math.round(rgba[1] || 0))); 353 | var b = Math.max(0, Math.min(255, Math.round(rgba[2] || 0))); 354 | var alpha = rgba[3]; 355 | if (typeof alpha === "undefined" || !isFinite(alpha)) { 356 | alpha = 1; 357 | } 358 | var a = Math.max(0, Math.min(255, Math.round(alpha * 255))); 359 | var alphaParam = a === 255 ? "" : (a | 1 << 8).toString(16).slice(1); 360 | var result = (b | g << 8 | r << 16 | 1 << 24).toString(16).slice(1) + alphaParam; 361 | return "#" + result; 362 | } 363 | } 364 | }); 365 | 366 | // node_modules/canvas-sketch-util/lib/css-color.js 367 | var require_css_color = __commonJS({ 368 | "node_modules/canvas-sketch-util/lib/css-color.js"(exports, module) { 369 | var names = require_css_color_names(); 370 | var HSLUtil = require_hsl(); 371 | var hexToRGBA = require_hex_to_rgba(); 372 | var RGBAToHex = require_rgba_to_hex(); 373 | var wrap = require_wrap(); 374 | function parseStyle(str) { 375 | if (typeof str !== "string") { 376 | throw new TypeError("Color parsing must be performed on a string parameter"); 377 | } 378 | str = str.toLowerCase(); 379 | if (str in names) { 380 | str = names[str]; 381 | } else if (str === "transparent") { 382 | str = "#00000000"; 383 | } 384 | var rgba, hsla, hex; 385 | if (/^#[a-f0-9]+$/.test(str)) { 386 | rgba = hexToRGBA(str); 387 | hex = RGBAToHex(rgba); 388 | hsla = HSLUtil.RGBAToHSLA(rgba); 389 | } else { 390 | var match = /^((?:rgb|hsl)a?)\s*\(([^)]*)\)/.exec(str); 391 | if (!match) 392 | return null; 393 | var type = match[1].replace(/a$/, ""); 394 | var parts = match[2].replace(/^\s+|\s+$/g, "").split(/\s*,\s*/).map(function(n, i) { 395 | if (i <= 2) 396 | return Math.round(parseFloat(n) || 0); 397 | else { 398 | n = parseFloat(n); 399 | if (typeof n !== "number" || !isFinite(n)) 400 | n = 1; 401 | return n; 402 | } 403 | }); 404 | if (typeof parts[3] === "undefined" || !isFinite(parts[3])) { 405 | parts[3] = 1; 406 | } 407 | if (type === "rgb") { 408 | hsla = HSLUtil.RGBAToHSLA(parts); 409 | rgba = parts; 410 | } else if (type === "hsl") { 411 | rgba = HSLUtil.HSLAToRGBA(parts); 412 | parts[0] = wrap(parts[0], 0, 360); 413 | hsla = parts; 414 | } 415 | hex = RGBAToHex(rgba); 416 | } 417 | if (!rgba && !hex && !hsla) 418 | return null; 419 | var ret = { 420 | hex, 421 | alpha: rgba[3], 422 | rgb: rgba.slice(0, 3), 423 | rgba, 424 | hsl: hsla.slice(0, 3), 425 | hsla 426 | }; 427 | return ret; 428 | } 429 | module.exports.parse = parseColor; 430 | function parseColor(color) { 431 | if (typeof color === "string") { 432 | return parseStyle(color); 433 | } else if (Array.isArray(color) && color.length >= 3) { 434 | var rgbStr = rgbStyle(color[0], color[1], color[2], color[3]); 435 | return parseStyle(rgbStr); 436 | } else if (color && typeof color === "object") { 437 | var str; 438 | if (color.hex) 439 | str = color.hex; 440 | else if (color.rgba) 441 | str = rgbStyle(color.rgba[0], color.rgba[1], color.rgba[2], color.rgba[3]); 442 | else if (color.hsla) 443 | str = hslStyle(color.hsla[0], color.hsla[1], color.hsla[2], color.hsla[3]); 444 | else if (color.rgb) 445 | str = rgbStyle(color.rgb[0], color.rgb[1], color.rgb[2]); 446 | else if (color.hsl) 447 | str = hslStyle(color.hsl[0], color.hsl[1], color.hsl[2]); 448 | if (str) 449 | return parseStyle(str); 450 | } 451 | return null; 452 | } 453 | module.exports.style = style; 454 | function style(color) { 455 | var result = module.exports.parse(color); 456 | if (result) { 457 | var rgba = result.rgba; 458 | return rgbStyle(rgba[0], rgba[1], rgba[2], rgba[3]); 459 | } 460 | return null; 461 | } 462 | function rgbStyle(r, g, b, a) { 463 | r = Math.max(0, Math.min(255, Math.round(r))); 464 | g = Math.max(0, Math.min(255, Math.round(g))); 465 | b = Math.max(0, Math.min(255, Math.round(b))); 466 | if (a === 1 || !isFinite(a) || typeof a === "undefined") { 467 | return "rgb(" + [r, g, b].join(", ") + ")"; 468 | } else { 469 | a = Math.max(0, Math.min(1, a)); 470 | return "rgba(" + [r, g, b, a].join(", ") + ")"; 471 | } 472 | } 473 | function hslStyle(h, s, l, a) { 474 | h = wrap(h, 0, 360); 475 | h = Math.max(0, Math.min(360, Math.round(h))); 476 | s = Math.max(0, Math.min(100, Math.round(s))); 477 | l = Math.max(0, Math.min(100, Math.round(l))); 478 | if (a === 1 || !isFinite(a) || typeof a === "undefined") { 479 | return "hsl(" + [h, s, l].join(", ") + ")"; 480 | } else { 481 | a = Math.max(0, Math.min(1, a)); 482 | return "hsla(" + [h, s, l, a].join(", ") + ")"; 483 | } 484 | } 485 | } 486 | }); 487 | 488 | // node_modules/canvas-sketch-util/lib/relative-luminance.js 489 | var require_relative_luminance = __commonJS({ 490 | "node_modules/canvas-sketch-util/lib/relative-luminance.js"(exports, module) { 491 | var rc = 0.2126; 492 | var gc = 0.7152; 493 | var bc = 0.0722; 494 | var lowc = 1 / 12.92; 495 | function adjustGamma(a) { 496 | return Math.pow((a + 0.055) / 1.055, 2.4); 497 | } 498 | module.exports = relativeLuminance; 499 | function relativeLuminance(rgb) { 500 | var rsrgb = rgb[0] / 255; 501 | var gsrgb = rgb[1] / 255; 502 | var bsrgb = rgb[2] / 255; 503 | var r = rsrgb <= 0.03928 ? rsrgb * lowc : adjustGamma(rsrgb); 504 | var g = gsrgb <= 0.03928 ? gsrgb * lowc : adjustGamma(gsrgb); 505 | var b = bsrgb <= 0.03928 ? bsrgb * lowc : adjustGamma(bsrgb); 506 | return r * rc + g * gc + b * bc; 507 | } 508 | } 509 | }); 510 | 511 | // node_modules/canvas-sketch-util/color.js 512 | var require_color = __commonJS({ 513 | "node_modules/canvas-sketch-util/color.js"(exports, module) { 514 | var cssColor = require_css_color(); 515 | var names = require_css_color_names(); 516 | var rgbLuminance = require_relative_luminance(); 517 | var HSLUtil = require_hsl(); 518 | var hexToRGBA = require_hex_to_rgba(); 519 | var RGBAToHex = require_rgba_to_hex(); 520 | module.exports.parse = cssColor.parse; 521 | module.exports.style = cssColor.style; 522 | module.exports.names = names; 523 | module.exports.relativeLuminance = function relativeLuminance(color) { 524 | var result = module.exports.parse(color); 525 | if (!result) 526 | return null; 527 | return rgbLuminance(result.rgb); 528 | }; 529 | module.exports.contrastRatio = function contrastRatio(colorA, colorB) { 530 | var a = module.exports.relativeLuminance(colorA); 531 | var b = module.exports.relativeLuminance(colorB); 532 | if (a == null || b == null) 533 | return null; 534 | var l1 = Math.max(a, b); 535 | var l2 = Math.min(a, b); 536 | return (l1 + 0.05) / (l2 + 0.05); 537 | }; 538 | module.exports.offsetHSL = function(color, h, s, l) { 539 | var result = module.exports.parse(color); 540 | if (!result) 541 | return null; 542 | result.hsla[0] += h || 0; 543 | result.hsla[1] = Math.max(0, Math.min(100, result.hsla[1] + (s || 0))); 544 | result.hsla[2] = Math.max(0, Math.min(100, result.hsla[2] + (l || 0))); 545 | return module.exports.parse({ hsla: result.hsla }); 546 | }; 547 | module.exports.blend = function(background, foreground, opacity) { 548 | var bg = module.exports.parse(background); 549 | var fg = module.exports.parse(foreground); 550 | if (bg == null || fg == null) 551 | return null; 552 | var c0 = bg.rgba; 553 | var c1 = fg.rgba; 554 | opacity = typeof opacity === "number" && isFinite(opacity) ? opacity : 1; 555 | var alpha = opacity * c1[3]; 556 | if (alpha >= 1) { 557 | return fg; 558 | } 559 | for (var i = 0; i < 3; i++) { 560 | c1[i] = c1[i] * alpha + c0[i] * (c0[3] * (1 - alpha)); 561 | } 562 | c1[3] = Math.max(0, Math.min(1, alpha + c0[3] * (1 - alpha))); 563 | return module.exports.parse(c1); 564 | }; 565 | module.exports.hexToRGBA = hexToRGBA; 566 | module.exports.RGBAToHex = RGBAToHex; 567 | module.exports.RGBAToHSLA = HSLUtil.RGBAToHSLA; 568 | module.exports.HSLAToRGBA = HSLUtil.HSLAToRGBA; 569 | } 570 | }); 571 | 572 | // src/renderer.js 573 | var import_color = __toESM(require_color(), 1); 574 | function createContext(frame, { width, height }) { 575 | let _fillStyle = { r: 0, g: 0, b: 0 }; 576 | const context = { 577 | get fillStyle() { 578 | return _fillStyle; 579 | }, 580 | set fillStyle(f) { 581 | try { 582 | if (typeof f !== "string") 583 | f = f.toString(); 584 | const [r, g, b] = import_color.default.parse(f).rgb.map((n) => n / 255); 585 | _fillStyle = { r, g, b }; 586 | } catch (err) { 587 | _fillStyle = { r: 0, g: 0, b: 0 }; 588 | } 589 | }, 590 | fillRect(x, y, w, h) { 591 | const child = figma.createRectangle(); 592 | const newColor = Object.assign({}, _fillStyle); 593 | child.x = x; 594 | child.y = y; 595 | child.resize(w, h); 596 | child.fills = [{ type: "SOLID", color: newColor }]; 597 | frame.appendChild(child); 598 | }, 599 | fillEllipse(x, y, w, h = w) { 600 | const child = figma.createEllipse(); 601 | const newColor = Object.assign({}, _fillStyle); 602 | child.x = x; 603 | child.y = y; 604 | child.resize(w, h); 605 | child.fills = [{ type: "SOLID", color: newColor }]; 606 | frame.appendChild(child); 607 | } 608 | }; 609 | return context; 610 | } 611 | 612 | // src/util.js 613 | function getRandomHash() { 614 | let result = "0x"; 615 | for (let i = 64; i > 0; --i) 616 | result += "0123456789abcdef"[~~(Math.random() * 16)]; 617 | return result; 618 | } 619 | var hash; 620 | var xs_state; 621 | var originalState; 622 | var pureMathRandom = Math.random; 623 | var getSeed = () => hash; 624 | var setSeed = (h) => { 625 | hash = h; 626 | console.log("Random Seed:", hash); 627 | xs_state = Uint32Array.from( 628 | [0, 0, 0, 0].map((_, i) => parseInt(hash.substr(i * 8 + 2, 8), 16)) 629 | ); 630 | originalState = xs_state.slice(); 631 | }; 632 | var randomizeSeed = () => { 633 | setSeed(getRandomHash()); 634 | }; 635 | var prng = () => { 636 | let s, t = xs_state[3]; 637 | xs_state[3] = xs_state[2]; 638 | xs_state[2] = xs_state[1]; 639 | xs_state[1] = s = xs_state[0]; 640 | t ^= t << 11; 641 | t ^= t >>> 8; 642 | xs_state[0] = t ^ s ^ s >>> 19; 643 | return xs_state[0] / 4294967296; 644 | }; 645 | function patchMath() { 646 | Math.random = () => prng(); 647 | } 648 | function unpatchMath() { 649 | Math.random = pureMathRandom; 650 | } 651 | var reseed = () => { 652 | xs_state.set(originalState); 653 | }; 654 | randomizeSeed(); 655 | 656 | // src/plugin.js 657 | var quitMessage = "Plugin closed"; 658 | var message; 659 | var msgTimeout; 660 | var notify = (msg, opts = {}) => { 661 | if (message && message.cancel) { 662 | message.cancel(); 663 | } 664 | clearTimeout(msgTimeout); 665 | msgTimeout = setTimeout(() => { 666 | message = figma.notify(msg, opts); 667 | }, 0); 668 | }; 669 | var exit = (msg) => figma.closePlugin(msg); 670 | var connected = false; 671 | var hasError = false; 672 | var curState; 673 | var showError = (message2, lineNumber) => { 674 | hasError = true; 675 | connected = false; 676 | notify(`Script Error: ${message2} on line ${lineNumber}`, { 677 | error: true, 678 | timeout: Infinity, 679 | onDequeue: (reason) => { 680 | exit(quitMessage); 681 | } 682 | }); 683 | }; 684 | var reDraw = (useOldSeed = true) => { 685 | if (curState && curState.error) { 686 | const { lineNumber, message: message2 } = curState.error; 687 | showError(message2, lineNumber); 688 | return; 689 | } 690 | try { 691 | const curCode = curState ? curState.code : ""; 692 | const src = `const module = { 693 | id: ".", 694 | exports: {}, 695 | parent: null 696 | }; 697 | ${curCode}; 698 | return _program_;`; 699 | reseed(); 700 | const runner = new Function(src); 701 | const mExports = runner() || {}; 702 | if (mExports && typeof mExports.default === "function") { 703 | draw(mExports.default, useOldSeed); 704 | } 705 | const wasError = hasError; 706 | hasError = false; 707 | if (wasError !== hasError) { 708 | connected = false; 709 | firstConnect(); 710 | } 711 | } catch (err) { 712 | const { lineNumber, message: message2 } = err; 713 | console.error("Error evaluating script:"); 714 | console.error(err.message); 715 | console.error(err.stack); 716 | showError(message2, lineNumber); 717 | } 718 | }; 719 | var firstConnect = () => { 720 | connected = true; 721 | const file = (curState ? curState.file : null) || "[unknown]"; 722 | notify(`\u2728 Connected to ${file}`, { 723 | timeout: Infinity, 724 | button: { 725 | text: "\u{1F3B2}", 726 | action() { 727 | randomizeSeed(); 728 | reDraw(false); 729 | return false; 730 | } 731 | }, 732 | onDequeue: (reason) => { 733 | exit(quitMessage); 734 | } 735 | }); 736 | }; 737 | function findCanvas(expectedHash) { 738 | return figma.currentPage.findOne((node) => { 739 | const str = node.getPluginData("sketch"); 740 | if (str) { 741 | try { 742 | const data = JSON.parse(str); 743 | return data.hash === expectedHash; 744 | } catch (err) { 745 | return false; 746 | } 747 | } else 748 | return false; 749 | }); 750 | } 751 | function getCanvas(useOldSeed = true) { 752 | const expectedHash = curState.hash; 753 | let doc = findCanvas(expectedHash); 754 | if (!doc) { 755 | const frame = figma.createFrame(); 756 | frame.resize(1024, 1024); 757 | doc = frame; 758 | } else { 759 | if (useOldSeed) { 760 | try { 761 | const data = JSON.parse(doc.getPluginData("sketch")); 762 | if (data.seed) { 763 | setSeed(data.seed); 764 | } 765 | } catch (err) { 766 | } 767 | } 768 | } 769 | const curStateWithSeed = Object.assign({}, curState, { seed: getSeed() }); 770 | doc.setPluginData("sketch", JSON.stringify(curStateWithSeed)); 771 | doc.name = curStateWithSeed.name; 772 | const oldSel = figma.currentPage.selection; 773 | if (!oldSel.includes(doc)) { 774 | figma.currentPage.selection = [doc]; 775 | figma.viewport.scrollAndZoomIntoView([doc]); 776 | } 777 | return doc; 778 | } 779 | function draw(fn, useOldSeed = true) { 780 | const doc = getCanvas(useOldSeed); 781 | for (const child of doc.children) { 782 | child.remove(); 783 | } 784 | const { width, height } = doc; 785 | const context = createContext(doc, { width, height }); 786 | fn({ context, width, height }); 787 | doc.expanded = false; 788 | } 789 | async function run() { 790 | patchMath(); 791 | figma.on("close", () => { 792 | unpatchMath(); 793 | }); 794 | figma.showUI(__html__, { visible: false }); 795 | figma.ui.onmessage = (msg) => { 796 | if (msg.type === "disconnect") { 797 | exit(`\u26A0\uFE0F ${msg.reason}`); 798 | } else if (msg.type === "error") { 799 | exit(`\u26A0\uFE0F ${msg.reason}`); 800 | } else if (msg.type === "update") { 801 | curState = Object.assign({}, msg); 802 | if (!connected) { 803 | firstConnect(); 804 | } 805 | reDraw(true); 806 | } 807 | }; 808 | } 809 | run(); 810 | })(); 811 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LocalWebsockets", 3 | "id": "1243616542221478810", 4 | "api": "1.0.0", 5 | "main": "dist/plugin.js", 6 | "capabilities": [], 7 | "enableProposedApi": false, 8 | "editorType": ["figma"], 9 | "ui": "src/ui.html" 10 | } 11 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-plugin-websockets", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "figma-plugin-websockets", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@parcel/watcher": "^2.1.0", 13 | "canvas-sketch-util": "^1.10.0", 14 | "esbuild": "^0.17.19", 15 | "express": "^4.18.2", 16 | "seed-random": "^2.2.0", 17 | "ws": "^8.13.0" 18 | } 19 | }, 20 | "node_modules/@esbuild/darwin-arm64": { 21 | "version": "0.17.19", 22 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", 23 | "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", 24 | "cpu": [ 25 | "arm64" 26 | ], 27 | "optional": true, 28 | "os": [ 29 | "darwin" 30 | ], 31 | "engines": { 32 | "node": ">=12" 33 | } 34 | }, 35 | "node_modules/@parcel/watcher": { 36 | "version": "2.1.0", 37 | "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.1.0.tgz", 38 | "integrity": "sha512-8s8yYjd19pDSsBpbkOHnT6Z2+UJSuLQx61pCFM0s5wSRvKCEMDjd/cHY3/GI1szHIWbpXpsJdg3V6ISGGx9xDw==", 39 | "hasInstallScript": true, 40 | "dependencies": { 41 | "is-glob": "^4.0.3", 42 | "micromatch": "^4.0.5", 43 | "node-addon-api": "^3.2.1", 44 | "node-gyp-build": "^4.3.0" 45 | }, 46 | "engines": { 47 | "node": ">= 10.0.0" 48 | }, 49 | "funding": { 50 | "type": "opencollective", 51 | "url": "https://opencollective.com/parcel" 52 | } 53 | }, 54 | "node_modules/abs-svg-path": { 55 | "version": "0.1.1", 56 | "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", 57 | "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==" 58 | }, 59 | "node_modules/accepts": { 60 | "version": "1.3.8", 61 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 62 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 63 | "dependencies": { 64 | "mime-types": "~2.1.34", 65 | "negotiator": "0.6.3" 66 | }, 67 | "engines": { 68 | "node": ">= 0.6" 69 | } 70 | }, 71 | "node_modules/adaptive-bezier-curve": { 72 | "version": "1.0.3", 73 | "resolved": "https://registry.npmjs.org/adaptive-bezier-curve/-/adaptive-bezier-curve-1.0.3.tgz", 74 | "integrity": "sha512-mDcwN284LlNAdunqnVmS0PAoDNHKze/PY8zvpCdxzyXD+ZZFeMWQ3FKNBw0VMOd9IfnhIyzAWJDXzRcWnXtoSg==" 75 | }, 76 | "node_modules/almost-equal": { 77 | "version": "1.1.0", 78 | "resolved": "https://registry.npmjs.org/almost-equal/-/almost-equal-1.1.0.tgz", 79 | "integrity": "sha512-0V/PkoculFl5+0Lp47JoxUcO0xSxhIBvm+BxHdD/OgXNmdRpRHCFnKVuUoWyS9EzQP+otSGv0m9Lb4yVkQBn2A==" 80 | }, 81 | "node_modules/an-array": { 82 | "version": "1.0.0", 83 | "resolved": "https://registry.npmjs.org/an-array/-/an-array-1.0.0.tgz", 84 | "integrity": "sha512-M175GYI7RmsYu24Ok383yZQa3eveDfNnmhTe3OQ3bm70bEovz2gWenH+ST/n32M8lrwLWk74hcPds5CDRPe2wg==" 85 | }, 86 | "node_modules/array-almost-equal": { 87 | "version": "1.0.0", 88 | "resolved": "https://registry.npmjs.org/array-almost-equal/-/array-almost-equal-1.0.0.tgz", 89 | "integrity": "sha512-NiQJq0vH5nEebm7FMs52fli23nwCh+RBKZZtA5g5qUmqGAkb9n8/WOTW8zjDocLADXXM1CRanFfPTMdrI2yZwg==", 90 | "dependencies": { 91 | "almost-equal": "0.0.0", 92 | "an-array": "^1.0.0" 93 | } 94 | }, 95 | "node_modules/array-almost-equal/node_modules/almost-equal": { 96 | "version": "0.0.0", 97 | "resolved": "https://registry.npmjs.org/almost-equal/-/almost-equal-0.0.0.tgz", 98 | "integrity": "sha512-/wvWNupeAr7sp7J2ZXvQvd97uyxsokx4TXdV6YMhd/EDwctlgY3wUGZCgQx1sJQyxU+c8EWRCWsLGOumdiS9aw==" 99 | }, 100 | "node_modules/array-flatten": { 101 | "version": "1.1.1", 102 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 103 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 104 | }, 105 | "node_modules/body-parser": { 106 | "version": "1.20.1", 107 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", 108 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", 109 | "dependencies": { 110 | "bytes": "3.1.2", 111 | "content-type": "~1.0.4", 112 | "debug": "2.6.9", 113 | "depd": "2.0.0", 114 | "destroy": "1.2.0", 115 | "http-errors": "2.0.0", 116 | "iconv-lite": "0.4.24", 117 | "on-finished": "2.4.1", 118 | "qs": "6.11.0", 119 | "raw-body": "2.5.1", 120 | "type-is": "~1.6.18", 121 | "unpipe": "1.0.0" 122 | }, 123 | "engines": { 124 | "node": ">= 0.8", 125 | "npm": "1.2.8000 || >= 1.4.16" 126 | } 127 | }, 128 | "node_modules/braces": { 129 | "version": "3.0.2", 130 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 131 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 132 | "dependencies": { 133 | "fill-range": "^7.0.1" 134 | }, 135 | "engines": { 136 | "node": ">=8" 137 | } 138 | }, 139 | "node_modules/bytes": { 140 | "version": "3.1.2", 141 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 142 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 143 | "engines": { 144 | "node": ">= 0.8" 145 | } 146 | }, 147 | "node_modules/call-bind": { 148 | "version": "1.0.2", 149 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 150 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 151 | "dependencies": { 152 | "function-bind": "^1.1.1", 153 | "get-intrinsic": "^1.0.2" 154 | }, 155 | "funding": { 156 | "url": "https://github.com/sponsors/ljharb" 157 | } 158 | }, 159 | "node_modules/canvas-sketch-util": { 160 | "version": "1.10.0", 161 | "resolved": "https://registry.npmjs.org/canvas-sketch-util/-/canvas-sketch-util-1.10.0.tgz", 162 | "integrity": "sha512-IiJWEQDBinl6KZVLYcdp79ZJXXKsS1HuDUoWtwWh0NJMVUA6jaM0pxcBRYiaujBXjQFPT91pl8L9iFFmma3+zA==", 163 | "dependencies": { 164 | "abs-svg-path": "^0.1.1", 165 | "almost-equal": "^1.1.0", 166 | "array-almost-equal": "^1.0.0", 167 | "clone": "^2.1.2", 168 | "color-luminance": "^2.1.0", 169 | "convert-length": "^1.0.1", 170 | "d3-path": "^1.0.8", 171 | "defined": "^1.0.0", 172 | "float-hsl2rgb": "^1.0.2", 173 | "float-rgb2hsl": "^1.0.1", 174 | "lineclip": "^1.1.5", 175 | "normalize-svg-path": "^1.0.1", 176 | "parse-color": "^1.0.0", 177 | "parse-svg-path": "^0.1.2", 178 | "primitive-quad": "^2.0.0", 179 | "regl": "^1.3.7", 180 | "seed-random": "^2.2.0", 181 | "simplex-noise": "^2.4.0", 182 | "svg-path-contours": "^2.0.0" 183 | } 184 | }, 185 | "node_modules/clone": { 186 | "version": "2.1.2", 187 | "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", 188 | "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", 189 | "engines": { 190 | "node": ">=0.8" 191 | } 192 | }, 193 | "node_modules/color-convert": { 194 | "version": "0.5.3", 195 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", 196 | "integrity": "sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==" 197 | }, 198 | "node_modules/color-luminance": { 199 | "version": "2.1.0", 200 | "resolved": "https://registry.npmjs.org/color-luminance/-/color-luminance-2.1.0.tgz", 201 | "integrity": "sha512-J2WalPvlpNvOYW8YNxffayMHmA/Ysbx9crRdWorAG6mSbO22yliPvhldw0slsRhpFiQpFdHpFo69X3v9OzklOA==" 202 | }, 203 | "node_modules/content-disposition": { 204 | "version": "0.5.4", 205 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 206 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 207 | "dependencies": { 208 | "safe-buffer": "5.2.1" 209 | }, 210 | "engines": { 211 | "node": ">= 0.6" 212 | } 213 | }, 214 | "node_modules/content-type": { 215 | "version": "1.0.5", 216 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 217 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 218 | "engines": { 219 | "node": ">= 0.6" 220 | } 221 | }, 222 | "node_modules/convert-length": { 223 | "version": "1.0.1", 224 | "resolved": "https://registry.npmjs.org/convert-length/-/convert-length-1.0.1.tgz", 225 | "integrity": "sha512-w94Vge3sck6J1NHuA0du367/JTbj1zfjTapX7ixR2W+KlU4zb+NRTbou3x9nTpm5vlcXmjoaUJQpVN9pULCdxQ==", 226 | "dependencies": { 227 | "defined": "^1.0.0" 228 | } 229 | }, 230 | "node_modules/cookie": { 231 | "version": "0.5.0", 232 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 233 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", 234 | "engines": { 235 | "node": ">= 0.6" 236 | } 237 | }, 238 | "node_modules/cookie-signature": { 239 | "version": "1.0.6", 240 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 241 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 242 | }, 243 | "node_modules/d3-path": { 244 | "version": "1.0.9", 245 | "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", 246 | "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" 247 | }, 248 | "node_modules/debug": { 249 | "version": "2.6.9", 250 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 251 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 252 | "dependencies": { 253 | "ms": "2.0.0" 254 | } 255 | }, 256 | "node_modules/defined": { 257 | "version": "1.0.1", 258 | "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", 259 | "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", 260 | "funding": { 261 | "url": "https://github.com/sponsors/ljharb" 262 | } 263 | }, 264 | "node_modules/depd": { 265 | "version": "2.0.0", 266 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 267 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 268 | "engines": { 269 | "node": ">= 0.8" 270 | } 271 | }, 272 | "node_modules/destroy": { 273 | "version": "1.2.0", 274 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 275 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 276 | "engines": { 277 | "node": ">= 0.8", 278 | "npm": "1.2.8000 || >= 1.4.16" 279 | } 280 | }, 281 | "node_modules/ee-first": { 282 | "version": "1.1.1", 283 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 284 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 285 | }, 286 | "node_modules/encodeurl": { 287 | "version": "1.0.2", 288 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 289 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 290 | "engines": { 291 | "node": ">= 0.8" 292 | } 293 | }, 294 | "node_modules/esbuild": { 295 | "version": "0.17.19", 296 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", 297 | "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", 298 | "hasInstallScript": true, 299 | "bin": { 300 | "esbuild": "bin/esbuild" 301 | }, 302 | "engines": { 303 | "node": ">=12" 304 | }, 305 | "optionalDependencies": { 306 | "@esbuild/android-arm": "0.17.19", 307 | "@esbuild/android-arm64": "0.17.19", 308 | "@esbuild/android-x64": "0.17.19", 309 | "@esbuild/darwin-arm64": "0.17.19", 310 | "@esbuild/darwin-x64": "0.17.19", 311 | "@esbuild/freebsd-arm64": "0.17.19", 312 | "@esbuild/freebsd-x64": "0.17.19", 313 | "@esbuild/linux-arm": "0.17.19", 314 | "@esbuild/linux-arm64": "0.17.19", 315 | "@esbuild/linux-ia32": "0.17.19", 316 | "@esbuild/linux-loong64": "0.17.19", 317 | "@esbuild/linux-mips64el": "0.17.19", 318 | "@esbuild/linux-ppc64": "0.17.19", 319 | "@esbuild/linux-riscv64": "0.17.19", 320 | "@esbuild/linux-s390x": "0.17.19", 321 | "@esbuild/linux-x64": "0.17.19", 322 | "@esbuild/netbsd-x64": "0.17.19", 323 | "@esbuild/openbsd-x64": "0.17.19", 324 | "@esbuild/sunos-x64": "0.17.19", 325 | "@esbuild/win32-arm64": "0.17.19", 326 | "@esbuild/win32-ia32": "0.17.19", 327 | "@esbuild/win32-x64": "0.17.19" 328 | } 329 | }, 330 | "node_modules/escape-html": { 331 | "version": "1.0.3", 332 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 333 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 334 | }, 335 | "node_modules/etag": { 336 | "version": "1.8.1", 337 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 338 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 339 | "engines": { 340 | "node": ">= 0.6" 341 | } 342 | }, 343 | "node_modules/express": { 344 | "version": "4.18.2", 345 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", 346 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", 347 | "dependencies": { 348 | "accepts": "~1.3.8", 349 | "array-flatten": "1.1.1", 350 | "body-parser": "1.20.1", 351 | "content-disposition": "0.5.4", 352 | "content-type": "~1.0.4", 353 | "cookie": "0.5.0", 354 | "cookie-signature": "1.0.6", 355 | "debug": "2.6.9", 356 | "depd": "2.0.0", 357 | "encodeurl": "~1.0.2", 358 | "escape-html": "~1.0.3", 359 | "etag": "~1.8.1", 360 | "finalhandler": "1.2.0", 361 | "fresh": "0.5.2", 362 | "http-errors": "2.0.0", 363 | "merge-descriptors": "1.0.1", 364 | "methods": "~1.1.2", 365 | "on-finished": "2.4.1", 366 | "parseurl": "~1.3.3", 367 | "path-to-regexp": "0.1.7", 368 | "proxy-addr": "~2.0.7", 369 | "qs": "6.11.0", 370 | "range-parser": "~1.2.1", 371 | "safe-buffer": "5.2.1", 372 | "send": "0.18.0", 373 | "serve-static": "1.15.0", 374 | "setprototypeof": "1.2.0", 375 | "statuses": "2.0.1", 376 | "type-is": "~1.6.18", 377 | "utils-merge": "1.0.1", 378 | "vary": "~1.1.2" 379 | }, 380 | "engines": { 381 | "node": ">= 0.10.0" 382 | } 383 | }, 384 | "node_modules/fill-range": { 385 | "version": "7.0.1", 386 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 387 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 388 | "dependencies": { 389 | "to-regex-range": "^5.0.1" 390 | }, 391 | "engines": { 392 | "node": ">=8" 393 | } 394 | }, 395 | "node_modules/finalhandler": { 396 | "version": "1.2.0", 397 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 398 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 399 | "dependencies": { 400 | "debug": "2.6.9", 401 | "encodeurl": "~1.0.2", 402 | "escape-html": "~1.0.3", 403 | "on-finished": "2.4.1", 404 | "parseurl": "~1.3.3", 405 | "statuses": "2.0.1", 406 | "unpipe": "~1.0.0" 407 | }, 408 | "engines": { 409 | "node": ">= 0.8" 410 | } 411 | }, 412 | "node_modules/float-hsl2rgb": { 413 | "version": "1.0.2", 414 | "resolved": "https://registry.npmjs.org/float-hsl2rgb/-/float-hsl2rgb-1.0.2.tgz", 415 | "integrity": "sha512-O9YGK/8X0tl2GpT9SVk9wSPTVv1IkAjikhKdHkbHbC7X9aX+64i3HPeWX7zwStDuO7T3KIamHC4B16T80mMeXA==" 416 | }, 417 | "node_modules/float-rgb2hsl": { 418 | "version": "1.0.1", 419 | "resolved": "https://registry.npmjs.org/float-rgb2hsl/-/float-rgb2hsl-1.0.1.tgz", 420 | "integrity": "sha512-sdMM9qC3IRI/M7J1htyVfNFD4pCi6MIznXTXTq3/h4C+qtMz6AMTRHwcuD6ScswVXMS/F+0B5DNn5HINQK0iOA==" 421 | }, 422 | "node_modules/forwarded": { 423 | "version": "0.2.0", 424 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 425 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 426 | "engines": { 427 | "node": ">= 0.6" 428 | } 429 | }, 430 | "node_modules/fresh": { 431 | "version": "0.5.2", 432 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 433 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 434 | "engines": { 435 | "node": ">= 0.6" 436 | } 437 | }, 438 | "node_modules/function-bind": { 439 | "version": "1.1.1", 440 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 441 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 442 | }, 443 | "node_modules/get-intrinsic": { 444 | "version": "1.2.1", 445 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", 446 | "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", 447 | "dependencies": { 448 | "function-bind": "^1.1.1", 449 | "has": "^1.0.3", 450 | "has-proto": "^1.0.1", 451 | "has-symbols": "^1.0.3" 452 | }, 453 | "funding": { 454 | "url": "https://github.com/sponsors/ljharb" 455 | } 456 | }, 457 | "node_modules/has": { 458 | "version": "1.0.3", 459 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 460 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 461 | "dependencies": { 462 | "function-bind": "^1.1.1" 463 | }, 464 | "engines": { 465 | "node": ">= 0.4.0" 466 | } 467 | }, 468 | "node_modules/has-proto": { 469 | "version": "1.0.1", 470 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", 471 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", 472 | "engines": { 473 | "node": ">= 0.4" 474 | }, 475 | "funding": { 476 | "url": "https://github.com/sponsors/ljharb" 477 | } 478 | }, 479 | "node_modules/has-symbols": { 480 | "version": "1.0.3", 481 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 482 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 483 | "engines": { 484 | "node": ">= 0.4" 485 | }, 486 | "funding": { 487 | "url": "https://github.com/sponsors/ljharb" 488 | } 489 | }, 490 | "node_modules/http-errors": { 491 | "version": "2.0.0", 492 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 493 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 494 | "dependencies": { 495 | "depd": "2.0.0", 496 | "inherits": "2.0.4", 497 | "setprototypeof": "1.2.0", 498 | "statuses": "2.0.1", 499 | "toidentifier": "1.0.1" 500 | }, 501 | "engines": { 502 | "node": ">= 0.8" 503 | } 504 | }, 505 | "node_modules/iconv-lite": { 506 | "version": "0.4.24", 507 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 508 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 509 | "dependencies": { 510 | "safer-buffer": ">= 2.1.2 < 3" 511 | }, 512 | "engines": { 513 | "node": ">=0.10.0" 514 | } 515 | }, 516 | "node_modules/inherits": { 517 | "version": "2.0.4", 518 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 519 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 520 | }, 521 | "node_modules/ipaddr.js": { 522 | "version": "1.9.1", 523 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 524 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 525 | "engines": { 526 | "node": ">= 0.10" 527 | } 528 | }, 529 | "node_modules/is-extglob": { 530 | "version": "2.1.1", 531 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 532 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 533 | "engines": { 534 | "node": ">=0.10.0" 535 | } 536 | }, 537 | "node_modules/is-glob": { 538 | "version": "4.0.3", 539 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 540 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 541 | "dependencies": { 542 | "is-extglob": "^2.1.1" 543 | }, 544 | "engines": { 545 | "node": ">=0.10.0" 546 | } 547 | }, 548 | "node_modules/is-number": { 549 | "version": "7.0.0", 550 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 551 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 552 | "engines": { 553 | "node": ">=0.12.0" 554 | } 555 | }, 556 | "node_modules/lineclip": { 557 | "version": "1.1.5", 558 | "resolved": "https://registry.npmjs.org/lineclip/-/lineclip-1.1.5.tgz", 559 | "integrity": "sha512-KlA/wRSjpKl7tS9iRUdlG72oQ7qZ1IlVbVgHwoO10TBR/4gQ86uhKow6nlzMAJJhjCWKto8OeoAzzIzKSmN25A==" 560 | }, 561 | "node_modules/media-typer": { 562 | "version": "0.3.0", 563 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 564 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 565 | "engines": { 566 | "node": ">= 0.6" 567 | } 568 | }, 569 | "node_modules/merge-descriptors": { 570 | "version": "1.0.1", 571 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 572 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 573 | }, 574 | "node_modules/methods": { 575 | "version": "1.1.2", 576 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 577 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 578 | "engines": { 579 | "node": ">= 0.6" 580 | } 581 | }, 582 | "node_modules/micromatch": { 583 | "version": "4.0.5", 584 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", 585 | "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", 586 | "dependencies": { 587 | "braces": "^3.0.2", 588 | "picomatch": "^2.3.1" 589 | }, 590 | "engines": { 591 | "node": ">=8.6" 592 | } 593 | }, 594 | "node_modules/mime": { 595 | "version": "1.6.0", 596 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 597 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 598 | "bin": { 599 | "mime": "cli.js" 600 | }, 601 | "engines": { 602 | "node": ">=4" 603 | } 604 | }, 605 | "node_modules/mime-db": { 606 | "version": "1.52.0", 607 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 608 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 609 | "engines": { 610 | "node": ">= 0.6" 611 | } 612 | }, 613 | "node_modules/mime-types": { 614 | "version": "2.1.35", 615 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 616 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 617 | "dependencies": { 618 | "mime-db": "1.52.0" 619 | }, 620 | "engines": { 621 | "node": ">= 0.6" 622 | } 623 | }, 624 | "node_modules/ms": { 625 | "version": "2.0.0", 626 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 627 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 628 | }, 629 | "node_modules/negotiator": { 630 | "version": "0.6.3", 631 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 632 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 633 | "engines": { 634 | "node": ">= 0.6" 635 | } 636 | }, 637 | "node_modules/node-addon-api": { 638 | "version": "3.2.1", 639 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", 640 | "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" 641 | }, 642 | "node_modules/node-gyp-build": { 643 | "version": "4.6.0", 644 | "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", 645 | "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", 646 | "bin": { 647 | "node-gyp-build": "bin.js", 648 | "node-gyp-build-optional": "optional.js", 649 | "node-gyp-build-test": "build-test.js" 650 | } 651 | }, 652 | "node_modules/normalize-svg-path": { 653 | "version": "1.1.0", 654 | "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", 655 | "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", 656 | "dependencies": { 657 | "svg-arc-to-cubic-bezier": "^3.0.0" 658 | } 659 | }, 660 | "node_modules/object-inspect": { 661 | "version": "1.12.3", 662 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", 663 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", 664 | "funding": { 665 | "url": "https://github.com/sponsors/ljharb" 666 | } 667 | }, 668 | "node_modules/on-finished": { 669 | "version": "2.4.1", 670 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 671 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 672 | "dependencies": { 673 | "ee-first": "1.1.1" 674 | }, 675 | "engines": { 676 | "node": ">= 0.8" 677 | } 678 | }, 679 | "node_modules/parse-color": { 680 | "version": "1.0.0", 681 | "resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz", 682 | "integrity": "sha512-fuDHYgFHJGbpGMgw9skY/bj3HL/Jrn4l/5rSspy00DoT4RyLnDcRvPxdZ+r6OFwIsgAuhDh4I09tAId4mI12bw==", 683 | "dependencies": { 684 | "color-convert": "~0.5.0" 685 | } 686 | }, 687 | "node_modules/parse-svg-path": { 688 | "version": "0.1.2", 689 | "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", 690 | "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==" 691 | }, 692 | "node_modules/parseurl": { 693 | "version": "1.3.3", 694 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 695 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 696 | "engines": { 697 | "node": ">= 0.8" 698 | } 699 | }, 700 | "node_modules/path-to-regexp": { 701 | "version": "0.1.7", 702 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 703 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 704 | }, 705 | "node_modules/picomatch": { 706 | "version": "2.3.1", 707 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 708 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 709 | "engines": { 710 | "node": ">=8.6" 711 | }, 712 | "funding": { 713 | "url": "https://github.com/sponsors/jonschlinkert" 714 | } 715 | }, 716 | "node_modules/primitive-quad": { 717 | "version": "2.0.0", 718 | "resolved": "https://registry.npmjs.org/primitive-quad/-/primitive-quad-2.0.0.tgz", 719 | "integrity": "sha512-JlPRH5x3QEqoIeHvxHY6ePLhpjSG7FEGxitR8sbfkfCjJK+OEVODTE4j6US12gMqvQqXzRGkkmuYYvJCozifMA==" 720 | }, 721 | "node_modules/proxy-addr": { 722 | "version": "2.0.7", 723 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 724 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 725 | "dependencies": { 726 | "forwarded": "0.2.0", 727 | "ipaddr.js": "1.9.1" 728 | }, 729 | "engines": { 730 | "node": ">= 0.10" 731 | } 732 | }, 733 | "node_modules/qs": { 734 | "version": "6.11.0", 735 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 736 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 737 | "dependencies": { 738 | "side-channel": "^1.0.4" 739 | }, 740 | "engines": { 741 | "node": ">=0.6" 742 | }, 743 | "funding": { 744 | "url": "https://github.com/sponsors/ljharb" 745 | } 746 | }, 747 | "node_modules/range-parser": { 748 | "version": "1.2.1", 749 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 750 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 751 | "engines": { 752 | "node": ">= 0.6" 753 | } 754 | }, 755 | "node_modules/raw-body": { 756 | "version": "2.5.1", 757 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 758 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 759 | "dependencies": { 760 | "bytes": "3.1.2", 761 | "http-errors": "2.0.0", 762 | "iconv-lite": "0.4.24", 763 | "unpipe": "1.0.0" 764 | }, 765 | "engines": { 766 | "node": ">= 0.8" 767 | } 768 | }, 769 | "node_modules/regl": { 770 | "version": "1.7.0", 771 | "resolved": "https://registry.npmjs.org/regl/-/regl-1.7.0.tgz", 772 | "integrity": "sha512-bEAtp/qrtKucxXSJkD4ebopFZYP0q1+3Vb2WECWv/T8yQEgKxDxJ7ztO285tAMaYZVR6mM1GgI6CCn8FROtL1w==" 773 | }, 774 | "node_modules/safe-buffer": { 775 | "version": "5.2.1", 776 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 777 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 778 | "funding": [ 779 | { 780 | "type": "github", 781 | "url": "https://github.com/sponsors/feross" 782 | }, 783 | { 784 | "type": "patreon", 785 | "url": "https://www.patreon.com/feross" 786 | }, 787 | { 788 | "type": "consulting", 789 | "url": "https://feross.org/support" 790 | } 791 | ] 792 | }, 793 | "node_modules/safer-buffer": { 794 | "version": "2.1.2", 795 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 796 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 797 | }, 798 | "node_modules/seed-random": { 799 | "version": "2.2.0", 800 | "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", 801 | "integrity": "sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==" 802 | }, 803 | "node_modules/send": { 804 | "version": "0.18.0", 805 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 806 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 807 | "dependencies": { 808 | "debug": "2.6.9", 809 | "depd": "2.0.0", 810 | "destroy": "1.2.0", 811 | "encodeurl": "~1.0.2", 812 | "escape-html": "~1.0.3", 813 | "etag": "~1.8.1", 814 | "fresh": "0.5.2", 815 | "http-errors": "2.0.0", 816 | "mime": "1.6.0", 817 | "ms": "2.1.3", 818 | "on-finished": "2.4.1", 819 | "range-parser": "~1.2.1", 820 | "statuses": "2.0.1" 821 | }, 822 | "engines": { 823 | "node": ">= 0.8.0" 824 | } 825 | }, 826 | "node_modules/send/node_modules/ms": { 827 | "version": "2.1.3", 828 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 829 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 830 | }, 831 | "node_modules/serve-static": { 832 | "version": "1.15.0", 833 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 834 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 835 | "dependencies": { 836 | "encodeurl": "~1.0.2", 837 | "escape-html": "~1.0.3", 838 | "parseurl": "~1.3.3", 839 | "send": "0.18.0" 840 | }, 841 | "engines": { 842 | "node": ">= 0.8.0" 843 | } 844 | }, 845 | "node_modules/setprototypeof": { 846 | "version": "1.2.0", 847 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 848 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 849 | }, 850 | "node_modules/side-channel": { 851 | "version": "1.0.4", 852 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 853 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 854 | "dependencies": { 855 | "call-bind": "^1.0.0", 856 | "get-intrinsic": "^1.0.2", 857 | "object-inspect": "^1.9.0" 858 | }, 859 | "funding": { 860 | "url": "https://github.com/sponsors/ljharb" 861 | } 862 | }, 863 | "node_modules/simplex-noise": { 864 | "version": "2.4.0", 865 | "resolved": "https://registry.npmjs.org/simplex-noise/-/simplex-noise-2.4.0.tgz", 866 | "integrity": "sha512-OjyDWm/QZjVbMrPxDVi9b2as+SeNn9EBXlrWVRlFW+TSyWMSXouDryXkQN0vf5YP+QZKobrmkvx1eQYPLtuqfw==" 867 | }, 868 | "node_modules/statuses": { 869 | "version": "2.0.1", 870 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 871 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 872 | "engines": { 873 | "node": ">= 0.8" 874 | } 875 | }, 876 | "node_modules/svg-arc-to-cubic-bezier": { 877 | "version": "3.2.0", 878 | "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", 879 | "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==" 880 | }, 881 | "node_modules/svg-path-contours": { 882 | "version": "2.0.0", 883 | "resolved": "https://registry.npmjs.org/svg-path-contours/-/svg-path-contours-2.0.0.tgz", 884 | "integrity": "sha512-mUpqlUkchMV5lQq2DdPIulNQ2lqPjYTUvY8bUUql/SlOMdPkavijO/oJgvimz31CC4Hvfce6uq+Jn0xzRysmVw==", 885 | "dependencies": { 886 | "abs-svg-path": "^0.1.1", 887 | "adaptive-bezier-curve": "^1.0.3", 888 | "normalize-svg-path": "^0.1.0", 889 | "vec2-copy": "^1.0.0" 890 | } 891 | }, 892 | "node_modules/svg-path-contours/node_modules/normalize-svg-path": { 893 | "version": "0.1.0", 894 | "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-0.1.0.tgz", 895 | "integrity": "sha512-1/kmYej2iedi5+ROxkRESL/pI02pkg0OBnaR4hJkSIX6+ORzepwbuUXfrdZaPjysTsJInj0Rj5NuX027+dMBvA==" 896 | }, 897 | "node_modules/to-regex-range": { 898 | "version": "5.0.1", 899 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 900 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 901 | "dependencies": { 902 | "is-number": "^7.0.0" 903 | }, 904 | "engines": { 905 | "node": ">=8.0" 906 | } 907 | }, 908 | "node_modules/toidentifier": { 909 | "version": "1.0.1", 910 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 911 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 912 | "engines": { 913 | "node": ">=0.6" 914 | } 915 | }, 916 | "node_modules/type-is": { 917 | "version": "1.6.18", 918 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 919 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 920 | "dependencies": { 921 | "media-typer": "0.3.0", 922 | "mime-types": "~2.1.24" 923 | }, 924 | "engines": { 925 | "node": ">= 0.6" 926 | } 927 | }, 928 | "node_modules/unpipe": { 929 | "version": "1.0.0", 930 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 931 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 932 | "engines": { 933 | "node": ">= 0.8" 934 | } 935 | }, 936 | "node_modules/utils-merge": { 937 | "version": "1.0.1", 938 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 939 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 940 | "engines": { 941 | "node": ">= 0.4.0" 942 | } 943 | }, 944 | "node_modules/vary": { 945 | "version": "1.1.2", 946 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 947 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 948 | "engines": { 949 | "node": ">= 0.8" 950 | } 951 | }, 952 | "node_modules/vec2-copy": { 953 | "version": "1.0.0", 954 | "resolved": "https://registry.npmjs.org/vec2-copy/-/vec2-copy-1.0.0.tgz", 955 | "integrity": "sha512-jeitylCmqqyM4Z2blr4vLpScsROaiJfhN2dFOjn1VK01cM4fi5GNt60L0Zxhm0OT1vYYiv7BKDOZch0YfPA8qw==" 956 | }, 957 | "node_modules/ws": { 958 | "version": "8.13.0", 959 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", 960 | "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", 961 | "engines": { 962 | "node": ">=10.0.0" 963 | }, 964 | "peerDependencies": { 965 | "bufferutil": "^4.0.1", 966 | "utf-8-validate": ">=5.0.2" 967 | }, 968 | "peerDependenciesMeta": { 969 | "bufferutil": { 970 | "optional": true 971 | }, 972 | "utf-8-validate": { 973 | "optional": true 974 | } 975 | } 976 | } 977 | } 978 | } 979 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-plugin-websockets", 3 | "version": "1.0.0", 4 | "description": "a figma plugin proof of concept", 5 | "main": "src/server.js", 6 | "type": "module", 7 | "scripts": { 8 | "plugin:dev": "esbuild src/plugin.js --bundle --outfile=dist/plugin.js --format=iife --watch", 9 | "build": "esbuild src/plugin.js --bundle --outfile=dist/plugin.js --format=iife" 10 | }, 11 | "author": "Matt DesLauriers", 12 | "license": "MIT", 13 | "dependencies": { 14 | "seed-random": "^2.2.0", 15 | "@parcel/watcher": "^2.1.0", 16 | "canvas-sketch-util": "^1.10.0", 17 | "esbuild": "^0.17.19", 18 | "express": "^4.18.2", 19 | "ws": "^8.13.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | import createContext from "./renderer.js"; 2 | import { 3 | setSeed, 4 | reseed, 5 | randomizeSeed, 6 | getSeed, 7 | patchMath, 8 | unpatchMath, 9 | } from "./util.js"; 10 | 11 | const quitMessage = "Plugin closed"; 12 | 13 | let message, msgTimeout; 14 | const notify = (msg, opts = {}) => { 15 | if (message && message.cancel) { 16 | message.cancel(); 17 | } 18 | clearTimeout(msgTimeout); 19 | msgTimeout = setTimeout(() => { 20 | message = figma.notify(msg, opts); 21 | }, 0); 22 | }; 23 | 24 | const exit = (msg) => figma.closePlugin(msg); 25 | 26 | let connected = false, 27 | hasError = false; 28 | 29 | let curState; 30 | let imageBytes; 31 | 32 | const showError = (message, lineNumber) => { 33 | hasError = true; 34 | connected = false; 35 | notify(`Script Error: ${message} on line ${lineNumber}`, { 36 | error: true, 37 | timeout: Infinity, 38 | onDequeue: (reason) => { 39 | exit(quitMessage); 40 | }, 41 | }); 42 | }; 43 | 44 | const reDraw = (useOldSeed = true) => { 45 | if (curState && curState.error) { 46 | const { lineNumber, message } = curState.error; 47 | showError(message, lineNumber); 48 | return; 49 | } 50 | 51 | try { 52 | // TODO: Fix dice roll on subsequent hashing 53 | 54 | const curCode = curState ? curState.code : ""; 55 | const src = `const module = { 56 | id: ".", 57 | exports: {}, 58 | parent: null 59 | }; 60 | ${curCode}; 61 | return _program_;`; 62 | // reset random seed 63 | reseed(); 64 | const runner = new Function(src); 65 | const mExports = runner() || {}; 66 | 67 | if (mExports && typeof mExports.default === "function") { 68 | draw(mExports.default, useOldSeed); 69 | } 70 | const wasError = hasError; 71 | hasError = false; 72 | if (wasError !== hasError) { 73 | connected = false; 74 | firstConnect(); 75 | } 76 | } catch (err) { 77 | const { lineNumber, message } = err; 78 | console.error("Error evaluating script:"); 79 | console.error(err.message); 80 | console.error(err.stack); 81 | showError(message, lineNumber); 82 | } 83 | }; 84 | 85 | const firstConnect = () => { 86 | connected = true; 87 | const file = (curState ? curState.file : null) || "[unknown]"; 88 | notify(`✨ Connected to ${file}`, { 89 | timeout: Infinity, 90 | button: { 91 | text: "🎲", 92 | action() { 93 | randomizeSeed(); 94 | reDraw(false); 95 | return false; 96 | }, 97 | }, 98 | onDequeue: (reason) => { 99 | exit(quitMessage); 100 | }, 101 | }); 102 | }; 103 | 104 | function findCanvas(expectedHash) { 105 | return figma.currentPage.findOne((node) => { 106 | const str = node.getPluginData("sketch"); 107 | if (str) { 108 | try { 109 | const data = JSON.parse(str); 110 | return data.hash === expectedHash; 111 | } catch (err) { 112 | return false; 113 | } 114 | } else return false; 115 | }); 116 | } 117 | 118 | function getCanvas(useOldSeed = true) { 119 | const expectedHash = curState.hash; 120 | let doc = findCanvas(expectedHash); 121 | 122 | if (!doc) { 123 | const frame = figma.createFrame(); 124 | frame.resize(1024, 1024); 125 | doc = frame; 126 | } else { 127 | if (useOldSeed) { 128 | // doc exists, pull its seed 129 | try { 130 | const data = JSON.parse(doc.getPluginData("sketch")); 131 | if (data.seed) { 132 | setSeed(data.seed); 133 | } 134 | } catch (err) {} 135 | } 136 | } 137 | 138 | const curStateWithSeed = Object.assign({}, curState, { seed: getSeed() }); 139 | doc.setPluginData("sketch", JSON.stringify(curStateWithSeed)); 140 | doc.name = curStateWithSeed.name; 141 | // doc.name = `${curStateWithSeed.name}-${curStateWithSeed.seed}`; 142 | const oldSel = figma.currentPage.selection; 143 | if (!oldSel.includes(doc)) { 144 | figma.currentPage.selection = [doc]; 145 | figma.viewport.scrollAndZoomIntoView([doc]); 146 | } 147 | return doc; 148 | } 149 | 150 | function draw(fn, useOldSeed = true) { 151 | const doc = getCanvas(useOldSeed); 152 | for (const child of doc.children) { 153 | child.remove(); 154 | } 155 | 156 | const { width, height } = doc; 157 | const context = createContext(doc, { width, height }); 158 | fn({ context, width, height }); 159 | doc.expanded = false; 160 | } 161 | 162 | async function run() { 163 | patchMath(); 164 | figma.on("close", () => { 165 | unpatchMath(); 166 | }); 167 | 168 | figma.showUI(__html__, { visible: false }); 169 | figma.ui.onmessage = (msg) => { 170 | if (msg.type === "disconnect") { 171 | exit(`⚠️ ${msg.reason}`); 172 | } else if (msg.type === "error") { 173 | exit(`⚠️ ${msg.reason}`); 174 | } else if (msg.type === "update") { 175 | curState = Object.assign({}, msg); 176 | if (!connected) { 177 | firstConnect(); 178 | } 179 | reDraw(true); 180 | } 181 | }; 182 | } 183 | 184 | run(); 185 | -------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | import Color from "canvas-sketch-util/color.js"; 2 | 3 | export default function createContext(frame, { width, height }) { 4 | let _fillStyle = { r: 0, g: 0, b: 0 }; 5 | 6 | const context = { 7 | get fillStyle() { 8 | return _fillStyle; 9 | }, 10 | set fillStyle(f) { 11 | try { 12 | if (typeof f !== "string") f = f.toString(); 13 | const [r, g, b] = Color.parse(f).rgb.map((n) => n / 0xff); 14 | _fillStyle = { r, g, b }; 15 | } catch (err) { 16 | _fillStyle = { r: 0, g: 0, b: 0 }; 17 | } 18 | }, 19 | fillRect(x, y, w, h) { 20 | const child = figma.createRectangle(); 21 | const newColor = Object.assign({}, _fillStyle); 22 | child.x = x; 23 | child.y = y; 24 | child.resize(w, h); 25 | child.fills = [{ type: "SOLID", color: newColor }]; 26 | frame.appendChild(child); 27 | }, 28 | fillEllipse(x, y, w, h = w) { 29 | const child = figma.createEllipse(); 30 | const newColor = Object.assign({}, _fillStyle); 31 | child.x = x; 32 | child.y = y; 33 | child.resize(w, h); 34 | child.fills = [{ type: "SOLID", color: newColor }]; 35 | frame.appendChild(child); 36 | }, 37 | }; 38 | 39 | return context; 40 | } 41 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import WebSocket, { WebSocketServer } from "ws"; 3 | import watcher from "@parcel/watcher"; 4 | import * as path from "path"; 5 | import * as esbuild from "esbuild"; 6 | import crypto from "crypto"; 7 | 8 | const hasher = (name) => crypto.createHash("md5").update(name).digest("hex"); 9 | const port = 19407; 10 | 11 | const argv = process.argv.slice(2); 12 | if (argv.length === 0) throw new Error(`Must specify an input file`); 13 | const inFile = path.resolve(argv[0]); 14 | const fileBasename = path.basename(inFile); 15 | const extension = path.extname(fileBasename); 16 | const name = path.basename(fileBasename, extension); 17 | const watchDir = path.dirname(inFile); 18 | const hash = hasher(inFile); 19 | const cwd = process.cwd(); 20 | 21 | const options = { 22 | preview: true, 23 | }; 24 | 25 | let result = { code: null, error: null }; 26 | await readInput(); 27 | 28 | const app = express(); 29 | const server = app.listen(port, () => { 30 | console.log(`Listening on port ${port}`); 31 | }); 32 | 33 | const wss = new WebSocketServer({ server }); 34 | wss.on("connection", function connection(ws) { 35 | broadcastState([ws]); 36 | ws.on("error", console.error); 37 | ws.on("message", function message(data) { 38 | // rebroadcast to other clients 39 | broadcastRaw(wss.clients, data.toString()); 40 | }); 41 | }); 42 | 43 | console.log(`Watching: ${path.relative(cwd, inFile)}`); 44 | let subscription = await watcher.subscribe(watchDir, async (err, events) => { 45 | if (err) console.error(err); 46 | else { 47 | const match = events.find((p) => path.resolve(p.path) === inFile); 48 | if (match) { 49 | await readInput(); 50 | broadcastState(wss.clients); 51 | } 52 | } 53 | }); 54 | 55 | async function readInput() { 56 | try { 57 | const res = await esbuild.build({ 58 | entryPoints: [inFile], 59 | bundle: true, 60 | sourcemap: false, 61 | format: "iife", 62 | globalName: "_program_", 63 | minify: true, 64 | write: false, 65 | }); 66 | const { errors, outputFiles } = res; 67 | result = { 68 | code: outputFiles[0].text, 69 | error: errors.length ? errors[0] : null, 70 | }; 71 | } catch (err) { 72 | result = { 73 | code: null, 74 | error: { 75 | message: err.errors[0].text, 76 | lineNumber: err.errors[0].location.line, 77 | }, 78 | }; 79 | } 80 | } 81 | 82 | function broadcastRaw(clients = [], rawStr) { 83 | clients.forEach(function each(client) { 84 | if (client.readyState === WebSocket.OPEN) { 85 | client.send(rawStr); 86 | } 87 | }); 88 | } 89 | 90 | function broadcastState(clients = []) { 91 | return broadcastRaw( 92 | clients, 93 | JSON.stringify({ 94 | type: "update", 95 | code: result.code, 96 | error: result.error, 97 | hash, 98 | file: fileBasename, 99 | name, 100 | options, 101 | }) 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /src/ui.html: -------------------------------------------------------------------------------- 1 | 57 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | // Generates a pure random hash, useful for testing 2 | // i.e. not deterministic! 3 | export function getRandomHash() { 4 | let result = "0x"; 5 | for (let i = 64; i > 0; --i) 6 | result += "0123456789abcdef"[~~(Math.random() * 16)]; 7 | return result; 8 | } 9 | 10 | let hash, xs_state, originalState; 11 | let pureMathRandom = Math.random; 12 | 13 | export const getSeed = () => hash; 14 | 15 | export const setSeed = (h) => { 16 | hash = h; 17 | console.log("Random Seed:", hash); 18 | xs_state = Uint32Array.from( 19 | [0, 0, 0, 0].map((_, i) => parseInt(hash.substr(i * 8 + 2, 8), 16)) 20 | ); 21 | originalState = xs_state.slice(); 22 | }; 23 | 24 | export const randomizeSeed = () => { 25 | setSeed(getRandomHash()); 26 | }; 27 | 28 | export const prng = () => { 29 | /* Algorithm "xor128" from p. 5 of Marsaglia, "Xorshift RNGs" */ 30 | let s, 31 | t = xs_state[3]; 32 | xs_state[3] = xs_state[2]; 33 | xs_state[2] = xs_state[1]; 34 | xs_state[1] = s = xs_state[0]; 35 | t ^= t << 11; 36 | t ^= t >>> 8; 37 | xs_state[0] = t ^ s ^ (s >>> 19); 38 | return xs_state[0] / 0x100000000; 39 | }; 40 | 41 | export function patchMath() { 42 | Math.random = () => prng(); 43 | } 44 | 45 | export function unpatchMath() { 46 | Math.random = pureMathRandom; 47 | } 48 | 49 | export const reseed = () => { 50 | xs_state.set(originalState); 51 | }; 52 | 53 | randomizeSeed(); 54 | -------------------------------------------------------------------------------- /test/sketch.js: -------------------------------------------------------------------------------- 1 | import Color from "canvas-sketch-util/color.js"; 2 | import { range as randomRange } from "canvas-sketch-util/random.js"; 3 | 4 | // simple HSL to hex color util 5 | const hsl = (hue, sat, light) => Color.parse({ hsl: [hue, sat, light] }).hex; 6 | 7 | export default function main({ context, width, height }) { 8 | const baseHue = Math.random() * 360; 9 | const margin = width * 0.2; 10 | 11 | const newColor = () => { 12 | const hue = baseHue + randomRange(-1, 1) * 5; 13 | const sat = 50 + randomRange(-1, 1) * 10; 14 | const light = 50 + randomRange(-1, 1) * 10; 15 | return hsl(hue, sat, light); 16 | }; 17 | 18 | context.fillStyle = hsl(baseHue + 90, 25, 80); 19 | context.fillRect(0, 0, width, height); 20 | 21 | // Draw N dots 22 | const count = 50; 23 | for (let i = 0; i < count; i++) { 24 | const r = width * 0.05; 25 | context.fillStyle = newColor(); 26 | if (Math.random() > 0.5) { 27 | context.fillEllipse( 28 | randomRange(margin, width - margin), 29 | randomRange(margin, height - margin), 30 | r 31 | ); 32 | } else { 33 | context.fillRect( 34 | randomRange(margin, width - margin) - r / 2, 35 | randomRange(margin, height - margin) - r / 2, 36 | r, 37 | r 38 | ); 39 | } 40 | } 41 | } 42 | --------------------------------------------------------------------------------