├── README.md ├── connect-gesture.html ├── connect-gesture.js ├── react-svg.html └── react ├── font-awesome-unicode-map.js ├── index.html ├── photobooth.json.js ├── the-graph-app.js ├── the-graph-edge.js ├── the-graph-graph.js ├── the-graph-group.js ├── the-graph-node.js ├── the-graph-port.js ├── the-graph-svg.css ├── the-graph-tooltip.js └── the-graph.js /README.md: -------------------------------------------------------------------------------- 1 | prototyping 2 | =========== 3 | 4 | prototypes for making meemoo + dataflow + noflo 5 | 6 | The React nodal graph editor prototype is now a real project here: https://github.com/the-grid/the-graph 7 | -------------------------------------------------------------------------------- /connect-gesture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | connect gesture 5 | 6 | 7 | 8 | 84 | 85 | 86 | 87 | 88 |
89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /connect-gesture.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | "use strict"; 3 | 4 | var w = window.innerWidth; 5 | var h = window.innerHeight; 6 | 7 | function randomInt (max) { 8 | return Math.floor(Math.random()*max); 9 | } 10 | 11 | // Make some random nodes 12 | var count = 5; 13 | var graph = {}; 14 | var nodes = graph.nodes = []; 15 | var ioMax = 5; 16 | var nodeW = 75; 17 | var nodeH = 75; 18 | var nodeW2 = Math.floor(nodeW/2); 19 | var nodeH2 = Math.floor(nodeH/2); 20 | var margin = 10; 21 | var gridW = nodeW + margin*2; 22 | var gridH = nodeH + margin*2; 23 | var gridCols = Math.floor( (w-margin*2)/(nodeW+margin*2) ); 24 | var gridRows = Math.floor( (h-margin*2)/(nodeH+margin*2) ); 25 | 26 | // CSS grid bg 27 | document.body.style.backgroundSize = gridW+"px "+gridH+"px"; 28 | 29 | var i, j, k; 30 | for (i=0; i 0) { 142 | event.clientX = event.touches[0].clientX; 143 | event.clientY = event.touches[0].clientY; 144 | } 145 | } 146 | 147 | function gestureStart (event) { 148 | event.preventDefault(); 149 | var model = event.target.model; 150 | if (!model || model.type!=="node") { return; } 151 | gestureItem = model; 152 | var gesture = model.gesture = {}; 153 | touchNormalize(event); 154 | gesture.startX = event.clientX; 155 | gesture.startY = event.clientY; 156 | gesture.action = null; 157 | }; 158 | 159 | function gestureMove (event) { 160 | event.preventDefault(); 161 | if (!gestureItem) { return; } 162 | 163 | console.log(event); 164 | 165 | var gesture = gestureItem.gesture; 166 | touchNormalize(event); 167 | gesture.deltaX = event.clientX - gesture.startX; 168 | gesture.deltaY = event.clientY - gesture.startY; 169 | 170 | if (!gesture.action) { 171 | if (Math.abs(gesture.deltaY) > threshold) { 172 | // Move 173 | gesture.action = "move"; 174 | } else if (gesture.deltaX > threshold) { 175 | // Start wire from output 176 | gesture.action = "wire-out"; 177 | nodeShowOutputs(gestureItem); 178 | } else if (gesture.deltaX < 0-threshold) { 179 | // Start wire from input 180 | gesture.action = "wire-in"; 181 | nodeShowInputs(gestureItem); 182 | } 183 | } 184 | else { 185 | switch (gesture.action) { 186 | case "move": 187 | translate(gestureItem.view, event.clientX-nodeW2, event.clientY-nodeH2); 188 | break; 189 | case "wire-out": 190 | var model = event.target.model; 191 | if (event.type === "touchmove" && event.target===gestureItem.view) { 192 | var over = document.elementFromPoint(event.clientX, event.clientY); 193 | model = over.model; 194 | } 195 | if (!model) { break; } 196 | else if (model.type === "output" || model.type === "input") { 197 | portSelect(model); 198 | // Dont mouseover node 199 | event.stopPropagation(); 200 | } 201 | else if (model.type === "node") { 202 | nodeShowInputs(model); 203 | } 204 | break; 205 | case "wire-in": 206 | var model = event.target.model; 207 | if (event.type === "touchmove" && event.target===gestureItem.view) { 208 | var over = document.elementFromPoint(event.clientX, event.clientY); 209 | model = over.model; 210 | } 211 | if (!model) { break; } 212 | else if (model.type === "output" || model.type === "input") { 213 | portSelect(model); 214 | // Dont mouseover node 215 | event.stopPropagation(); 216 | } 217 | else if (model.type === "node") { 218 | nodeShowOutputs(model); 219 | } 220 | break; 221 | default: 222 | break; 223 | } 224 | } 225 | }; 226 | 227 | function gestureStop (event) { 228 | console.log("stop", event); 229 | event.preventDefault(); 230 | if (!gestureItem) { return; } 231 | if (gestureItem.gesture.action === "move"){ 232 | //Snap 233 | if (event.changedTouches && event.changedTouches[0]) { 234 | event.clientX = event.changedTouches[0].clientX; 235 | event.clientY = event.changedTouches[0].clientY; 236 | } 237 | nodeSnapToGrid(gestureItem, event.clientX, event.clientY); 238 | } else { 239 | graphHidePorts(gestureItem.parent); 240 | } 241 | gestureItem = null; 242 | }; 243 | 244 | // IO helpers 245 | 246 | function nodeBringToTop (node) { 247 | node.view.parentNode.appendChild(node.view); 248 | } 249 | 250 | function nodeShowInputs (node) { 251 | nodeBringToTop(node); 252 | if (node.insShown) { return; } 253 | for( var i=0; i 2 | 3 | 4 | redirect 5 | 6 | 7 | 8 | moved here 9 | 10 | -------------------------------------------------------------------------------- /react/font-awesome-unicode-map.js: -------------------------------------------------------------------------------- 1 | (function (context) { 2 | "use strict"; 3 | 4 | context.TheGraph.FONT_AWESOME = { 5 | "glass": "\uf000", 6 | "music": "\uf001", 7 | "search": "\uf002", 8 | "envelope-o": "\uf003", 9 | "heart": "\uf004", 10 | "star": "\uf005", 11 | "star-o": "\uf006", 12 | "user": "\uf007", 13 | "film": "\uf008", 14 | "th-large": "\uf009", 15 | "th": "\uf00a", 16 | "th-list": "\uf00b", 17 | "check": "\uf00c", 18 | "times": "\uf00d", 19 | "search-plus": "\uf00e", 20 | "search-minus": "\uf010", 21 | "power-off": "\uf011", 22 | "signal": "\uf012", 23 | "cog": "\uf013", 24 | "trash-o": "\uf014", 25 | "home": "\uf015", 26 | "file-o": "\uf016", 27 | "clock-o": "\uf017", 28 | "road": "\uf018", 29 | "download": "\uf019", 30 | "arrow-circle-o-down": "\uf01a", 31 | "arrow-circle-o-up": "\uf01b", 32 | "inbox": "\uf01c", 33 | "play-circle-o": "\uf01d", 34 | "repeat": "\uf01e", 35 | "refresh": "\uf021", 36 | "list-alt": "\uf022", 37 | "lock": "\uf023", 38 | "flag": "\uf024", 39 | "headphones": "\uf025", 40 | "volume-off": "\uf026", 41 | "volume-down": "\uf027", 42 | "volume-up": "\uf028", 43 | "qrcode": "\uf029", 44 | "barcode": "\uf02a", 45 | "tag": "\uf02b", 46 | "tags": "\uf02c", 47 | "book": "\uf02d", 48 | "bookmark": "\uf02e", 49 | "print": "\uf02f", 50 | "camera": "\uf030", 51 | "font": "\uf031", 52 | "bold": "\uf032", 53 | "italic": "\uf033", 54 | "text-height": "\uf034", 55 | "text-width": "\uf035", 56 | "align-left": "\uf036", 57 | "align-center": "\uf037", 58 | "align-right": "\uf038", 59 | "align-justify": "\uf039", 60 | "list": "\uf03a", 61 | "outdent": "\uf03b", 62 | "indent": "\uf03c", 63 | "video-camera": "\uf03d", 64 | "picture-o": "\uf03e", 65 | "pencil": "\uf040", 66 | "map-marker": "\uf041", 67 | "adjust": "\uf042", 68 | "tint": "\uf043", 69 | "pencil-square-o": "\uf044", 70 | "share-square-o": "\uf045", 71 | "check-square-o": "\uf046", 72 | "arrows": "\uf047", 73 | "step-backward": "\uf048", 74 | "fast-backward": "\uf049", 75 | "backward": "\uf04a", 76 | "play": "\uf04b", 77 | "pause": "\uf04c", 78 | "stop": "\uf04d", 79 | "forward": "\uf04e", 80 | "fast-forward": "\uf050", 81 | "step-forward": "\uf051", 82 | "eject": "\uf052", 83 | "chevron-left": "\uf053", 84 | "chevron-right": "\uf054", 85 | "plus-circle": "\uf055", 86 | "minus-circle": "\uf056", 87 | "times-circle": "\uf057", 88 | "check-circle": "\uf058", 89 | "question-circle": "\uf059", 90 | "info-circle": "\uf05a", 91 | "crosshairs": "\uf05b", 92 | "times-circle-o": "\uf05c", 93 | "check-circle-o": "\uf05d", 94 | "ban": "\uf05e", 95 | "arrow-left": "\uf060", 96 | "arrow-right": "\uf061", 97 | "arrow-up": "\uf062", 98 | "arrow-down": "\uf063", 99 | "share": "\uf064", 100 | "expand": "\uf065", 101 | "compress": "\uf066", 102 | "plus": "\uf067", 103 | "minus": "\uf068", 104 | "asterisk": "\uf069", 105 | "exclamation-circle": "\uf06a", 106 | "gift": "\uf06b", 107 | "leaf": "\uf06c", 108 | "fire": "\uf06d", 109 | "eye": "\uf06e", 110 | "eye-slash": "\uf070", 111 | "exclamation-triangle": "\uf071", 112 | "plane": "\uf072", 113 | "calendar": "\uf073", 114 | "random": "\uf074", 115 | "comment": "\uf075", 116 | "magnet": "\uf076", 117 | "chevron-up": "\uf077", 118 | "chevron-down": "\uf078", 119 | "retweet": "\uf079", 120 | "shopping-cart": "\uf07a", 121 | "folder": "\uf07b", 122 | "folder-open": "\uf07c", 123 | "arrows-v": "\uf07d", 124 | "arrows-h": "\uf07e", 125 | "bar-chart-o": "\uf080", 126 | "twitter-square": "\uf081", 127 | "facebook-square": "\uf082", 128 | "camera-retro": "\uf083", 129 | "key": "\uf084", 130 | "cogs": "\uf085", 131 | "comments": "\uf086", 132 | "thumbs-o-up": "\uf087", 133 | "thumbs-o-down": "\uf088", 134 | "star-half": "\uf089", 135 | "heart-o": "\uf08a", 136 | "sign-out": "\uf08b", 137 | "linkedin-square": "\uf08c", 138 | "thumb-tack": "\uf08d", 139 | "external-link": "\uf08e", 140 | "sign-in": "\uf090", 141 | "trophy": "\uf091", 142 | "github-square": "\uf092", 143 | "upload": "\uf093", 144 | "lemon-o": "\uf094", 145 | "phone": "\uf095", 146 | "square-o": "\uf096", 147 | "bookmark-o": "\uf097", 148 | "phone-square": "\uf098", 149 | "twitter": "\uf099", 150 | "facebook": "\uf09a", 151 | "github": "\uf09b", 152 | "unlock": "\uf09c", 153 | "credit-card": "\uf09d", 154 | "rss": "\uf09e", 155 | "hdd-o": "\uf0a0", 156 | "bullhorn": "\uf0a1", 157 | "bell": "\uf0f3", 158 | "certificate": "\uf0a3", 159 | "hand-o-right": "\uf0a4", 160 | "hand-o-left": "\uf0a5", 161 | "hand-o-up": "\uf0a6", 162 | "hand-o-down": "\uf0a7", 163 | "arrow-circle-left": "\uf0a8", 164 | "arrow-circle-right": "\uf0a9", 165 | "arrow-circle-up": "\uf0aa", 166 | "arrow-circle-down": "\uf0ab", 167 | "globe": "\uf0ac", 168 | "wrench": "\uf0ad", 169 | "tasks": "\uf0ae", 170 | "filter": "\uf0b0", 171 | "briefcase": "\uf0b1", 172 | "arrows-alt": "\uf0b2", 173 | "users": "\uf0c0", 174 | "link": "\uf0c1", 175 | "cloud": "\uf0c2", 176 | "flask": "\uf0c3", 177 | "scissors": "\uf0c4", 178 | "files-o": "\uf0c5", 179 | "paperclip": "\uf0c6", 180 | "floppy-o": "\uf0c7", 181 | "square": "\uf0c8", 182 | "bars": "\uf0c9", 183 | "list-ul": "\uf0ca", 184 | "list-ol": "\uf0cb", 185 | "strikethrough": "\uf0cc", 186 | "underline": "\uf0cd", 187 | "table": "\uf0ce", 188 | "magic": "\uf0d0", 189 | "truck": "\uf0d1", 190 | "pinterest": "\uf0d2", 191 | "pinterest-square": "\uf0d3", 192 | "google-plus-square": "\uf0d4", 193 | "google-plus": "\uf0d5", 194 | "money": "\uf0d6", 195 | "caret-down": "\uf0d7", 196 | "caret-up": "\uf0d8", 197 | "caret-left": "\uf0d9", 198 | "caret-right": "\uf0da", 199 | "columns": "\uf0db", 200 | "sort": "\uf0dc", 201 | "sort-asc": "\uf0dd", 202 | "sort-desc": "\uf0de", 203 | "envelope": "\uf0e0", 204 | "linkedin": "\uf0e1", 205 | "undo": "\uf0e2", 206 | "gavel": "\uf0e3", 207 | "tachometer": "\uf0e4", 208 | "comment-o": "\uf0e5", 209 | "comments-o": "\uf0e6", 210 | "bolt": "\uf0e7", 211 | "sitemap": "\uf0e8", 212 | "umbrella": "\uf0e9", 213 | "clipboard": "\uf0ea", 214 | "lightbulb-o": "\uf0eb", 215 | "exchange": "\uf0ec", 216 | "cloud-download": "\uf0ed", 217 | "cloud-upload": "\uf0ee", 218 | "user-md": "\uf0f0", 219 | "stethoscope": "\uf0f1", 220 | "suitcase": "\uf0f2", 221 | "bell-o": "\uf0a2", 222 | "coffee": "\uf0f4", 223 | "cutlery": "\uf0f5", 224 | "file-text-o": "\uf0f6", 225 | "building-o": "\uf0f7", 226 | "hospital-o": "\uf0f8", 227 | "ambulance": "\uf0f9", 228 | "medkit": "\uf0fa", 229 | "fighter-jet": "\uf0fb", 230 | "beer": "\uf0fc", 231 | "h-square": "\uf0fd", 232 | "plus-square": "\uf0fe", 233 | "angle-double-left": "\uf100", 234 | "angle-double-right": "\uf101", 235 | "angle-double-up": "\uf102", 236 | "angle-double-down": "\uf103", 237 | "angle-left": "\uf104", 238 | "angle-right": "\uf105", 239 | "angle-up": "\uf106", 240 | "angle-down": "\uf107", 241 | "desktop": "\uf108", 242 | "laptop": "\uf109", 243 | "tablet": "\uf10a", 244 | "mobile": "\uf10b", 245 | "circle-o": "\uf10c", 246 | "quote-left": "\uf10d", 247 | "quote-right": "\uf10e", 248 | "spinner": "\uf110", 249 | "circle": "\uf111", 250 | "reply": "\uf112", 251 | "github-alt": "\uf113", 252 | "folder-o": "\uf114", 253 | "folder-open-o": "\uf115", 254 | "smile-o": "\uf118", 255 | "frown-o": "\uf119", 256 | "meh-o": "\uf11a", 257 | "gamepad": "\uf11b", 258 | "keyboard-o": "\uf11c", 259 | "flag-o": "\uf11d", 260 | "flag-checkered": "\uf11e", 261 | "terminal": "\uf120", 262 | "code": "\uf121", 263 | "reply-all": "\uf122", 264 | "mail-reply-all": "\uf122", 265 | "star-half-o": "\uf123", 266 | "location-arrow": "\uf124", 267 | "crop": "\uf125", 268 | "code-fork": "\uf126", 269 | "chain-broken": "\uf127", 270 | "question": "\uf128", 271 | "info": "\uf129", 272 | "exclamation": "\uf12a", 273 | "superscript": "\uf12b", 274 | "subscript": "\uf12c", 275 | "eraser": "\uf12d", 276 | "puzzle-piece": "\uf12e", 277 | "microphone": "\uf130", 278 | "microphone-slash": "\uf131", 279 | "shield": "\uf132", 280 | "calendar-o": "\uf133", 281 | "fire-extinguisher": "\uf134", 282 | "rocket": "\uf135", 283 | "maxcdn": "\uf136", 284 | "chevron-circle-left": "\uf137", 285 | "chevron-circle-right": "\uf138", 286 | "chevron-circle-up": "\uf139", 287 | "chevron-circle-down": "\uf13a", 288 | "html5": "\uf13b", 289 | "css3": "\uf13c", 290 | "anchor": "\uf13d", 291 | "unlock-alt": "\uf13e", 292 | "bullseye": "\uf140", 293 | "ellipsis-h": "\uf141", 294 | "ellipsis-v": "\uf142", 295 | "rss-square": "\uf143", 296 | "play-circle": "\uf144", 297 | "ticket": "\uf145", 298 | "minus-square": "\uf146", 299 | "minus-square-o": "\uf147", 300 | "level-up": "\uf148", 301 | "level-down": "\uf149", 302 | "check-square": "\uf14a", 303 | "pencil-square": "\uf14b", 304 | "external-link-square": "\uf14c", 305 | "share-square": "\uf14d", 306 | "compass": "\uf14e", 307 | "caret-square-o-down": "\uf150", 308 | "caret-square-o-up": "\uf151", 309 | "caret-square-o-right": "\uf152", 310 | "eur": "\uf153", 311 | "gbp": "\uf154", 312 | "usd": "\uf155", 313 | "inr": "\uf156", 314 | "jpy": "\uf157", 315 | "rub": "\uf158", 316 | "krw": "\uf159", 317 | "btc": "\uf15a", 318 | "file": "\uf15b", 319 | "file-text": "\uf15c", 320 | "sort-alpha-asc": "\uf15d", 321 | "sort-alpha-desc": "\uf15e", 322 | "sort-amount-asc": "\uf160", 323 | "sort-amount-desc": "\uf161", 324 | "sort-numeric-asc": "\uf162", 325 | "sort-numeric-desc": "\uf163", 326 | "thumbs-up": "\uf164", 327 | "thumbs-down": "\uf165", 328 | "youtube-square": "\uf166", 329 | "youtube": "\uf167", 330 | "xing": "\uf168", 331 | "xing-square": "\uf169", 332 | "youtube-play": "\uf16a", 333 | "dropbox": "\uf16b", 334 | "stack-overflow": "\uf16c", 335 | "instagram": "\uf16d", 336 | "flickr": "\uf16e", 337 | "adn": "\uf170", 338 | "bitbucket": "\uf171", 339 | "bitbucket-square": "\uf172", 340 | "tumblr": "\uf173", 341 | "tumblr-square": "\uf174", 342 | "long-arrow-down": "\uf175", 343 | "long-arrow-up": "\uf176", 344 | "long-arrow-left": "\uf177", 345 | "long-arrow-right": "\uf178", 346 | "apple": "\uf179", 347 | "windows": "\uf17a", 348 | "android": "\uf17b", 349 | "linux": "\uf17c", 350 | "dribbble": "\uf17d", 351 | "skype": "\uf17e", 352 | "foursquare": "\uf180", 353 | "trello": "\uf181", 354 | "female": "\uf182", 355 | "male": "\uf183", 356 | "gittip": "\uf184", 357 | "sun-o": "\uf185", 358 | "moon-o": "\uf186", 359 | "archive": "\uf187", 360 | "bug": "\uf188", 361 | "vk": "\uf189", 362 | "weibo": "\uf18a", 363 | "renren": "\uf18b", 364 | "pagelines": "\uf18c", 365 | "stack-exchange": "\uf18d", 366 | "arrow-circle-o-right": "\uf18e", 367 | "arrow-circle-o-left": "\uf190", 368 | "caret-square-o-left": "\uf191", 369 | "dot-circle-o": "\uf192", 370 | "wheelchair": "\uf193", 371 | "vimeo-square": "\uf194", 372 | "try": "\uf195", 373 | "plus-square-o": "\uf196" 374 | } 375 | 376 | })(this); -------------------------------------------------------------------------------- /react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | react + svg test 5 | 6 | 7 | 8 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | source ~ 40 | wheel to zoom ~ 41 | 42 | drag to pan 43 |
44 | 45 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /react/photobooth.json.js: -------------------------------------------------------------------------------- 1 | loadGraph({ 2 | "id": "ti6yn", 3 | "project": "", 4 | "properties": { 5 | "name": "photobooth", 6 | "environment": { 7 | "runtime": "html", 8 | "src": "preview/iframe.html", 9 | "width": 300, 10 | "height": 300, 11 | "content": " \n \n\n\n \n \n \n \n\n\n
" 12 | } 13 | }, 14 | "exports": [], 15 | "groups": [ 16 | { 17 | "name": "elements", 18 | "nodes": ["dom/GetElement_ah82a", "dom/GetElement_f4nkd", "dom/GetElement_z64xf"], 19 | "metadata": { 20 | "description": "get the elements from the dom" 21 | } 22 | }, 23 | { 24 | "name": "countdown", 25 | "nodes": ["core/Split_lbwyz", "interaction/ListenMouse_1u0rk", "strings/SendString_zry4n", "core/RunTimeout_3wulz", "dom/AddClass_9rihj", "core/Split_ho5ib", "strings/SendString_lnf0z", "core/Kick_7efi8", "dom/RemoveClass_ec7ug"], 26 | "metadata": { 27 | "description": "" 28 | } 29 | }, 30 | { 31 | "name": "changefilter", 32 | "nodes": ["dom/GetElement_e16dy", "dom/GetElement_85so0", "interaction/ListenMouse_bil4d", "interaction/ListenMouse_aii7r"], 33 | "metadata": { 34 | "description": "" 35 | } 36 | }, 37 | { 38 | "name": "save", 39 | "nodes": ["dom/GetElement_ah36i", "core/MakeFunction_t17n", "core/Split_xyb8x", "strings/SendString_7g9h8", "dom/GetElement_4houj", "core/RepeatAsync_grqs3", "dom/CreateElement_sf6ec", "dom/SetAttribute_3bmlw"], 40 | "metadata": { 41 | "description": "" 42 | } 43 | }, 44 | { 45 | "name": "filter", 46 | "nodes": ["dom/GetElement_j674o", "routers/KickRouter_bzaiw", "interaction/ListenChange_z7m5u", "math/Multiply_rbxrn", "math/Multiply_3v13k", "seriously/HueSaturation_bzfvt", "core/Split_jx318", "math/Multiply_3eldx", "seriously/TVGlitch_e1qre", "seriously/Hex_lx162", "seriously/Ascii_17c9q", "seriously/Invert_7xnl3", "seriously/Edge_egmkb", "seriously/Split_7oj15", "seriously/SetTarget_kii7s"], 47 | "metadata": { 48 | "description": "" 49 | } 50 | } 51 | ], 52 | "processes": { 53 | "dom/GetElement_f4nkd": { 54 | "component": "dom/GetElement", 55 | "metadata": { 56 | "x": 324, 57 | "y": 144, 58 | "label": "startButton" 59 | } 60 | }, 61 | "interaction/ListenMouse_1w3vt": { 62 | "component": "interaction/ListenMouse", 63 | "metadata": { 64 | "x": 324, 65 | "y": 288, 66 | "label": "clickStart" 67 | } 68 | }, 69 | "gum/GetUserMedia_9e9i4": { 70 | "component": "gum/GetUserMedia", 71 | "metadata": { 72 | "x": 324, 73 | "y": 648, 74 | "label": "webCam" 75 | } 76 | }, 77 | "dom/GetElement_z64xf": { 78 | "component": "dom/GetElement", 79 | "metadata": { 80 | "x": 504, 81 | "y": 144, 82 | "label": "videoEl" 83 | } 84 | }, 85 | "dom/SetAttribute_uto4k": { 86 | "component": "dom/SetAttribute", 87 | "metadata": { 88 | "x": 432, 89 | "y": 648, 90 | "label": "setVideoSrc" 91 | } 92 | }, 93 | "seriously/SetSource_szf33": { 94 | "component": "seriously/SetSource", 95 | "metadata": { 96 | "x": 576, 97 | "y": 648, 98 | "label": "setFilterSource" 99 | } 100 | }, 101 | "seriously/Ascii_17c9q": { 102 | "component": "seriously/Ascii", 103 | "metadata": { 104 | "x": 1080, 105 | "y": 396, 106 | "label": "seriously/Ascii" 107 | } 108 | }, 109 | "dom/GetElement_ah82a": { 110 | "component": "dom/GetElement", 111 | "metadata": { 112 | "x": 1404, 113 | "y": 144, 114 | "label": "canvasEl" 115 | } 116 | }, 117 | "seriously/SetTarget_kii7s": { 118 | "component": "seriously/SetTarget", 119 | "metadata": { 120 | "x": 1404, 121 | "y": 648, 122 | "label": "filterTarget" 123 | } 124 | }, 125 | "dom/GetElement_85so0": { 126 | "component": "dom/GetElement", 127 | "metadata": { 128 | "x": 720, 129 | "y": 144, 130 | "label": "prevButton" 131 | } 132 | }, 133 | "interaction/ListenMouse_aii7r": { 134 | "component": "interaction/ListenMouse", 135 | "metadata": { 136 | "x": 720, 137 | "y": 288, 138 | "label": "clickPrev" 139 | } 140 | }, 141 | "dom/GetElement_e16dy": { 142 | "component": "dom/GetElement", 143 | "metadata": { 144 | "x": 864, 145 | "y": 144, 146 | "label": "nextButton" 147 | } 148 | }, 149 | "interaction/ListenMouse_bil4d": { 150 | "component": "interaction/ListenMouse", 151 | "metadata": { 152 | "x": 864, 153 | "y": 288, 154 | "label": "clickNext" 155 | } 156 | }, 157 | "routers/KickRouter_bzaiw": { 158 | "component": "routers/KickRouter", 159 | "metadata": { 160 | "x": 828, 161 | "y": 648, 162 | "label": "chooseFilter" 163 | } 164 | }, 165 | "seriously/TVGlitch_e1qre": { 166 | "component": "seriously/TVGlitch", 167 | "metadata": { 168 | "x": 1080, 169 | "y": 504, 170 | "label": "seriously/TVGlitch" 171 | } 172 | }, 173 | "seriously/Hex_lx162": { 174 | "component": "seriously/Hex", 175 | "metadata": { 176 | "x": 1008, 177 | "y": 612, 178 | "label": "seriously/Hex" 179 | } 180 | }, 181 | "seriously/Edge_egmkb": { 182 | "component": "seriously/Edge", 183 | "metadata": { 184 | "x": 1152, 185 | "y": 612, 186 | "label": "seriously/Edge" 187 | } 188 | }, 189 | "seriously/Invert_7xnl3": { 190 | "component": "seriously/Invert", 191 | "metadata": { 192 | "x": 1008, 193 | "y": 828, 194 | "label": "negative" 195 | } 196 | }, 197 | "seriously/Split_7oj15": { 198 | "component": "seriously/Split", 199 | "metadata": { 200 | "x": 1152, 201 | "y": 756, 202 | "label": "halfScreen" 203 | } 204 | }, 205 | "core/Split_jx318": { 206 | "component": "core/Split", 207 | "metadata": { 208 | "x": 1008, 209 | "y": 720, 210 | "label": "core/Split" 211 | } 212 | }, 213 | "core/Split_occbw": { 214 | "component": "core/Split", 215 | "metadata": { 216 | "x": 684, 217 | "y": 648, 218 | "label": "core/Split" 219 | } 220 | }, 221 | "core/Split_y0bla": { 222 | "component": "core/Split", 223 | "metadata": { 224 | "x": 504, 225 | "y": 468, 226 | "label": "core/Split" 227 | } 228 | }, 229 | "seriously/HueSaturation_bzfvt": { 230 | "component": "seriously/HueSaturation", 231 | "metadata": { 232 | "x": 1080, 233 | "y": 972, 234 | "label": "seriously/HueSaturation" 235 | } 236 | }, 237 | "core/Split_jzzu2": { 238 | "component": "core/Split", 239 | "metadata": { 240 | "x": 1404, 241 | "y": 288, 242 | "label": "core/Split" 243 | } 244 | }, 245 | "core/Kick_7efi8": { 246 | "component": "core/Kick", 247 | "metadata": { 248 | "x": 1476, 249 | "y": 792, 250 | "label": "sendCanvas" 251 | } 252 | }, 253 | "core/MakeFunction_t17n": { 254 | "component": "core/MakeFunction", 255 | "metadata": { 256 | "x": 1296, 257 | "y": 936, 258 | "label": "canvasToJPEG" 259 | } 260 | }, 261 | "dom/GetElement_ah36i": { 262 | "component": "dom/GetElement", 263 | "metadata": { 264 | "x": 1584, 265 | "y": 144, 266 | "label": "saveButton" 267 | } 268 | }, 269 | "interaction/ListenMouse_1u0rk": { 270 | "component": "interaction/ListenMouse", 271 | "metadata": { 272 | "x": 1584, 273 | "y": 288, 274 | "label": "clickSave" 275 | } 276 | }, 277 | "dom/CreateElement_sf6ec": { 278 | "component": "dom/CreateElement", 279 | "metadata": { 280 | "x": 1584, 281 | "y": 1008, 282 | "label": "dom/CreateElement" 283 | } 284 | }, 285 | "dom/SetAttribute_3bmlw": { 286 | "component": "dom/SetAttribute", 287 | "metadata": { 288 | "x": 1584, 289 | "y": 1152, 290 | "label": "dom/SetAttribute" 291 | } 292 | }, 293 | "core/RepeatAsync_grqs3": { 294 | "component": "core/RepeatAsync", 295 | "metadata": { 296 | "x": 1440, 297 | "y": 1152, 298 | "label": "core/RepeatAsync" 299 | } 300 | }, 301 | "dom/GetElement_4houj": { 302 | "component": "dom/GetElement", 303 | "metadata": { 304 | "x": 1584, 305 | "y": 792, 306 | "label": "savedEl" 307 | } 308 | }, 309 | "core/Split_xyb8x": { 310 | "component": "core/Split", 311 | "metadata": { 312 | "x": 1296, 313 | "y": 1080, 314 | "label": "core/Split" 315 | } 316 | }, 317 | "strings/SendString_7g9h8": { 318 | "component": "strings/SendString", 319 | "metadata": { 320 | "x": 1440, 321 | "y": 1008, 322 | "label": "strings/SendString" 323 | } 324 | }, 325 | "core/RepeatAsync_647ff": { 326 | "component": "core/RepeatAsync", 327 | "metadata": { 328 | "x": 612, 329 | "y": 792, 330 | "label": "core/RepeatAsync" 331 | } 332 | }, 333 | "core/Kick_4njgs": { 334 | "component": "core/Kick", 335 | "metadata": { 336 | "x": 756, 337 | "y": 792, 338 | "label": "kickFirstFilter" 339 | } 340 | }, 341 | "dom/GetElement_j674o": { 342 | "component": "dom/GetElement", 343 | "metadata": { 344 | "x": 432, 345 | "y": 1044, 346 | "label": "sliderEl" 347 | } 348 | }, 349 | "interaction/ListenChange_z7m5u": { 350 | "component": "interaction/ListenChange", 351 | "metadata": { 352 | "x": 612, 353 | "y": 1044, 354 | "label": "slid" 355 | } 356 | }, 357 | "math/Multiply_3eldx": { 358 | "component": "math/Multiply", 359 | "metadata": { 360 | "x": 828, 361 | "y": 1152, 362 | "label": "x2xPI" 363 | } 364 | }, 365 | "math/Multiply_3v13k": { 366 | "component": "math/Multiply", 367 | "metadata": { 368 | "x": 828, 369 | "y": 1044, 370 | "label": "tenth" 371 | } 372 | }, 373 | "math/Multiply_rbxrn": { 374 | "component": "math/Multiply", 375 | "metadata": { 376 | "x": 828, 377 | "y": 936, 378 | "label": "tenth" 379 | } 380 | }, 381 | "core/RunTimeout_3wulz": { 382 | "component": "core/RunTimeout", 383 | "metadata": { 384 | "x": 1584, 385 | "y": 432, 386 | "label": "core/RunTimeout" 387 | } 388 | }, 389 | "core/Split_ho5ib": { 390 | "component": "core/Split", 391 | "metadata": { 392 | "x": 1584, 393 | "y": 576, 394 | "label": "core/Split" 395 | } 396 | }, 397 | "dom/AddClass_9rihj": { 398 | "component": "dom/AddClass", 399 | "metadata": { 400 | "x": 1836, 401 | "y": 360, 402 | "label": "dom/AddClass" 403 | } 404 | }, 405 | "core/Split_lbwyz": { 406 | "component": "core/Split", 407 | "metadata": { 408 | "x": 1728, 409 | "y": 144, 410 | "label": "core/Split" 411 | } 412 | }, 413 | "dom/RemoveClass_ec7ug": { 414 | "component": "dom/RemoveClass", 415 | "metadata": { 416 | "x": 1836, 417 | "y": 648, 418 | "label": "dom/RemoveClass" 419 | } 420 | }, 421 | "strings/SendString_zry4n": { 422 | "component": "strings/SendString", 423 | "metadata": { 424 | "x": 1728, 425 | "y": 360, 426 | "label": "countdown" 427 | } 428 | }, 429 | "strings/SendString_lnf0z": { 430 | "component": "strings/SendString", 431 | "metadata": { 432 | "x": 1728, 433 | "y": 648, 434 | "label": "countdown" 435 | } 436 | } 437 | }, 438 | "connections": [ 439 | { 440 | "src": { 441 | "process": "dom/GetElement_f4nkd", 442 | "port": "element" 443 | }, 444 | "tgt": { 445 | "process": "interaction/ListenMouse_1w3vt", 446 | "port": "element" 447 | }, 448 | "metadata": { 449 | "route": "10" 450 | } 451 | }, 452 | { 453 | "src": { 454 | "process": "interaction/ListenMouse_1w3vt", 455 | "port": "click" 456 | }, 457 | "tgt": { 458 | "process": "gum/GetUserMedia_9e9i4", 459 | "port": "start" 460 | }, 461 | "metadata": { 462 | "route": "9" 463 | } 464 | }, 465 | { 466 | "src": { 467 | "process": "gum/GetUserMedia_9e9i4", 468 | "port": "url" 469 | }, 470 | "tgt": { 471 | "process": "dom/SetAttribute_uto4k", 472 | "port": "value" 473 | }, 474 | "metadata": { 475 | "route": "5" 476 | } 477 | }, 478 | { 479 | "src": { 480 | "process": "seriously/Ascii_17c9q", 481 | "port": "out" 482 | }, 483 | "tgt": { 484 | "process": "seriously/SetTarget_kii7s", 485 | "port": "source" 486 | }, 487 | "metadata": { 488 | "route": "5" 489 | } 490 | }, 491 | { 492 | "src": { 493 | "process": "dom/GetElement_85so0", 494 | "port": "element" 495 | }, 496 | "tgt": { 497 | "process": "interaction/ListenMouse_aii7r", 498 | "port": "element" 499 | }, 500 | "metadata": { 501 | "route": "10" 502 | } 503 | }, 504 | { 505 | "src": { 506 | "process": "dom/GetElement_e16dy", 507 | "port": "element" 508 | }, 509 | "tgt": { 510 | "process": "interaction/ListenMouse_bil4d", 511 | "port": "element" 512 | }, 513 | "metadata": { 514 | "route": "10" 515 | } 516 | }, 517 | { 518 | "src": { 519 | "process": "interaction/ListenMouse_aii7r", 520 | "port": "click" 521 | }, 522 | "tgt": { 523 | "process": "routers/KickRouter_bzaiw", 524 | "port": "prev" 525 | }, 526 | "metadata": { 527 | "route": "9" 528 | } 529 | }, 530 | { 531 | "src": { 532 | "process": "interaction/ListenMouse_bil4d", 533 | "port": "click" 534 | }, 535 | "tgt": { 536 | "process": "routers/KickRouter_bzaiw", 537 | "port": "next" 538 | }, 539 | "metadata": { 540 | "route": "9" 541 | } 542 | }, 543 | { 544 | "src": { 545 | "process": "seriously/TVGlitch_e1qre", 546 | "port": "out" 547 | }, 548 | "tgt": { 549 | "process": "seriously/SetTarget_kii7s", 550 | "port": "source" 551 | }, 552 | "metadata": { 553 | "route": "5" 554 | } 555 | }, 556 | { 557 | "src": { 558 | "process": "routers/KickRouter_bzaiw", 559 | "port": "out" 560 | }, 561 | "tgt": { 562 | "process": "seriously/Hex_lx162", 563 | "port": "source" 564 | }, 565 | "metadata": { 566 | "route": "5" 567 | } 568 | }, 569 | { 570 | "src": { 571 | "process": "seriously/Edge_egmkb", 572 | "port": "out" 573 | }, 574 | "tgt": { 575 | "process": "seriously/SetTarget_kii7s", 576 | "port": "source" 577 | }, 578 | "metadata": { 579 | "route": "5" 580 | } 581 | }, 582 | { 583 | "src": { 584 | "process": "seriously/Hex_lx162", 585 | "port": "out" 586 | }, 587 | "tgt": { 588 | "process": "seriously/Edge_egmkb", 589 | "port": "source" 590 | }, 591 | "metadata": { 592 | "route": "5" 593 | } 594 | }, 595 | { 596 | "src": { 597 | "process": "routers/KickRouter_bzaiw", 598 | "port": "out" 599 | }, 600 | "tgt": { 601 | "process": "core/Split_jx318", 602 | "port": "in" 603 | }, 604 | "metadata": { 605 | "route": "5" 606 | } 607 | }, 608 | { 609 | "src": { 610 | "process": "core/Split_jx318", 611 | "port": "out" 612 | }, 613 | "tgt": { 614 | "process": "seriously/Invert_7xnl3", 615 | "port": "source" 616 | }, 617 | "metadata": { 618 | "route": "5" 619 | } 620 | }, 621 | { 622 | "src": { 623 | "process": "seriously/Split_7oj15", 624 | "port": "out" 625 | }, 626 | "tgt": { 627 | "process": "seriously/SetTarget_kii7s", 628 | "port": "source" 629 | }, 630 | "metadata": { 631 | "route": "5" 632 | } 633 | }, 634 | { 635 | "src": { 636 | "process": "seriously/SetSource_szf33", 637 | "port": "out" 638 | }, 639 | "tgt": { 640 | "process": "core/Split_occbw", 641 | "port": "in" 642 | }, 643 | "metadata": { 644 | "route": "5" 645 | } 646 | }, 647 | { 648 | "src": { 649 | "process": "core/Split_occbw", 650 | "port": "out" 651 | }, 652 | "tgt": { 653 | "process": "routers/KickRouter_bzaiw", 654 | "port": "in" 655 | }, 656 | "metadata": { 657 | "route": "5" 658 | } 659 | }, 660 | { 661 | "src": { 662 | "process": "dom/GetElement_z64xf", 663 | "port": "element" 664 | }, 665 | "tgt": { 666 | "process": "core/Split_y0bla", 667 | "port": "in" 668 | }, 669 | "metadata": { 670 | "route": "10" 671 | } 672 | }, 673 | { 674 | "src": { 675 | "process": "core/Split_y0bla", 676 | "port": "out" 677 | }, 678 | "tgt": { 679 | "process": "dom/SetAttribute_uto4k", 680 | "port": "element" 681 | }, 682 | "metadata": { 683 | "route": "10" 684 | } 685 | }, 686 | { 687 | "src": { 688 | "process": "core/Split_y0bla", 689 | "port": "out" 690 | }, 691 | "tgt": { 692 | "process": "seriously/SetSource_szf33", 693 | "port": "source" 694 | }, 695 | "metadata": { 696 | "route": "10" 697 | } 698 | }, 699 | { 700 | "src": { 701 | "process": "routers/KickRouter_bzaiw", 702 | "port": "out" 703 | }, 704 | "tgt": { 705 | "process": "seriously/HueSaturation_bzfvt", 706 | "port": "source" 707 | }, 708 | "metadata": { 709 | "route": "5" 710 | } 711 | }, 712 | { 713 | "src": { 714 | "process": "seriously/HueSaturation_bzfvt", 715 | "port": "out" 716 | }, 717 | "tgt": { 718 | "process": "seriously/SetTarget_kii7s", 719 | "port": "source" 720 | }, 721 | "metadata": { 722 | "route": "5" 723 | } 724 | }, 725 | { 726 | "src": { 727 | "process": "dom/GetElement_ah82a", 728 | "port": "element" 729 | }, 730 | "tgt": { 731 | "process": "core/Split_jzzu2", 732 | "port": "in" 733 | }, 734 | "metadata": { 735 | "route": "10" 736 | } 737 | }, 738 | { 739 | "src": { 740 | "process": "core/Split_jzzu2", 741 | "port": "out" 742 | }, 743 | "tgt": { 744 | "process": "seriously/SetTarget_kii7s", 745 | "port": "target" 746 | }, 747 | "metadata": { 748 | "route": "10" 749 | } 750 | }, 751 | { 752 | "src": { 753 | "process": "core/Split_jzzu2", 754 | "port": "out" 755 | }, 756 | "tgt": { 757 | "process": "core/Kick_7efi8", 758 | "port": "data" 759 | }, 760 | "metadata": { 761 | "route": "10" 762 | } 763 | }, 764 | { 765 | "src": { 766 | "process": "core/Kick_7efi8", 767 | "port": "out" 768 | }, 769 | "tgt": { 770 | "process": "core/MakeFunction_t17n", 771 | "port": "in" 772 | }, 773 | "metadata": { 774 | "route": 0 775 | } 776 | }, 777 | { 778 | "src": { 779 | "process": "core/Split_jx318", 780 | "port": "out" 781 | }, 782 | "tgt": { 783 | "process": "seriously/Split_7oj15", 784 | "port": "sourcea" 785 | }, 786 | "metadata": { 787 | "route": "5" 788 | } 789 | }, 790 | { 791 | "src": { 792 | "process": "seriously/Invert_7xnl3", 793 | "port": "out" 794 | }, 795 | "tgt": { 796 | "process": "seriously/Split_7oj15", 797 | "port": "sourceb" 798 | }, 799 | "metadata": { 800 | "route": "5" 801 | } 802 | }, 803 | { 804 | "src": { 805 | "process": "dom/CreateElement_sf6ec", 806 | "port": "element" 807 | }, 808 | "tgt": { 809 | "process": "dom/SetAttribute_3bmlw", 810 | "port": "element" 811 | }, 812 | "metadata": { 813 | "route": "10" 814 | } 815 | }, 816 | { 817 | "src": { 818 | "process": "dom/GetElement_4houj", 819 | "port": "element" 820 | }, 821 | "tgt": { 822 | "process": "dom/CreateElement_sf6ec", 823 | "port": "container" 824 | }, 825 | "metadata": { 826 | "route": "10" 827 | } 828 | }, 829 | { 830 | "src": { 831 | "process": "core/RepeatAsync_grqs3", 832 | "port": "out" 833 | }, 834 | "tgt": { 835 | "process": "dom/SetAttribute_3bmlw", 836 | "port": "value" 837 | }, 838 | "metadata": { 839 | "route": 0 840 | } 841 | }, 842 | { 843 | "src": { 844 | "process": "core/Split_xyb8x", 845 | "port": "out" 846 | }, 847 | "tgt": { 848 | "process": "core/RepeatAsync_grqs3", 849 | "port": "in" 850 | }, 851 | "metadata": { 852 | "route": 0 853 | } 854 | }, 855 | { 856 | "src": { 857 | "process": "core/MakeFunction_t17n", 858 | "port": "out" 859 | }, 860 | "tgt": { 861 | "process": "core/Split_xyb8x", 862 | "port": "in" 863 | }, 864 | "metadata": { 865 | "route": 0 866 | } 867 | }, 868 | { 869 | "src": { 870 | "process": "core/Split_xyb8x", 871 | "port": "out" 872 | }, 873 | "tgt": { 874 | "process": "strings/SendString_7g9h8", 875 | "port": "in" 876 | }, 877 | "metadata": { 878 | "route": 0 879 | } 880 | }, 881 | { 882 | "src": { 883 | "process": "strings/SendString_7g9h8", 884 | "port": "out" 885 | }, 886 | "tgt": { 887 | "process": "dom/CreateElement_sf6ec", 888 | "port": "tagname" 889 | }, 890 | "metadata": { 891 | "route": "3" 892 | } 893 | }, 894 | { 895 | "src": { 896 | "process": "core/Split_occbw", 897 | "port": "out" 898 | }, 899 | "tgt": { 900 | "process": "core/RepeatAsync_647ff", 901 | "port": "in" 902 | }, 903 | "metadata": { 904 | "route": "0" 905 | } 906 | }, 907 | { 908 | "src": { 909 | "process": "core/RepeatAsync_647ff", 910 | "port": "out" 911 | }, 912 | "tgt": { 913 | "process": "core/Kick_4njgs", 914 | "port": "in" 915 | }, 916 | "metadata": { 917 | "route": 0 918 | } 919 | }, 920 | { 921 | "src": { 922 | "process": "core/Kick_4njgs", 923 | "port": "out" 924 | }, 925 | "tgt": { 926 | "process": "routers/KickRouter_bzaiw", 927 | "port": "index" 928 | }, 929 | "metadata": { 930 | "route": 0 931 | } 932 | }, 933 | { 934 | "src": { 935 | "process": "routers/KickRouter_bzaiw", 936 | "port": "out" 937 | }, 938 | "tgt": { 939 | "process": "seriously/Ascii_17c9q", 940 | "port": "source" 941 | }, 942 | "metadata": { 943 | "route": "5" 944 | } 945 | }, 946 | { 947 | "src": { 948 | "process": "routers/KickRouter_bzaiw", 949 | "port": "out" 950 | }, 951 | "tgt": { 952 | "process": "seriously/TVGlitch_e1qre", 953 | "port": "source" 954 | }, 955 | "metadata": { 956 | "route": "5" 957 | } 958 | }, 959 | { 960 | "src": { 961 | "process": "dom/GetElement_j674o", 962 | "port": "element" 963 | }, 964 | "tgt": { 965 | "process": "interaction/ListenChange_z7m5u", 966 | "port": "element" 967 | }, 968 | "metadata": { 969 | "route": "10" 970 | } 971 | }, 972 | { 973 | "src": { 974 | "process": "interaction/ListenChange_z7m5u", 975 | "port": "value" 976 | }, 977 | "tgt": { 978 | "process": "math/Multiply_3eldx", 979 | "port": "multiplicand" 980 | }, 981 | "metadata": { 982 | "route": "3" 983 | } 984 | }, 985 | { 986 | "src": { 987 | "process": "math/Multiply_3eldx", 988 | "port": "product" 989 | }, 990 | "tgt": { 991 | "process": "seriously/Split_7oj15", 992 | "port": "angle" 993 | }, 994 | "metadata": { 995 | "route": "3" 996 | } 997 | }, 998 | { 999 | "src": { 1000 | "process": "interaction/ListenChange_z7m5u", 1001 | "port": "value" 1002 | }, 1003 | "tgt": { 1004 | "process": "math/Multiply_3v13k", 1005 | "port": "multiplicand" 1006 | }, 1007 | "metadata": { 1008 | "route": "3" 1009 | } 1010 | }, 1011 | { 1012 | "src": { 1013 | "process": "math/Multiply_3v13k", 1014 | "port": "product" 1015 | }, 1016 | "tgt": { 1017 | "process": "seriously/Hex_lx162", 1018 | "port": "size" 1019 | }, 1020 | "metadata": { 1021 | "route": "3" 1022 | } 1023 | }, 1024 | { 1025 | "src": { 1026 | "process": "interaction/ListenChange_z7m5u", 1027 | "port": "value" 1028 | }, 1029 | "tgt": { 1030 | "process": "math/Multiply_rbxrn", 1031 | "port": "multiplicand" 1032 | }, 1033 | "metadata": { 1034 | "route": "3" 1035 | } 1036 | }, 1037 | { 1038 | "src": { 1039 | "process": "math/Multiply_rbxrn", 1040 | "port": "product" 1041 | }, 1042 | "tgt": { 1043 | "process": "seriously/TVGlitch_e1qre", 1044 | "port": "distortion" 1045 | }, 1046 | "metadata": { 1047 | "route": "3" 1048 | } 1049 | }, 1050 | { 1051 | "src": { 1052 | "process": "interaction/ListenChange_z7m5u", 1053 | "port": "value" 1054 | }, 1055 | "tgt": { 1056 | "process": "seriously/HueSaturation_bzfvt", 1057 | "port": "hue" 1058 | }, 1059 | "metadata": { 1060 | "route": "3" 1061 | } 1062 | }, 1063 | { 1064 | "src": { 1065 | "process": "interaction/ListenMouse_1u0rk", 1066 | "port": "click" 1067 | }, 1068 | "tgt": { 1069 | "process": "core/RunTimeout_3wulz", 1070 | "port": "start" 1071 | }, 1072 | "metadata": { 1073 | "route": "9" 1074 | } 1075 | }, 1076 | { 1077 | "src": { 1078 | "process": "core/RunTimeout_3wulz", 1079 | "port": "out" 1080 | }, 1081 | "tgt": { 1082 | "process": "core/Split_ho5ib", 1083 | "port": "in" 1084 | }, 1085 | "metadata": { 1086 | "route": "0" 1087 | } 1088 | }, 1089 | { 1090 | "src": { 1091 | "process": "core/Split_ho5ib", 1092 | "port": "out" 1093 | }, 1094 | "tgt": { 1095 | "process": "core/Kick_7efi8", 1096 | "port": "in" 1097 | }, 1098 | "metadata": { 1099 | "route": "0" 1100 | } 1101 | }, 1102 | { 1103 | "src": { 1104 | "process": "dom/GetElement_ah36i", 1105 | "port": "element" 1106 | }, 1107 | "tgt": { 1108 | "process": "core/Split_lbwyz", 1109 | "port": "in" 1110 | }, 1111 | "metadata": { 1112 | "route": "10" 1113 | } 1114 | }, 1115 | { 1116 | "src": { 1117 | "process": "core/Split_lbwyz", 1118 | "port": "out" 1119 | }, 1120 | "tgt": { 1121 | "process": "interaction/ListenMouse_1u0rk", 1122 | "port": "element" 1123 | }, 1124 | "metadata": { 1125 | "route": "10" 1126 | } 1127 | }, 1128 | { 1129 | "src": { 1130 | "process": "core/Split_lbwyz", 1131 | "port": "out" 1132 | }, 1133 | "tgt": { 1134 | "process": "dom/AddClass_9rihj", 1135 | "port": "element" 1136 | }, 1137 | "metadata": { 1138 | "route": "10" 1139 | } 1140 | }, 1141 | { 1142 | "src": { 1143 | "process": "core/Split_lbwyz", 1144 | "port": "out" 1145 | }, 1146 | "tgt": { 1147 | "process": "dom/RemoveClass_ec7ug", 1148 | "port": "element" 1149 | }, 1150 | "metadata": { 1151 | "route": "10" 1152 | } 1153 | }, 1154 | { 1155 | "src": { 1156 | "process": "strings/SendString_zry4n", 1157 | "port": "out" 1158 | }, 1159 | "tgt": { 1160 | "process": "dom/AddClass_9rihj", 1161 | "port": "class" 1162 | }, 1163 | "metadata": { 1164 | "route": "0" 1165 | } 1166 | }, 1167 | { 1168 | "src": { 1169 | "process": "strings/SendString_lnf0z", 1170 | "port": "out" 1171 | }, 1172 | "tgt": { 1173 | "process": "dom/RemoveClass_ec7ug", 1174 | "port": "class" 1175 | }, 1176 | "metadata": { 1177 | "route": "0" 1178 | } 1179 | }, 1180 | { 1181 | "src": { 1182 | "process": "core/Split_ho5ib", 1183 | "port": "out" 1184 | }, 1185 | "tgt": { 1186 | "process": "strings/SendString_lnf0z", 1187 | "port": "in" 1188 | }, 1189 | "metadata": { 1190 | "route": "0" 1191 | } 1192 | }, 1193 | { 1194 | "src": { 1195 | "process": "interaction/ListenMouse_1u0rk", 1196 | "port": "click" 1197 | }, 1198 | "tgt": { 1199 | "process": "strings/SendString_zry4n", 1200 | "port": "in" 1201 | }, 1202 | "metadata": { 1203 | "route": "9" 1204 | } 1205 | }, 1206 | { 1207 | "data": "#start", 1208 | "tgt": { 1209 | "process": "dom/GetElement_f4nkd", 1210 | "port": "selector" 1211 | } 1212 | }, 1213 | { 1214 | "data": "#vid", 1215 | "tgt": { 1216 | "process": "dom/GetElement_z64xf", 1217 | "port": "selector" 1218 | } 1219 | }, 1220 | { 1221 | "data": "src", 1222 | "tgt": { 1223 | "process": "dom/SetAttribute_uto4k", 1224 | "port": "attribute" 1225 | } 1226 | }, 1227 | { 1228 | "data": "#out", 1229 | "tgt": { 1230 | "process": "dom/GetElement_ah82a", 1231 | "port": "selector" 1232 | } 1233 | }, 1234 | { 1235 | "data": "#prev", 1236 | "tgt": { 1237 | "process": "dom/GetElement_85so0", 1238 | "port": "selector" 1239 | } 1240 | }, 1241 | { 1242 | "data": "#next", 1243 | "tgt": { 1244 | "process": "dom/GetElement_e16dy", 1245 | "port": "selector" 1246 | } 1247 | }, 1248 | { 1249 | "data": 0.01, 1250 | "tgt": { 1251 | "process": "seriously/Split_7oj15", 1252 | "port": "fuzzy" 1253 | } 1254 | }, 1255 | { 1256 | "data": "0.5", 1257 | "tgt": { 1258 | "process": "seriously/Split_7oj15", 1259 | "port": "split" 1260 | } 1261 | }, 1262 | { 1263 | "data": "0.25", 1264 | "tgt": { 1265 | "process": "seriously/HueSaturation_bzfvt", 1266 | "port": "saturation" 1267 | } 1268 | }, 1269 | { 1270 | "data": "return x.toDataURL(\"image/jpeg\", 0.85);", 1271 | "tgt": { 1272 | "process": "core/MakeFunction_t17n", 1273 | "port": "function" 1274 | } 1275 | }, 1276 | { 1277 | "data": "#save", 1278 | "tgt": { 1279 | "process": "dom/GetElement_ah36i", 1280 | "port": "selector" 1281 | } 1282 | }, 1283 | { 1284 | "data": "#saved", 1285 | "tgt": { 1286 | "process": "dom/GetElement_4houj", 1287 | "port": "selector" 1288 | } 1289 | }, 1290 | { 1291 | "data": "src", 1292 | "tgt": { 1293 | "process": "dom/SetAttribute_3bmlw", 1294 | "port": "attribute" 1295 | } 1296 | }, 1297 | { 1298 | "data": "img", 1299 | "tgt": { 1300 | "process": "strings/SendString_7g9h8", 1301 | "port": "string" 1302 | } 1303 | }, 1304 | { 1305 | "data": "0", 1306 | "tgt": { 1307 | "process": "core/Kick_4njgs", 1308 | "port": "data" 1309 | } 1310 | }, 1311 | { 1312 | "data": "#slider", 1313 | "tgt": { 1314 | "process": "dom/GetElement_j674o", 1315 | "port": "selector" 1316 | } 1317 | }, 1318 | { 1319 | "data": "6.283185", 1320 | "tgt": { 1321 | "process": "math/Multiply_3eldx", 1322 | "port": "multiplier" 1323 | } 1324 | }, 1325 | { 1326 | "data": 0.01, 1327 | "tgt": { 1328 | "process": "seriously/TVGlitch_e1qre", 1329 | "port": "verticalsync" 1330 | } 1331 | }, 1332 | { 1333 | "data": "0.04", 1334 | "tgt": { 1335 | "process": "seriously/TVGlitch_e1qre", 1336 | "port": "linesync" 1337 | } 1338 | }, 1339 | { 1340 | "data": 0.01, 1341 | "tgt": { 1342 | "process": "seriously/TVGlitch_e1qre", 1343 | "port": "time" 1344 | } 1345 | }, 1346 | { 1347 | "data": "0.01", 1348 | "tgt": { 1349 | "process": "seriously/TVGlitch_e1qre", 1350 | "port": "bars" 1351 | } 1352 | }, 1353 | { 1354 | "data": 0.15, 1355 | "tgt": { 1356 | "process": "seriously/TVGlitch_e1qre", 1357 | "port": "scanlines" 1358 | } 1359 | }, 1360 | { 1361 | "data": "0.1", 1362 | "tgt": { 1363 | "process": "math/Multiply_3v13k", 1364 | "port": "multiplier" 1365 | } 1366 | }, 1367 | { 1368 | "data": "0.1", 1369 | "tgt": { 1370 | "process": "math/Multiply_rbxrn", 1371 | "port": "multiplier" 1372 | } 1373 | }, 1374 | { 1375 | "data": "3000", 1376 | "tgt": { 1377 | "process": "core/RunTimeout_3wulz", 1378 | "port": "time" 1379 | } 1380 | }, 1381 | { 1382 | "data": "countdown", 1383 | "tgt": { 1384 | "process": "strings/SendString_zry4n", 1385 | "port": "string" 1386 | } 1387 | }, 1388 | { 1389 | "data": "countdown", 1390 | "tgt": { 1391 | "process": "strings/SendString_lnf0z", 1392 | "port": "string" 1393 | } 1394 | } 1395 | ] 1396 | }) -------------------------------------------------------------------------------- /react/the-graph-app.js: -------------------------------------------------------------------------------- 1 | (function (context) { 2 | "use strict"; 3 | 4 | var TheGraph = context.TheGraph; 5 | 6 | 7 | TheGraph.App = React.createClass({ 8 | minZoom: 0.15, 9 | mixins: [TheGraph.mixins.FakeMouse], 10 | getInitialState: function() { 11 | // Autofit 12 | var fit = TheGraph.findFit(this.props.graph, this.props.width, this.props.height); 13 | 14 | return { 15 | x: fit.x, 16 | y: fit.y, 17 | scale: fit.scale, 18 | width: this.props.width, 19 | height: this.props.height, 20 | tooltip: "", 21 | tooltipX: 0, 22 | tooltipY: 0, 23 | tooltipVisible: false, 24 | nodeContext: null 25 | }; 26 | }, 27 | zoomFactor: 0, 28 | zoomX: 0, 29 | zoomY: 0, 30 | onWheel: function (event) { 31 | // TODO: fast transform3d here? 32 | 33 | // Don't bounce 34 | event.preventDefault(); 35 | 36 | if (!this.zoomFactor) { // WAT 37 | this.zoomFactor = 0; 38 | } 39 | 40 | this.zoomFactor += event.deltaY; 41 | this.zoomX = event.nativeEvent.pageX; 42 | this.zoomY = event.nativeEvent.pageY; 43 | requestAnimationFrame(this.scheduleZoom); 44 | }, 45 | scheduleZoom: function () { 46 | if (isNaN(this.zoomFactor)) { return; }; 47 | 48 | var scale = this.state.scale + (this.state.scale * this.zoomFactor/500); 49 | this.zoomFactor = 0; 50 | 51 | if (scale < this.minZoom) { 52 | scale = this.minZoom; 53 | } 54 | if (scale === this.state.scale) { return; } 55 | 56 | // Zoom and pan transform-origin equivalent 57 | var scaleD = scale / this.state.scale; 58 | var currentX = this.state.x; 59 | var currentY = this.state.y; 60 | var oX = this.zoomX; 61 | var oY = this.zoomY; 62 | var x = scaleD * (currentX - oX) + oX; 63 | var y = scaleD * (currentY - oY) + oY; 64 | 65 | this.setState({ 66 | scale: scale, 67 | x: x, 68 | y: y, 69 | tooltipVisible: false 70 | }); 71 | }, 72 | mouseX: 0, 73 | mouseY: 0, 74 | onMouseDown: function (event) { 75 | if (event.button !== 0) { 76 | // Context menu 77 | return; 78 | } 79 | 80 | var x, y; 81 | if (event.touches) { 82 | x = event.touches[0].pageX; 83 | y = event.touches[0].pageY; 84 | } else { 85 | x = event.pageX; 86 | y = event.pageY; 87 | } 88 | this.mouseX = x; 89 | this.mouseY = y; 90 | 91 | window.addEventListener("mousemove", this.onMouseMove); 92 | window.addEventListener("mouseup", this.onMouseUp); 93 | }, 94 | onMouseMove: function (event) { 95 | // Pan 96 | var x, y; 97 | if (event.touches) { 98 | x = event.touches[0].pageX; 99 | y = event.touches[0].pageY; 100 | } else { 101 | x = event.pageX; 102 | y = event.pageY; 103 | } 104 | var deltaX = x - this.mouseX; 105 | var deltaY = y - this.mouseY; 106 | this.setState({ 107 | x: this.state.x + deltaX, 108 | y: this.state.y + deltaY 109 | }); 110 | this.mouseX = x; 111 | this.mouseY = y; 112 | }, 113 | onMouseUp: function (event) { 114 | window.removeEventListener("mousemove", this.onMouseMove); 115 | window.removeEventListener("mouseup", this.onMouseUp); 116 | }, 117 | showNodeContext: function (event) { 118 | this.setState({ 119 | nodeContext: event.detail, 120 | tooltipVisible: false 121 | }); 122 | }, 123 | hideContext: function (event) { 124 | this.setState({ 125 | nodeContext: null 126 | }); 127 | }, 128 | changeTooltip: function (event) { 129 | var tooltip = event.detail.tooltip; 130 | 131 | // Don't go over right edge 132 | var x = event.detail.x + 10; 133 | var width = tooltip.length*6; 134 | if (x + width > this.props.width) { 135 | x = event.detail.x - width - 10; 136 | } 137 | 138 | this.setState({ 139 | tooltip: tooltip, 140 | tooltipVisible: true, 141 | tooltipX: x, 142 | tooltipY: event.detail.y + 20 143 | }); 144 | }, 145 | hideTooltip: function (event) { 146 | this.setState({ 147 | tooltipVisible: false 148 | }); 149 | }, 150 | // onFit: function (event) { 151 | // this.setState({ 152 | // x: event.detail.x, 153 | // y: event.detail.y, 154 | // scale: event.detail.scale 155 | // }); 156 | // }, 157 | componentDidMount: function (rootNode) { 158 | // Mouse listen to window for drag/release outside 159 | 160 | // Tooltip listener 161 | this.getDOMNode().addEventListener("the-graph-tooltip", this.changeTooltip); 162 | this.getDOMNode().addEventListener("the-graph-tooltip-hide", this.hideTooltip); 163 | 164 | // Custom event listeners 165 | this.getDOMNode().addEventListener("the-graph-node-context", this.showNodeContext); 166 | // this.getDOMNode().addEventListener("the-graph-edge-context", this.showEdgeContext); 167 | // this.getDOMNode().addEventListener("the-graph-group-context", this.showGroupContext); 168 | this.getDOMNode().addEventListener("the-graph-context-hide", this.hideContext); 169 | 170 | // Start zoom from middle if zoom before mouse move 171 | this.mouseX = Math.floor( window.innerWidth/2 ); 172 | this.mouseY = Math.floor( window.innerHeight/2 ); 173 | }, 174 | componentDidUpdate: function (prevProps, prevState, rootNode) { 175 | }, 176 | render: function() { 177 | // console.timeEnd("App.render"); 178 | // console.time("App.render"); 179 | 180 | // pan and zoom 181 | var sc = this.state.scale; 182 | var x = this.state.x; 183 | var y = this.state.y; 184 | var transform = "matrix("+sc+",0,0,"+sc+","+x+","+y+")"; 185 | 186 | var scaleClass = sc > TheGraph.zbpBig ? "big" : ( sc > TheGraph.zbpNormal ? "normal" : "small"); 187 | 188 | var contextMenu, contextModal; 189 | if ( this.state.nodeContext ) { 190 | contextMenu = this.state.nodeContext.getContext(); 191 | } 192 | if (contextMenu) { 193 | contextModal = [ 194 | React.DOM.rect({ 195 | className: "context-modal-bg", 196 | width: this.state.width, 197 | height: this.state.height, 198 | onMouseDown: this.hideContext 199 | }), 200 | contextMenu 201 | ]; 202 | } 203 | 204 | return React.DOM.div( 205 | { 206 | className: "the-graph " + scaleClass, 207 | name:"app", 208 | onWheel: this.onWheel, 209 | onMouseDown: this.onMouseDown, 210 | style: { 211 | width: this.state.width, 212 | height: this.state.height 213 | } 214 | }, 215 | React.DOM.svg( 216 | { 217 | width: this.state.width, 218 | height: this.state.height 219 | }, 220 | React.DOM.g( 221 | { 222 | className: "view", 223 | transform: transform 224 | }, 225 | TheGraph.Graph({ 226 | ref: "graph", 227 | graph: this.props.graph, 228 | scale: this.state.scale, 229 | app: this 230 | }) 231 | ), 232 | TheGraph.Tooltip({ 233 | ref: "tooltip", 234 | x: this.state.tooltipX, 235 | y: this.state.tooltipY, 236 | visible: this.state.tooltipVisible, 237 | label: this.state.tooltip 238 | }), 239 | React.DOM.g( 240 | { 241 | className: "context", 242 | children: contextModal 243 | } 244 | ) 245 | ) 246 | ); 247 | } 248 | }); 249 | 250 | 251 | })(this); -------------------------------------------------------------------------------- /react/the-graph-edge.js: -------------------------------------------------------------------------------- 1 | (function (context) { 2 | "use strict"; 3 | 4 | var TheGraph = context.TheGraph; 5 | 6 | // Const 7 | var CURVE = 50; 8 | 9 | 10 | // Edge view 11 | 12 | TheGraph.Edge = React.createClass({ 13 | mixins: [TheGraph.mixins.Tooltip], 14 | componentWillMount: function() { 15 | // Todo: listen for source/target moving; change state 16 | }, 17 | shouldComponentUpdate: function (nextProps, nextState) { 18 | // Only rerender if changed 19 | return ( 20 | nextProps.sX !== this.props.sX || 21 | nextProps.sY !== this.props.sY || 22 | nextProps.tX !== this.props.tX || 23 | nextProps.tY !== this.props.tY || 24 | nextProps.route !== this.props.route 25 | ); 26 | }, 27 | getTooltipTrigger: function () { 28 | return this.refs.touch.getDOMNode(); 29 | }, 30 | shouldShowTooltip: function () { 31 | return true; 32 | }, 33 | render: function () { 34 | var sourceX = this.props.sX; 35 | var sourceY = this.props.sY; 36 | var targetX = this.props.tX; 37 | var targetY = this.props.tY; 38 | 39 | var c1X, c1Y, c2X, c2Y; 40 | if (targetX < sourceX+CURVE && Math.abs(targetY-sourceY) > TheGraph.nodeSize) { 41 | // Stick out some 42 | c1X = sourceX + CURVE; 43 | c1Y = sourceY; 44 | c2X = targetX - CURVE; 45 | c2Y = targetY; 46 | } else { 47 | // Controls halfway between 48 | c1X = sourceX + (targetX - sourceX)/2; 49 | c1Y = sourceY; 50 | c2X = c1X; 51 | c2Y = targetY; 52 | } 53 | 54 | var path = [ 55 | "M", 56 | sourceX, sourceY, 57 | "C", 58 | c1X, c1Y, 59 | c2X, c2Y, 60 | targetX, targetY 61 | ].join(" "); 62 | 63 | return ( 64 | React.DOM.g( 65 | { 66 | className: "edge route"+this.props.route, 67 | title: this.props.label 68 | }, 69 | React.DOM.path({ 70 | className: "edge-bg", 71 | d: path 72 | }), 73 | React.DOM.path({ 74 | className: "edge-fg", 75 | d: path 76 | }), 77 | React.DOM.path({ 78 | className: "edge-touch", 79 | ref: "touch", 80 | d: path 81 | }) 82 | ) 83 | ); 84 | } 85 | }); 86 | 87 | })(this); -------------------------------------------------------------------------------- /react/the-graph-graph.js: -------------------------------------------------------------------------------- 1 | (function (context) { 2 | "use strict"; 3 | 4 | var TheGraph = context.TheGraph; 5 | 6 | 7 | // Graph view 8 | 9 | TheGraph.Graph = React.createClass({ 10 | mixins: [TheGraph.mixins.FakeMouse], 11 | getInitialState: function() { 12 | return { 13 | graph: this.props.graph 14 | }; 15 | }, 16 | componentDidMount: function () { 17 | this.getDOMNode().addEventListener("the-graph-node-move", this.markDirty); 18 | this.getDOMNode().addEventListener("the-graph-group-move", this.moveGroup); 19 | this.getDOMNode().addEventListener("the-graph-node-remove", this.removeNode); 20 | }, 21 | // triggerFit: function () { 22 | // // Zoom to fit 23 | // var fit = TheGraph.findFit(this.state.graph); 24 | // var fitEvent = new CustomEvent('the-graph-fit', { 25 | // detail: fit, 26 | // bubbles: true 27 | // }); 28 | // this.getDOMNode().dispatchEvent(fitEvent); 29 | // }, 30 | moveGroup: function (event) { 31 | var graph = this.state.graph; 32 | var group = graph.groups[ event.detail.index ]; 33 | var nodes = group.nodes; 34 | 35 | // Move each group member 36 | var len = nodes.length; 37 | for (var i=0; i " + 194 | connection.tgt.port.toUpperCase() + " " + target.metadata.label; 195 | var key = connection.src.process + "() " + connection.src.port.toUpperCase() + " -> " + 196 | connection.tgt.port.toUpperCase() + " " + connection.tgt.process + "()"; 197 | 198 | return TheGraph.Edge({ 199 | key: key, 200 | sX: source.metadata.x + TheGraph.nodeSize, 201 | sY: source.metadata.y + sourcePort.y, 202 | tX: target.metadata.x, 203 | tY: target.metadata.y + targetPort.y, 204 | label: label, 205 | route: route 206 | }); 207 | }); 208 | 209 | return React.DOM.g( 210 | { 211 | className: "graph"//, 212 | // onMouseDown: this.onMouseDown 213 | }, 214 | React.DOM.g({ 215 | className: "groups", 216 | children: groups 217 | }), 218 | React.DOM.g({ 219 | className: "edges", 220 | children: edges 221 | }), 222 | React.DOM.g({ 223 | className: "nodes", 224 | children: nodes 225 | }) 226 | ); 227 | } 228 | }); 229 | 230 | })(this); -------------------------------------------------------------------------------- /react/the-graph-group.js: -------------------------------------------------------------------------------- 1 | (function (context) { 2 | "use strict"; 3 | 4 | var TheGraph = context.TheGraph; 5 | 6 | 7 | // Group view 8 | 9 | TheGraph.Group = React.createClass({ 10 | mouseX: 0, 11 | mouseY: 0, 12 | onMouseDown: function (event) { 13 | // Don't drag graph 14 | event.stopPropagation(); 15 | 16 | // Touch to mouse 17 | var x, y; 18 | if (event.touches) { 19 | x = event.touches[0].pageX; 20 | y = event.touches[0].pageY; 21 | } else { 22 | x = event.pageX; 23 | y = event.pageY; 24 | } 25 | 26 | this.mouseX = x; 27 | this.mouseY = y; 28 | 29 | if (event.button !== 0) { 30 | // Show context menu 31 | this.highlight(); 32 | return; 33 | } 34 | 35 | window.addEventListener("mousemove", this.onMouseMove); 36 | window.addEventListener("mouseup", this.onMouseUp); 37 | 38 | }, 39 | highlight: function () { 40 | var highlightEvent = new CustomEvent('the-graph-group-highlight', { 41 | 'detail': { 42 | index: this.props.index, 43 | x: this.mouseX, 44 | y: this.mouseY 45 | }, 46 | 'bubbles': true 47 | }); 48 | this.getDOMNode().dispatchEvent(highlightEvent); 49 | }, 50 | onMouseMove: function (event) { 51 | // Don't fire on graph 52 | event.stopPropagation(); 53 | 54 | // Touch to mouse 55 | var x, y; 56 | if (event.touches) { 57 | x = event.touches[0].pageX; 58 | y = event.touches[0].pageY; 59 | } else { 60 | x = event.pageX; 61 | y = event.pageY; 62 | } 63 | 64 | var deltaX = Math.round( (x - this.mouseX) / this.props.scale ); 65 | var deltaY = Math.round( (y - this.mouseY) / this.props.scale ); 66 | this.mouseX = x; 67 | this.mouseY = y; 68 | 69 | var moveEvent = new CustomEvent('the-graph-group-move', { 70 | detail: { 71 | index: this.props.index, 72 | x: deltaX, 73 | y: deltaY 74 | }, 75 | bubbles: true 76 | }); 77 | this.getDOMNode().dispatchEvent(moveEvent); 78 | }, 79 | onMouseUp: function (event) { 80 | // Don't fire on graph 81 | event.stopPropagation(); 82 | 83 | window.removeEventListener("mousemove", this.onMouseMove); 84 | window.removeEventListener("mouseup", this.onMouseUp); 85 | }, 86 | render: function() { 87 | var x = this.props.minX - TheGraph.nodeSize/2; 88 | var y = this.props.minY - TheGraph.nodeSize/2; 89 | return ( 90 | React.DOM.g( 91 | { 92 | className: "group" 93 | // transform: "translate("+x+","+y+")" 94 | }, 95 | React.DOM.rect({ 96 | className: "group-box", 97 | x: x, 98 | y: y, 99 | rx: TheGraph.nodeRadius, 100 | ry: TheGraph.nodeRadius, 101 | width: this.props.maxX - this.props.minX + TheGraph.nodeSize*2, 102 | height: this.props.maxY - this.props.minY + TheGraph.nodeSize*2 103 | }), 104 | React.DOM.text({ 105 | className: "group-label drag", 106 | x: x + TheGraph.nodeRadius, 107 | y: y, 108 | children: this.props.label, 109 | onMouseDown: this.onMouseDown 110 | }), 111 | React.DOM.text({ 112 | className: "group-description", 113 | x: x + TheGraph.nodeRadius, 114 | y: y + 10 + TheGraph.nodeRadius, 115 | children: this.props.description 116 | }) 117 | ) 118 | ); 119 | } 120 | }); 121 | 122 | 123 | })(this); -------------------------------------------------------------------------------- /react/the-graph-node.js: -------------------------------------------------------------------------------- 1 | (function (context) { 2 | "use strict"; 3 | 4 | var TheGraph = context.TheGraph; 5 | 6 | // Font Awesome 7 | var faKeys = Object.keys(TheGraph.FONT_AWESOME); 8 | 9 | // Node view 10 | 11 | TheGraph.Node = React.createClass({ 12 | mixins: [ 13 | TheGraph.mixins.FakeMouse, 14 | TheGraph.mixins.Tooltip 15 | ], 16 | getInitialState: function() { 17 | return { 18 | // Random icon just for fun 19 | icon: faKeys[ Math.floor(Math.random()*faKeys.length) ] 20 | }; 21 | }, 22 | componentDidMount: function () { 23 | // Mouse listen to window for drag/release outside 24 | // window.addEventListener("mousemove", this.onMouseMove); 25 | // window.addEventListener("mouseup", this.onMouseUp); 26 | 27 | // Right-click 28 | this.getDOMNode().addEventListener("contextmenu", this.showContext); 29 | }, 30 | mouseX: 0, 31 | mouseY: 0, 32 | onMouseDown: function (event) { 33 | // Don't drag graph 34 | event.stopPropagation(); 35 | 36 | // Touch to mouse 37 | var x, y; 38 | if (event.touches) { 39 | x = event.touches[0].pageX; 40 | y = event.touches[0].pageY; 41 | } else { 42 | x = event.pageX; 43 | y = event.pageY; 44 | } 45 | 46 | if (event.button !== 0) { 47 | // Show context menu 48 | // this.showContext(); 49 | return; 50 | } 51 | 52 | this.mouseX = x; 53 | this.mouseY = y; 54 | 55 | window.addEventListener("mousemove", this.onMouseMove); 56 | window.addEventListener("mouseup", this.onMouseUp); 57 | 58 | }, 59 | onMouseMove: function (event) { 60 | // Don't fire on graph 61 | event.stopPropagation(); 62 | 63 | // Touch to mouse 64 | var x, y; 65 | if (event.touches) { 66 | x = event.touches[0].pageX; 67 | y = event.touches[0].pageY; 68 | } else { 69 | x = event.pageX; 70 | y = event.pageY; 71 | } 72 | 73 | var scale = this.props.app.state.scale; 74 | 75 | var deltaX = Math.round( (x - this.mouseX) / scale ); 76 | var deltaY = Math.round( (y - this.mouseY) / scale ); 77 | this.props.process.metadata.x += deltaX; 78 | this.props.process.metadata.y += deltaY; 79 | this.mouseX = x; 80 | this.mouseY = y; 81 | 82 | var highlightEvent = new CustomEvent('the-graph-node-move', { 83 | detail: null, 84 | bubbles: true 85 | }); 86 | this.getDOMNode().dispatchEvent(highlightEvent); 87 | }, 88 | onMouseUp: function (event) { 89 | // Don't fire on graph 90 | event.stopPropagation(); 91 | 92 | window.removeEventListener("mousemove", this.onMouseMove); 93 | window.removeEventListener("mouseup", this.onMouseUp); 94 | }, 95 | triggerRemove: function () { 96 | var contextEvent = new CustomEvent('the-graph-node-remove', { 97 | detail: this.props.key, 98 | bubbles: true 99 | }); 100 | this.getDOMNode().dispatchEvent(contextEvent); 101 | }, 102 | showContext: function (event) { 103 | // Don't show native context menu 104 | event.preventDefault(); 105 | 106 | var contextEvent = new CustomEvent('the-graph-node-context', { 107 | detail: this, 108 | bubbles: true 109 | }); 110 | this.getDOMNode().dispatchEvent(contextEvent); 111 | }, 112 | getContext: function () { 113 | var scale = this.props.app.state.scale; 114 | var appX = this.props.app.state.x; 115 | var appY = this.props.app.state.y; 116 | var x = (this.props.x + TheGraph.nodeSize/2) * scale + appX; 117 | var y = (this.props.y + TheGraph.nodeSize/2) * scale + appY; 118 | return TheGraph.NodeMenu({ 119 | key: "context." + this.props.key, 120 | label: this.props.label, 121 | node: this, 122 | process: this.props.process, 123 | x: x, 124 | y: y 125 | }); 126 | }, 127 | getTooltipTrigger: function () { 128 | return this.getDOMNode(); 129 | }, 130 | shouldShowTooltip: function () { 131 | // HACK 132 | return (this.props.app.state.scale < TheGraph.zbpNormal); 133 | }, 134 | shouldComponentUpdate: function (nextProps, nextState) { 135 | // Only rerender if moved 136 | return ( 137 | nextProps.x !== this.props.x || 138 | nextProps.y !== this.props.y 139 | ); 140 | }, 141 | render: function() { 142 | var metadata = this.props.process.metadata; 143 | 144 | var label = this.props.label; 145 | var sublabel = this.props.process.component; 146 | if (sublabel === label) { 147 | sublabel = ""; 148 | } 149 | var x = this.props.x; 150 | var y = this.props.y; 151 | 152 | // Ports 153 | var keys, count, index; 154 | 155 | // Inports 156 | var inports = metadata.ports.inports; 157 | keys = Object.keys(inports); 158 | count = keys.length; 159 | index = 0; 160 | var inportViews = keys.map(function(key){ 161 | index++; 162 | var info = inports[key]; 163 | info.y = TheGraph.nodeRadius + (TheGraph.nodeSide / (count+1) * index); 164 | return TheGraph.Port(info); 165 | }); 166 | 167 | // Outports 168 | var outports = metadata.ports.outports; 169 | keys = Object.keys(outports); 170 | count = keys.length; 171 | index = 0; 172 | var outportViews = keys.map(function(key){ 173 | index++; 174 | var info = outports[key]; 175 | info.y = TheGraph.nodeRadius + (TheGraph.nodeSide / (count+1) * index); 176 | return TheGraph.Port(info); 177 | }); 178 | 179 | return ( 180 | React.DOM.g( 181 | { 182 | className: "node drag", 183 | name: this.props.key, 184 | key: this.props.key, 185 | title: label, 186 | transform: "translate("+x+","+y+")", 187 | onMouseDown: this.onMouseDown 188 | }, 189 | React.DOM.rect({ 190 | className: "node-bg", // HACK to make the whole g draggable 191 | width: TheGraph.nodeSize, 192 | height: TheGraph.nodeSize + 35 193 | }), 194 | React.DOM.rect({ 195 | className: "node-rect drag", 196 | width: TheGraph.nodeSize, 197 | height: TheGraph.nodeSize, 198 | rx: TheGraph.nodeRadius, 199 | ry: TheGraph.nodeRadius 200 | }), 201 | React.DOM.text({ 202 | className: "icon node-icon drag", 203 | x: TheGraph.nodeSize/2, 204 | y: TheGraph.nodeSize/2, 205 | children: TheGraph.FONT_AWESOME[this.state.icon] 206 | }), 207 | React.DOM.g({ 208 | className: "inports", 209 | children: inportViews 210 | }), 211 | React.DOM.g({ 212 | className: "outports", 213 | children: outportViews 214 | }), 215 | React.DOM.text({ 216 | className: "node-label", 217 | x: TheGraph.nodeSize/2, 218 | y: TheGraph.nodeSize + 20, 219 | children: label 220 | }), 221 | React.DOM.text({ 222 | className: "node-sublabel", 223 | x: TheGraph.nodeSize/2, 224 | y: TheGraph.nodeSize + 35, 225 | children: sublabel 226 | }) 227 | ) 228 | ); 229 | } 230 | }); 231 | 232 | 233 | 234 | TheGraph.NodeMenu = React.createClass({ 235 | radius: 72, 236 | arcs: (function(){ 237 | var angleToX = function (percent, radius) { 238 | return radius * Math.cos(2*Math.PI * percent); 239 | }; 240 | var angleToY = function (percent, radius) { 241 | return radius * Math.sin(2*Math.PI * percent); 242 | }; 243 | var makeArcPath = function (startPercent, endPercent, radius) { 244 | return [ 245 | "M", angleToX(startPercent, radius), angleToY(startPercent, radius), 246 | "A", radius, radius, 0, 0, 0, angleToX(endPercent, radius), angleToY(endPercent, radius) 247 | ].join(" ") 248 | }; 249 | return { 250 | label: makeArcPath(7/8, 5/8, 36), 251 | ins: makeArcPath(5/8, 3/8, 36), 252 | outs: makeArcPath(1/8, -1/8, 36), 253 | remove: makeArcPath(3/8, 1/8, 36) 254 | } 255 | })(), 256 | stopPropagation: function (event) { 257 | // Don't drag graph 258 | event.stopPropagation(); 259 | }, 260 | triggerRemove: function (event) { 261 | this.props.node.triggerRemove(); 262 | 263 | // Hide self (overkill?) 264 | var contextEvent = new CustomEvent('the-graph-context-hide', { 265 | detail: null, 266 | bubbles: true 267 | }); 268 | this.getDOMNode().dispatchEvent(contextEvent); 269 | }, 270 | render: function() { 271 | 272 | if (this.props.process.metadata && this.props.process.metadata.ports) { 273 | // HACK 274 | var scale = this.props.node.props.app.state.scale; 275 | 276 | var ports = this.props.process.metadata.ports; 277 | 278 | var inkeys = Object.keys(ports.inports); 279 | var h = inkeys.length * TheGraph.contextPortSize; 280 | var i = 0; 281 | var inports = inkeys.map( function (key) { 282 | var inport = ports.inports[key]; 283 | var y = 0 - h/2 + i*TheGraph.contextPortSize + TheGraph.contextPortSize/2; 284 | i++; 285 | return TheGraph.PortMenu({ 286 | label: key, 287 | isIn: true, 288 | ox: (inport.x - TheGraph.nodeSize/2) * scale, 289 | oy: (inport.y - TheGraph.nodeSize/2) * scale, 290 | x: -100, 291 | y: y 292 | }); 293 | }); 294 | 295 | var outkeys = Object.keys(ports.outports); 296 | h = outkeys.length * TheGraph.contextPortSize; 297 | i = 0; 298 | var outports = outkeys.map( function (key) { 299 | var outport = ports.outports[key]; 300 | var y = 0 - h/2 + i*TheGraph.contextPortSize + TheGraph.contextPortSize/2; 301 | i++; 302 | return TheGraph.PortMenu({ 303 | label: key, 304 | isIn: false, 305 | ox: (outport.x - TheGraph.nodeSize/2) * scale, 306 | oy: (outport.y - TheGraph.nodeSize/2) * scale, 307 | x: 100, 308 | y: y 309 | }); 310 | }); 311 | } 312 | 313 | return ( 314 | React.DOM.g( 315 | { 316 | className: "context-node", 317 | transform: "translate("+this.props.x+","+this.props.y+")" 318 | }, 319 | React.DOM.text({ 320 | className: "context-node-label", 321 | x: 0, 322 | y: 0 - this.radius - 25, 323 | children: this.props.label 324 | }), 325 | React.DOM.g({ 326 | className: "context-inports", 327 | children: inports 328 | }), 329 | React.DOM.g({ 330 | className: "context-outports", 331 | children: outports 332 | }), 333 | React.DOM.g( 334 | { 335 | className: "context-slice context-node-info" 336 | // onMouseDown: this.stopPropagation, 337 | // onClick: this.triggerRemove 338 | }, 339 | React.DOM.path({ 340 | className: "context-arc context-node-info-bg", 341 | d: this.arcs.label 342 | }), 343 | React.DOM.text({ 344 | className: "icon context-icon context-node-info-icon", 345 | x: 0, 346 | y: -48, 347 | children: TheGraph.FONT_AWESOME["info-circle"] 348 | }) 349 | ), 350 | React.DOM.g( 351 | { 352 | className: "context-slice context-node-delete click", 353 | onMouseDown: this.stopPropagation, 354 | onClick: this.triggerRemove 355 | }, 356 | React.DOM.path({ 357 | className: "context-arc context-node-delete-bg", 358 | d: this.arcs.remove 359 | }), 360 | React.DOM.text({ 361 | className: "icon context-icon context-node-delete-icon", 362 | x: 0, 363 | y: 48, 364 | children: TheGraph.FONT_AWESOME["trash-o"] 365 | }) 366 | ), 367 | React.DOM.path({ 368 | className: "context-arc context-node-ins-bg", 369 | d: this.arcs.ins 370 | }), 371 | React.DOM.path({ 372 | className: "context-circle-x", 373 | d: "M -51 -51 L 51 51 M -51 51 L 51 -51" 374 | }), 375 | React.DOM.path({ 376 | className: "context-arc context-node-outs-bg", 377 | d: this.arcs.outs 378 | }), 379 | React.DOM.circle({ 380 | className: "context-circle", 381 | r: this.radius 382 | }), 383 | React.DOM.rect({ 384 | className: "node-rect", 385 | x: -24, 386 | y: -24, 387 | width: 48, 388 | height: 48, 389 | rx: TheGraph.nodeRadius, 390 | ry: TheGraph.nodeRadius 391 | }), 392 | React.DOM.text({ 393 | className: "icon context-icon", 394 | children: TheGraph.FONT_AWESOME[this.props.node.state.icon] 395 | }) 396 | ) 397 | ); 398 | } 399 | }); 400 | 401 | 402 | })(this); -------------------------------------------------------------------------------- /react/the-graph-port.js: -------------------------------------------------------------------------------- 1 | (function (context) { 2 | "use strict"; 3 | 4 | var TheGraph = context.TheGraph; 5 | 6 | 7 | // Port view 8 | 9 | TheGraph.Port = React.createClass({ 10 | render: function() { 11 | return ( 12 | React.DOM.g( 13 | { 14 | className: "port" 15 | }, 16 | React.DOM.circle({ 17 | className: "port-circle", 18 | cx: this.props.x, 19 | cy: this.props.y, 20 | r: 4 21 | }), 22 | React.DOM.text({ 23 | className: "port-label port-label-"+this.props.label.length, 24 | x: this.props.x, 25 | y: this.props.y, 26 | children: this.props.label 27 | }) 28 | ) 29 | ); 30 | } 31 | }); 32 | 33 | TheGraph.PortMenu = React.createClass({ 34 | render: function() { 35 | var path = [ 36 | "M", this.props.ox, this.props.oy, 37 | "L", this.props.x, this.props.y 38 | ].join(" "); 39 | 40 | return ( 41 | React.DOM.g( 42 | { 43 | className: "context-port click context-port-"+(this.props.isIn ? "in" : "out") 44 | }, 45 | React.DOM.path({ 46 | className: "context-port-path", 47 | d: path 48 | }), 49 | React.DOM.rect({ 50 | className: "context-port-bg", 51 | rx: TheGraph.nodeRadius, 52 | ry: TheGraph.nodeRadius, 53 | x: this.props.x + (this.props.isIn ? -120 : 0), 54 | y: this.props.y - TheGraph.contextPortSize/2, 55 | width: 120, 56 | height: TheGraph.contextPortSize-1 57 | }), 58 | React.DOM.circle({ 59 | className: "context-port-hole", 60 | cx: this.props.x, 61 | cy: this.props.y, 62 | r: 10 63 | }), 64 | React.DOM.text({ 65 | className: "context-port-label", 66 | x: this.props.x + (this.props.isIn ? -20 : 20), 67 | y: this.props.y, 68 | children: this.props.label 69 | }) 70 | ) 71 | ); 72 | } 73 | }); 74 | 75 | 76 | })(this); -------------------------------------------------------------------------------- /react/the-graph-svg.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | /* we want the svg version */ 3 | font-family: 'FontAwesome'; 4 | src: url('//netdna.bootstrapcdn.com/font-awesome/4.0.3/fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg'), 5 | url('//netdna.bootstrapcdn.com/font-awesome/4.0.3/fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'), 6 | url('//netdna.bootstrapcdn.com/font-awesome/4.0.3/fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'), 7 | url('//netdna.bootstrapcdn.com/font-awesome/4.0.3/fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | svg { 13 | -webkit-touch-callout: none; 14 | -webkit-user-select: none; 15 | -khtml-user-select: none; 16 | -moz-user-select: none; 17 | -ms-user-select: none; 18 | user-select: none; 19 | } 20 | .view { 21 | } 22 | .click { 23 | cursor: pointer; 24 | } 25 | .drag { 26 | cursor: pointer; 27 | cursor: -moz-grab; 28 | cursor: -webkit-grab; 29 | cursor: grab; 30 | } 31 | .node-rect { 32 | fill: hsla(0, 0%, 0%, 0.75); 33 | stroke: hsl(0, 0%, 50%); 34 | stroke-width: 2px; 35 | } 36 | .node:hover .node-rect { 37 | fill: hsla(0, 0%, 20%, 0.75); 38 | } 39 | .small .node-rect { 40 | fill: hsla(0, 0%, 10%, 0.9); 41 | stroke: none; 42 | } 43 | .node-bg { 44 | opacity: 0; 45 | } 46 | 47 | path { 48 | fill: none; 49 | } 50 | .edge-bg { 51 | stroke: black; 52 | stroke-width: 4px; 53 | } 54 | .edge:hover .edge-bg { 55 | stroke: gray; 56 | } 57 | .edge-fg { 58 | stroke: white; 59 | stroke-width: 2px; 60 | 61 | transition-property: stroke-width; 62 | transition-duration: 0.5s; 63 | } 64 | .edge-touch { 65 | stroke: white; 66 | stroke-width: 10px; 67 | opacity: 0; 68 | } 69 | .small .edge-bg { 70 | stroke-width: 8px; 71 | } 72 | .small .edge-fg { 73 | stroke-width: 4px; 74 | } 75 | 76 | text { 77 | font-family: "SourceCodePro", "Source Code Pro", Helvetica, Arial, sans-serif; 78 | text-rendering: geometricPrecision; /* makes text scale smoothly */ 79 | font-size: 14px; 80 | fill: white; 81 | text-anchor: start; 82 | } 83 | 84 | .node-label, 85 | .node-sublabel { 86 | text-anchor: middle; 87 | } 88 | .small .node-label { 89 | visibility: hidden; 90 | } 91 | 92 | .node-sublabel { 93 | font-size: 9px; 94 | visibility: hidden; 95 | } 96 | .big .node-sublabel { 97 | visibility: visible; 98 | } 99 | 100 | 101 | 102 | /* Context menu - node */ 103 | .context-modal-bg { 104 | fill: rgba(0,0,0,0.5); 105 | } 106 | 107 | .context-icon { 108 | font-size: 30px; 109 | fill: hsl(0, 0%, 100%); 110 | } 111 | .context-node-label { 112 | text-anchor: middle; 113 | } 114 | .context-arc { 115 | stroke-width: 72px; 116 | stroke: hsla(211, 12%, 19%, 0.95); 117 | } 118 | .context-slice:hover .context-arc, 119 | .context-arc:hover { 120 | stroke: hsla(211, 12%, 40%, 0.95); 121 | } 122 | .context-circle { 123 | stroke: hsl(0, 0%, 75%); 124 | fill: none; 125 | stroke-width: 2px; 126 | } 127 | .context-circle-x { 128 | stroke: hsl(0, 0%, 50%); 129 | fill: none; 130 | stroke-width: 1px; 131 | } 132 | 133 | 134 | /* Context menu - port */ 135 | .context-port-bg { 136 | fill: hsla(211, 12%, 19%, 0.95); 137 | } 138 | .context-port-hole { 139 | fill: hsla(211, 12%, 19%, 0.95); 140 | stroke: hsl(0, 0%, 75%); 141 | } 142 | .context-port-path { 143 | stroke: hsl(0, 0%, 75%); 144 | } 145 | .context-port-label { 146 | fill: hsl(0, 0%, 75%); 147 | dominant-baseline: central; 148 | } 149 | .context-port-in .context-port-label { 150 | text-anchor: end; 151 | } 152 | .context-port-out .context-port-label { 153 | text-anchor: start; 154 | } 155 | 156 | 157 | .tooltip { 158 | opacity: 1.0; 159 | 160 | transition-property: opacity; 161 | transition-duration: 0.3s; 162 | } 163 | .tooltip.hidden { 164 | opacity: 0; 165 | } 166 | .tooltip-bg { 167 | fill: black; 168 | opacity: 0.75; 169 | } 170 | .tooltip-label { 171 | font-size: 10px; 172 | } 173 | 174 | .icon { 175 | font-family: 'FontAwesome'; 176 | text-anchor: middle; 177 | dominant-baseline: central; 178 | } 179 | .node-icon { 180 | font-size: 40px; 181 | fill: hsl(0, 0%, 75%); 182 | 183 | transition-property: font-size, fill; 184 | transition-duration: 0.5s, 0.3s; 185 | } 186 | .small .node-icon { 187 | fill: hsl(0, 0%, 100%); 188 | font-size: 68px; 189 | } 190 | .big .node-icon { 191 | fill: hsl(0, 0%, 25%); 192 | } 193 | 194 | .port-circle { 195 | fill: black; 196 | stroke: hsl(0, 0%, 50%); 197 | stroke-width: 1px; 198 | } 199 | .small .port-circle { 200 | visibility: hidden; 201 | } 202 | .port-label { 203 | visibility: hidden; 204 | font-size: 6px; 205 | /*alignment-baseline: middle;*/ 206 | dominant-baseline: central; 207 | } 208 | .port-label-8 { font-size: 5.8px; } 209 | .port-label-9 { font-size: 5.6px; } 210 | .port-label-10 { font-size: 5.4px; } 211 | .port-label-11 { font-size: 5.2px; } 212 | .port-label-12 { font-size: 5px; } 213 | 214 | .inports .port-label { 215 | text-anchor: start; 216 | } 217 | .outports .port-label { 218 | text-anchor: end; 219 | } 220 | .big .port-label { 221 | visibility: visible; 222 | } 223 | 224 | 225 | .group-box { 226 | fill: hsla(60, 100%, 20%, 0.25); 227 | 228 | transition-property: fill; 229 | transition-duration: 0.3s; 230 | } 231 | .group-box:hover { 232 | fill: hsla(60, 100%, 30%, 0.25); 233 | } 234 | 235 | .group-label { 236 | text-anchor: start; 237 | fill: white; 238 | font-size: 20px; 239 | transition-property: font-size; 240 | transition-duration: 0.5s; 241 | } 242 | .small .group-label { 243 | font-size: 30px; 244 | transition-property: font-size; 245 | transition-duration: 0.5s; 246 | } 247 | 248 | .group-description { 249 | fill: hsl(0, 0%, 75%); 250 | font-size: 12px; 251 | text-anchor: start; 252 | } 253 | .small .group-description { 254 | visibility: hidden; 255 | } 256 | 257 | .route0 .edge-fg { stroke: hsl( 10, 100%, 100%); } 258 | .route1 .edge-fg { stroke: hsl( 10, 100%, 70% ); } 259 | .route2 .edge-fg { stroke: hsl( 30, 100%, 70% ); } 260 | .route3 .edge-fg { stroke: hsl( 60, 100%, 70% ); } 261 | .route4 .edge-fg { stroke: hsl( 90, 100%, 70% ); } 262 | .route5 .edge-fg { stroke: hsl(120, 100%, 70% ); } 263 | .route6 .edge-fg { stroke: hsl(150, 100%, 70% ); } 264 | .route7 .edge-fg { stroke: hsl(180, 100%, 70% ); } 265 | .route8 .edge-fg { stroke: hsl(210, 100%, 70% ); } 266 | .route9 .edge-fg { stroke: hsl(240, 100%, 70% ); } 267 | .route10 .edge-fg { stroke: hsl(270, 100%, 70% ); } 268 | .route11 .edge-fg { stroke: hsl(300, 100%, 70% ); } 269 | .route12 .edge-fg { stroke: hsl(330, 100%, 70% ); } 270 | -------------------------------------------------------------------------------- /react/the-graph-tooltip.js: -------------------------------------------------------------------------------- 1 | (function (context) { 2 | "use strict"; 3 | 4 | var TheGraph = context.TheGraph; 5 | 6 | 7 | // Port view 8 | 9 | TheGraph.Tooltip = React.createClass({ 10 | componentDidUpdate: function (prevProps, prevState, rootNode) { 11 | // HACK til 0.9.0 12 | if (prevProps.label != this.props.label) { 13 | this.refs.label.getDOMNode().textContent = this.props.label; 14 | } 15 | if (prevProps.visible != this.props.visible) { 16 | var c = "tooltip" + (this.props.visible ? "" : " hidden"); 17 | this.getDOMNode().setAttribute("class", c); 18 | } 19 | }, 20 | render: function() { 21 | return ( 22 | React.DOM.g( 23 | { 24 | className: "tooltip" + (this.props.visible ? "" : " hidden"), 25 | transform: "translate("+this.props.x+","+this.props.y+")", 26 | }, 27 | React.DOM.rect({ 28 | className: "tooltip-bg", 29 | x: 0, 30 | y: -14, 31 | rx: 3, 32 | ry: 3, 33 | height: 16, 34 | width: this.props.label.length * 6 35 | }), 36 | React.DOM.text({ 37 | className: "tooltip-label", 38 | ref: "label", 39 | children: this.props.label 40 | }) 41 | ) 42 | ); 43 | } 44 | }); 45 | 46 | 47 | })(this); -------------------------------------------------------------------------------- /react/the-graph.js: -------------------------------------------------------------------------------- 1 | (function (context) { 2 | "use strict"; 3 | 4 | // Dumb module setup 5 | var TheGraph = context.TheGraph = { 6 | nodeSize: 72, 7 | nodeRadius: 8, 8 | nodeSide: 56, 9 | // Context menus 10 | contextPortSize: 36, 11 | // Zoom breakpoints 12 | zbpBig: 1.2, 13 | zbpNormal: 0.4, 14 | zbpSmall: 0.01 15 | }; 16 | 17 | // React setup 18 | React.initializeTouchEvents(true); 19 | 20 | // rAF shim 21 | window.requestAnimationFrame = window.requestAnimationFrame || 22 | window.webkitRequestAnimationFrame || 23 | window.mozRequestAnimationFrame || 24 | window.msRequestAnimationFrame; 25 | 26 | // Mixins to use throughout project 27 | TheGraph.mixins = {}; 28 | 29 | // Touch to mouse 30 | // Class must have onMouseDown onMouseMove onMouseUp 31 | TheGraph.mixins.FakeMouse = { 32 | onTouchStart: function (event) { 33 | event.preventDefault(); 34 | if (event.touches && event.touches.length === 1) { 35 | this.onMouseDown(event); 36 | } 37 | }, 38 | onTouchMove: function (event) { 39 | event.preventDefault(); 40 | this.onMouseMove(event); 41 | }, 42 | onTouchEnd: function (event) { 43 | event.preventDefault(); 44 | if (event.touches && event.touches.length === 0) { 45 | this.onMouseUp(event); 46 | } 47 | }, 48 | componentDidMount: function (rootNode) { 49 | // First touch maps to mouse 50 | this.getDOMNode().addEventListener("touchstart", this.onTouchStart); 51 | this.getDOMNode().addEventListener("touchmove", this.onTouchMove); 52 | this.getDOMNode().addEventListener("touchend", this.onTouchEnd); 53 | this.getDOMNode().addEventListener("touchcancel", this.onTouchEnd); 54 | } 55 | }; 56 | 57 | // Show fake tooltip 58 | // Class must have getTooltipTrigger (dom node) and shouldShowTooltip (boolean) 59 | TheGraph.mixins.Tooltip = { 60 | showTooltip: function (event) { 61 | if ( !this.shouldShowTooltip() ) { return; } 62 | 63 | var tooltipEvent = new CustomEvent('the-graph-tooltip', { 64 | detail: { 65 | tooltip: this.props.label, 66 | x: event.pageX, 67 | y: event.pageY 68 | }, 69 | bubbles: true 70 | }); 71 | this.getDOMNode().dispatchEvent(tooltipEvent); 72 | }, 73 | hideTooltip: function (event) { 74 | if ( !this.shouldShowTooltip() ) { return; } 75 | 76 | var tooltipEvent = new CustomEvent('the-graph-tooltip-hide', { 77 | bubbles: true 78 | }); 79 | this.getDOMNode().dispatchEvent(tooltipEvent); 80 | }, 81 | componentDidMount: function (rootNode) { 82 | if (navigator && navigator.userAgent.indexOf("Firefox") !== -1) { 83 | // HACK Ff does native tooltips on svg elements 84 | return; 85 | } 86 | var tooltipper = this.getTooltipTrigger(); 87 | tooltipper.addEventListener("mouseenter", this.showTooltip); 88 | tooltipper.addEventListener("mouseleave", this.hideTooltip); 89 | } 90 | }; 91 | 92 | TheGraph.findMinMax = function (graph, nodes) { 93 | if (nodes === undefined) { 94 | nodes = Object.keys(graph.processes); 95 | } 96 | if (nodes.length < 1) { 97 | return undefined; 98 | } 99 | var minX = Infinity; 100 | var minY = Infinity; 101 | var maxX = -Infinity; 102 | var maxY = -Infinity; 103 | 104 | var len = nodes.length; 105 | for (var i=0; i maxX) { maxX = process.metadata.x; } 114 | if (process.metadata.y > maxY) { maxY = process.metadata.y; } 115 | } 116 | if (!isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY)) { 117 | minX = 0; 118 | minY = 0; 119 | maxX = 0; 120 | maxY = 0; 121 | return; 122 | } 123 | return { 124 | minX: minX, 125 | minY: minY, 126 | maxX: maxX, 127 | maxY: maxY 128 | }; 129 | }; 130 | 131 | TheGraph.findFit = function (graph, width, height) { 132 | var limits = TheGraph.findMinMax(graph); 133 | if (!limits) { 134 | return {x:0, y:0, scale:1}; 135 | } 136 | limits.minX -= TheGraph.nodeSize; 137 | limits.minY -= TheGraph.nodeSize; 138 | limits.maxX += TheGraph.nodeSize * 2; 139 | limits.maxY += TheGraph.nodeSize * 2; 140 | 141 | var gWidth = limits.maxX - limits.minX; 142 | var gHeight = limits.maxY - limits.minY; 143 | 144 | var scaleX = width / gWidth; 145 | var scaleY = height / gHeight; 146 | 147 | var scale, x, y; 148 | if (scaleX < scaleY) { 149 | scale = scaleX; 150 | x = 0 - limits.minX * scale; 151 | y = 0 - limits.minY * scale + (height-(gHeight*scale))/2; 152 | } else { 153 | scale = scaleY; 154 | x = 0 - limits.minX * scale + (width-(gWidth*scale))/2; 155 | y = 0 - limits.minY * scale; 156 | } 157 | 158 | return { 159 | x: x, 160 | y: y, 161 | scale: scale 162 | }; 163 | } 164 | 165 | 166 | })(this); --------------------------------------------------------------------------------