├── .gitignore ├── .babelrc ├── README.md ├── www ├── index.html └── style.css ├── Makefile ├── package.json └── src ├── events.js ├── eval.js ├── compile.js ├── prims.js └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | {"presets": ["es2015"]} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flowy 2 | Everyday programming for ordinary people. Pre-pre-alpha. 3 | 4 | ## Credits 5 | 6 | Plagiarises lots of code from [@nathan](https://github.com/nathan)'s [Visual](https://github.com/nathan/visual) and also [Phosphorus](https://github.com/nathan/phosphorus), because he's great. 7 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Daft 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | site: 3 | node_modules/.bin/browserify src/main.js -t babelify | uglifyjs --mangle > _site/out.js 4 | cp www/index.html _site/ 5 | cp www/style.css _site/ 6 | 7 | test: 8 | node_modules/.bin/moduleserve --host 0.0.0.0 --port 8888 --transform babel www 9 | 10 | setup: 11 | npm install --dev 12 | curl https://raw.githubusercontent.com/Yaffle/BigInteger/gh-pages/BigInteger.js > node_modules/js-big-integer/BigInteger.js 13 | sed -Ei '' 's/^}(this)/}(module ? module.exports : this)/' node_modules/js-big-integer/BigInteger.js 14 | sed -Ei '' 's/.replace(/\.js$/, ""))$//' node_modules/moduleserve/client.js 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daft", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "bundle": "browserify src/main.js -t babelify | uglifyjs --mangle > _site/out.js" 9 | }, 10 | "author": "tjvr", 11 | "license": "", 12 | "devDependencies": { 13 | "babel-core": "^6.8.0", 14 | "babel-preset-es2015": "^6.6.0", 15 | "babelify": "^7.3.0", 16 | "browserify": "^13.0.1", 17 | "distfs": "^0.1.2", 18 | "moduleserve": "^0.7.1" 19 | }, 20 | "dependencies": { 21 | "fraction.js": "^3.3.1", 22 | "js-big-integer": "^1.0.2", 23 | "tinycolor2": "^1.3.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/events.js: -------------------------------------------------------------------------------- 1 | 2 | export const addEvents = function(cla /*, events... */) { 3 | [].slice.call(arguments, 1).forEach(function(event) { 4 | addEvent(cla, event); 5 | }); 6 | }; 7 | 8 | export const addEvent = function(cla, event) { 9 | var capital = event[0].toUpperCase() + event.substr(1); 10 | 11 | cla.prototype.addEventListener = cla.prototype.addEventListener || function(event, listener) { 12 | var listeners = this['$' + event] = this['$' + event] || []; 13 | listeners.push(listener); 14 | return this; 15 | }; 16 | 17 | cla.prototype.removeEventListener = cla.prototype.removeEventListener || function(event, listener) { 18 | var listeners = this['$' + event]; 19 | if (listeners) { 20 | var i = listeners.indexOf(listener); 21 | if (i !== -1) { 22 | listeners.splice(i, 1); 23 | } 24 | } 25 | return this; 26 | }; 27 | 28 | cla.prototype.dispatchEvent = cla.prototype.dispatchEvent || function(event, arg) { 29 | var listeners = this['$' + event]; 30 | if (listeners) { 31 | listeners.forEach(function(listener) { 32 | listener(arg); 33 | }); 34 | } 35 | var listener = this['on' + event]; 36 | if (listener) { 37 | listener(arg); 38 | } 39 | return this; 40 | }; 41 | 42 | cla.prototype['on' + capital] = function(listener) { 43 | this.addEventListener(event, listener); 44 | return this; 45 | }; 46 | 47 | cla.prototype['dispatch' + capital] = function(arg) { 48 | this.dispatchEvent(event, arg); 49 | return this; 50 | }; 51 | }; 52 | 53 | -------------------------------------------------------------------------------- /www/style.css: -------------------------------------------------------------------------------- 1 | 2 | html, body { 3 | position: absolute; 4 | top: 0; bottom: 0; left: 0; right: 0; 5 | width: 100%; 6 | height: 100%; 7 | margin: 0px; 8 | overflow: hidden; 9 | background: #eee; 10 | 11 | font: 9px/14px Lucida Grande, Verdana, Arial, DejaVu Sans, sans-serif; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | .absolute { 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | -webkit-transform-origin: 0 0; 23 | } 24 | 25 | .no-select { 26 | -webkit-user-select: none; 27 | -moz-user-select: none; 28 | -ms-user-select: none; 29 | -o-user-select: none; 30 | user-select: none; 31 | } 32 | 33 | .metrics-container { 34 | position: absolute; 35 | top: -1px; 36 | left: -1px; 37 | width: 1px; 38 | height: 1px; 39 | overflow: hidden; 40 | visibility: hidden; 41 | pointer-events: none; 42 | } 43 | 44 | .metrics { 45 | position: absolute; 46 | white-space: pre; 47 | padding: 0; 48 | } 49 | 50 | /* * */ 51 | 52 | .frame { 53 | overflow: hidden; 54 | } 55 | .frame-contents { 56 | position: absolute; 57 | cursor: default; 58 | transform-origin: top left; 59 | overflow: visible; 60 | } 61 | 62 | .workspace { 63 | position: relative; 64 | cursor: default; 65 | overflow: hidden; 66 | } 67 | 68 | .world { 69 | position: absolute; 70 | top: 0; 71 | left: 258px; 72 | right: 0; 73 | bottom: 0; 74 | } 75 | 76 | .palette { 77 | position: fixed; 78 | top: 0; 79 | bottom: 0; 80 | left: 0; 81 | width: 256px; 82 | z-index: 101; 83 | border-right: 2px solid #aaa; 84 | background: #fff; 85 | } 86 | 87 | .search { 88 | font: bold 14px/20px Noto Sans, Lucida Grande, Verdana, Arial, DejaVu Sans, sans-serif; 89 | display: block; 90 | height: 28px; 91 | -webkit-appearance: none; 92 | border: 1px solid #ccc; 93 | border-radius: 12px; 94 | padding: 0 6px 0 3px; 95 | color: rgba(0,0,0,0.7); 96 | outline: none; 97 | } 98 | .search:focus { 99 | box-shadow: 0 0 6px 1px rgba(0, 125, 224, 0.4); 100 | border-color: rgba(28, 139, 226, 0.5); 101 | } 102 | 103 | .feedback { 104 | position: fixed; 105 | z-index: 10000; 106 | pointer-events: none; 107 | } 108 | 109 | .dragging { 110 | position: fixed; 111 | z-index: 10001; 112 | } 113 | 114 | /* * */ 115 | 116 | .operator { 117 | background: #7a48c3; 118 | } 119 | 120 | .label { 121 | font: bold 14px/20px Noto Sans, Lucida Grande, Verdana, Arial, DejaVu Sans, sans-serif; 122 | color: #fff; 123 | z-index: 1; 124 | } 125 | 126 | .switch { 127 | z-index: 2; 128 | } 129 | .switch-knob { 130 | transition: transform 100ms linear; 131 | } 132 | 133 | .result { 134 | border-radius: 5px; 135 | } 136 | .result-contents { 137 | color: #000; 138 | white-space: pre; 139 | display: inline-block; 140 | min-height: 12px; 141 | transition: opacity 100ms linear, background 100ms linear; 142 | } 143 | .result-invalid { 144 | opacity: 0.6; 145 | background: rgba(255,255,127,0.5); 146 | } 147 | 148 | .view-Error { 149 | color: #e40046; 150 | } 151 | 152 | .view-Text, 153 | .view-Error { 154 | font: 14px/16px Helvetica Neue, Helvetica, Lucida Grande, Verdana, DejaVu Sans, sans-serif; 155 | } 156 | .view-Text:before, .view-Text:after { 157 | font: bold 14px/20px Baskerville, Georgia, serif; 158 | color: rgba(0,0,0, 0.7); 159 | content: "“"; 160 | } 161 | .view-Text:after { 162 | content: "”"; 163 | } 164 | 165 | .view-Int, 166 | .view-Float, 167 | .view-Frac-num, 168 | .view-Frac-den, 169 | .view-Uncertain-mean, 170 | .view-Uncertain-stddev { 171 | font: 14px/20px Source Code Pro, Monaco, monospace; 172 | } 173 | 174 | .view-Frac-num, 175 | .view-Frac-den { 176 | padding: 0 4px; 177 | font: 16px/20px Source Code Pro, Monaco, monospace; 178 | } 179 | 180 | .view-Uncertain-sym { 181 | font: 16px/20px Source Code Pro, Monaco, monospace; 182 | padding: 0 4px; 183 | margin: -1px 0 0; 184 | } 185 | 186 | .record-title, 187 | .field-sym { 188 | font: bold 12px/20px Helvetica Neue, Helvetica, Lucida Grande, Verdana, DejaVu Sans, sans-serif; 189 | } 190 | .record-title { 191 | text-align: center; 192 | color: #555; 193 | border-bottom: 1px solid #aaa; 194 | } 195 | 196 | .view-Symbol, 197 | .field-name, 198 | .heading { 199 | font: bold 14px/20px Noto Sans, Lucida Grande, Verdana, Arial, DejaVu Sans, sans-serif; 200 | color: #333; 201 | } 202 | .field-name { 203 | text-align: right; 204 | } 205 | .field-sym { 206 | padding: 0 4px; 207 | } 208 | 209 | .view-Bool-yes { color: #2a792a; } 210 | .view-Bool-no { color: #914646; } 211 | 212 | .view-Color { 213 | border: 1px solid #888; 214 | } 215 | 216 | .item-index, 217 | .row-index { 218 | font: small-caps 14px/20px Baskerville, Georgia, serif; 219 | text-align: right; 220 | padding: 0 2px; 221 | } 222 | .item-index:after { 223 | content: "."; 224 | display: inline; 225 | } 226 | 227 | .item-cell { 228 | border-radius: 4px; 229 | background: #e8e8e8; 230 | } 231 | .list-cell { 232 | border-radius: 4px; 233 | background: rgba(239, 111, 46, 0.4); 234 | } 235 | 236 | .row-header { 237 | background: #bec0bf; 238 | } 239 | .table-index, 240 | .header-cell { 241 | border-right: 1px solid #ababab; 242 | height: 20px !important; 243 | } 244 | .row-header, .header-cell { 245 | border-top: 1px solid #bbb; 246 | top: -1px; 247 | border-bottom: 1px solid #626262; 248 | } 249 | .row-record, .record-cell { 250 | border-bottom: 1px solid #bbb; 251 | } 252 | .row-record > .item-index, 253 | .record-cell { 254 | border-right: 1px solid #bbb; 255 | } 256 | 257 | .field { 258 | color: #505050; 259 | font: 14px/20px Noto Sans, Lucida Grande, Verdana, Arial, DejaVu Sans, sans-serif; 260 | margin: 0; 261 | padding: 0; 262 | border: 0; 263 | background: 0; 264 | outline: 0; 265 | box-shadow: none; 266 | } 267 | .field:focus { 268 | pointer-events: auto; 269 | } 270 | 271 | .text-field { 272 | padding: 0 4px 0; 273 | text-align: center; 274 | } 275 | 276 | .field-Menu { 277 | color: #fff; 278 | } 279 | .field-Color { 280 | padding: 0; 281 | } 282 | 283 | .progress { 284 | left: 6px; 285 | top: 7px; 286 | height: 2px; 287 | background: #468; 288 | opacity: 0; 289 | /* transition: .3s; */ 290 | } 291 | .progress-loading { 292 | opacity: 1; 293 | } 294 | .progress-error { 295 | background: #e40046; 296 | background: #ecc; 297 | } 298 | 299 | -------------------------------------------------------------------------------- /src/eval.js: -------------------------------------------------------------------------------- 1 | 2 | function assert(x) { 3 | if (!x) throw "Assertion failed!"; 4 | } 5 | 6 | import {bySpec, typeOf, literal} from "./prims"; 7 | 8 | export class Evaluator { 9 | constructor(nodes, links) { 10 | this.nodes = {}; 11 | nodes.forEach(json => this.add(Node.fromJSON(json))); 12 | links.forEach(json => { 13 | this.link(this.get(json.from), json.index, this.get(json.to)); 14 | }); 15 | nodes.forEach(node => { 16 | if (node.isSink) node.request(); 17 | }); 18 | setInterval(this.tick.bind(this), 1000 / 60); 19 | 20 | this.queue = []; 21 | } 22 | 23 | static fromJSON(json) { 24 | return new Evaluator(json.nodes, json.links); 25 | } 26 | 27 | toJSON() { 28 | var nodes = []; 29 | var links = []; 30 | this.nodes.forEach(node => { 31 | nodes.push(node.toJSON()); 32 | node.inputs.forEach((input, index) => { 33 | links.push({from: input.id, index: index, to: node}); 34 | }); 35 | }); 36 | return {nodes, links}; 37 | } 38 | 39 | add(node, id) { 40 | if (this.nodes.hasOwnProperty(id)) throw "oops"; 41 | this.nodes[id] = node; 42 | node.id = id; 43 | } 44 | 45 | get(nodeId) { 46 | return this.nodes[nodeId]; 47 | } 48 | 49 | linkFromJSON(json) { 50 | return {from: this.get(json.from), index: json.index, to: this.get(json.to)}; 51 | } 52 | 53 | onMessage(json) { 54 | switch (json.action) { 55 | case 'link': 56 | var link = this.linkFromJSON(json); 57 | link.to.replaceArg(link.index, link.from); 58 | return; 59 | case 'unlink': 60 | var link = this.linkFromJSON(json); 61 | link.to.replaceArg(link.index); 62 | return; 63 | case 'setLiteral': 64 | var node = this.get(json.id); 65 | node.assign(json.literal); 66 | return; 67 | case 'setSink': 68 | var node = this.get(json.id); 69 | node.isSink = json.isSink; 70 | return; 71 | case 'create': 72 | var node = json.hasOwnProperty('literal') ? new Observable(json.literal) : new Computed(json.name); 73 | this.add(node, json.id); 74 | return; 75 | case 'destroy': 76 | var node = this.get(json.id); 77 | this.remove(node); 78 | node.destroy(); 79 | return; 80 | default: 81 | throw json; 82 | } 83 | } 84 | 85 | sendMessage(json) {} 86 | 87 | emit(node, value) { 88 | var action = 'emit'; 89 | var id = node.id; 90 | var json = {action, id, value}; 91 | this.sendMessage(json); 92 | } 93 | 94 | progress(node, loaded, total) { 95 | var action = 'progress'; 96 | var id = node.id; 97 | var json = {action, id, loaded, total}; 98 | this.sendMessage(json); 99 | } 100 | 101 | /* * */ 102 | 103 | getPrim(name, inputs) { 104 | var byInputs = bySpec[name]; 105 | if (!byInputs) { 106 | console.log(`No prims for '${name}'`); 107 | return { 108 | output: null, 109 | func: () => {}, 110 | }; 111 | } 112 | 113 | var inputTypes = inputs.map(typeOf); 114 | var hash = inputTypes.join(", "); 115 | var prim = byInputs[hash]; // TODO 116 | if (!prim) { 117 | if (byInputs['Variadic']) { 118 | return byInputs['Variadic']; 119 | } 120 | 121 | console.log(`No prim for '${name}' inputs [${hash}] matched ${Object.keys(byInputs).join("; ")}`); 122 | 123 | // auto vectorisation 124 | var prim = autoVectorise(byInputs, inputs, inputTypes); 125 | if (prim) return prim; 126 | 127 | return { 128 | output: null, 129 | func: () => {}, 130 | }; 131 | // throw new Error(`No prim for '${name}' inputs [${inputs.join(', ')}]`); 132 | } 133 | return prim; 134 | } 135 | 136 | schedule(func) { 137 | if (this.queue.indexOf(func) === -1) { 138 | this.queue.push(func); 139 | } 140 | } 141 | 142 | unschedule(func) { 143 | var index = this.queue.indexOf(func); 144 | if (index !== -1) { 145 | this.queue.splice(index, 1); 146 | } 147 | } 148 | 149 | tick() { 150 | var queue = this.queue.slice(); 151 | this.queue = []; 152 | for (var i=0; i { 164 | if (inputTypes[index] === 'List') { 165 | if (arg.isTask) { 166 | assert(arg.isDone); 167 | arg = arg.result; 168 | } 169 | inputTypes[index] = typeOf(arg[0]); 170 | vectorise.push(index); 171 | } 172 | }); 173 | if (!vectorise.length) return; 174 | var hash = inputTypes.join(", "); 175 | var prim = byInputs[hash]; // TODO 176 | console.log(inputTypes); 177 | 178 | if (prim) { 179 | return { 180 | output: "List Future", 181 | func: function(...args) { 182 | var arrays = vectorise.map(index => args[index]); 183 | var len; 184 | for (var i=0; i {}); 205 | this.emit(threads); 206 | }, 207 | } 208 | } 209 | } 210 | 211 | /*****************************************************************************/ 212 | 213 | export class Observable { 214 | constructor(value) { 215 | this._value = value; 216 | this.subscribers = new Set(); 217 | } 218 | get isObservable() { return true; } 219 | 220 | assign(value) { 221 | value = value; 222 | this._value = value; 223 | var seen = {}; 224 | this.subscribers.forEach(s => s[0].invalidate(new Set())); 225 | } 226 | 227 | request() { 228 | return this._value; 229 | } 230 | 231 | subscribe(obj, index) { 232 | this.subscribers.add([obj, index]); 233 | this.update(); 234 | } 235 | 236 | unsubscribe(obj, index) { 237 | this.subscribers.delete([obj, index]); 238 | this.update(); 239 | } 240 | 241 | update() {} 242 | 243 | } 244 | 245 | /*****************************************************************************/ 246 | 247 | export class Computed extends Observable { 248 | constructor(block, args) { 249 | super(); 250 | this.block = block; 251 | this.args = args = args || []; 252 | this.reprSubscriber = null; 253 | 254 | this.inputs = block.split(" ").filter(x => x[0] === '%'); 255 | 256 | this._isSink = false; 257 | this._needed = false; 258 | this.thread = null; 259 | } 260 | 261 | get isSink() { return this._isSink; } 262 | set isSink(value) { 263 | if (this._isSink === value) return; 264 | this._isSink = value; 265 | this.update(); 266 | } 267 | 268 | update() { 269 | this.needed = this._isSink || !!this.subscribers.size; 270 | } 271 | 272 | get needed() { return this._needed; } 273 | set needed(value) { 274 | if (this._needed === value) return; 275 | this._needed = value; 276 | if (this.thread) this.thread.cancel(); 277 | if (value) { 278 | this.args.forEach((arg, index) => { 279 | if (this.inputs[index] === '%u') return; 280 | arg.subscribe(this, index); 281 | }); 282 | 283 | this.thread = new Thread(this); 284 | this.thread.start(); 285 | } else { 286 | this.thread = null; 287 | 288 | this.args.forEach((arg, index) => { 289 | arg.unsubscribe(this, index); 290 | }); 291 | } 292 | } 293 | 294 | assign(value) { throw "Computeds can't be assigned"; } 295 | _assign(value) { super.assign(value); } 296 | 297 | replaceArg(index, arg) { 298 | var old = this.args[index]; 299 | if (old) old.unsubscribe(this); 300 | if (arg === undefined && index === this.args.length - 1) { 301 | this.args.pop(); 302 | } else { 303 | this.args[index] = arg; 304 | } 305 | if (arg && this.needed && !this.inputs[index] !== '%u') { 306 | arg.subscribe(this, index); 307 | } 308 | if (this.needed) { 309 | this.invalidate(new Set()); 310 | } 311 | if (arg && this.block === 'display %s') { 312 | arg.reprSubscriber = this; 313 | } 314 | } 315 | 316 | invalidate(seen) { 317 | if (seen.has(this)) return; 318 | seen.add(this); 319 | if (this.thread) this.thread.cancel(); 320 | evaluator.emit(this, null); 321 | if (this.needed) { 322 | this.thread = new Thread(this); 323 | this.thread.start(); 324 | } else { 325 | this.thread = null; 326 | } 327 | this.subscribers.forEach(s => s[0].invalidate(seen)); 328 | } 329 | 330 | invalidateChildren() { 331 | var seen = new Set(); 332 | this.subscribers.forEach(s => s[0].invalidate(seen)); 333 | } 334 | 335 | request() { 336 | if (!this.thread) throw "oh dear"; 337 | return this.thread; 338 | } 339 | 340 | } 341 | 342 | /*****************************************************************************/ 343 | 344 | class Thread { 345 | constructor(computed) { 346 | this.target = computed; 347 | this.inputs = computed ? computed.args : null; 348 | 349 | this.prim = null; 350 | 351 | this.isRunning = false; 352 | this.isDone = false; 353 | this.isStopped = false; 354 | this.waiting = []; 355 | this.result = null; 356 | 357 | this.loaded = 0; 358 | this.total = null; 359 | this.lengthComputable = false; 360 | this.requests = []; 361 | 362 | this.evaluator = evaluator; 363 | } 364 | 365 | static fake(prim, inputs) { 366 | var thread = new Thread(null); 367 | 368 | // TODO unevaluated inputs 369 | var tasks = inputs.filter(task => task.isTask); // TODO 370 | thread.awaitAll(tasks, compute.bind(thread)); 371 | 372 | function compute() { 373 | var func = prim.func; 374 | var args = inputs.map((obj, index) => { 375 | if (!obj.isTask) return obj; 376 | // if (this.target.inputs[index] === '%u') { 377 | // return obj; 378 | // } 379 | return obj.result; 380 | }); 381 | 382 | if (prim.coercions) { 383 | for (var i=0; i { 416 | this.prim = evaluator.getPrim(name, inputs); 417 | this.schedule(compute); 418 | }; 419 | 420 | var compute = () => { 421 | var prim = this.prim; 422 | var func = prim.func; 423 | var args = inputs.map((obj, index) => { 424 | if (!obj || !obj.isTask) return obj; 425 | if (this.target.inputs[index] === '%u') { 426 | return obj; 427 | } 428 | return obj.result; 429 | }); 430 | 431 | if (prim.coercions) { 432 | for (var i=0; i { 448 | if (!obj) return obj; 449 | if (this.target.inputs[index] === '%u') { 450 | return obj; 451 | } 452 | return obj.request(); 453 | }); 454 | var tasks = inputs.filter(task => task && task.isTask); // TODO 455 | this.awaitAll(tasks, next); 456 | } 457 | 458 | schedule(func) { 459 | this.func = func; 460 | evaluator.schedule(func); 461 | } 462 | 463 | emit(result) { 464 | if (this.isStopped) return; 465 | this.isDone = true; 466 | this.result = result; 467 | this.dispatchEmit(result); 468 | if (this.target) { 469 | evaluator.emit(this.target, result); 470 | if (this.target.reprSubscriber && !this.target.reprSubscriber.needed) { 471 | var repr = this.target.reprSubscriber; 472 | new Thread(repr).start(); 473 | } 474 | } 475 | } 476 | 477 | withEmit(cb) { 478 | if (this.isDone) { 479 | cb(this.result); 480 | } else { 481 | this.onEmit(cb); 482 | } 483 | } 484 | 485 | awaitAll(tasks, func) { 486 | if (!func) throw "noo"; 487 | tasks.forEach(task => { 488 | if (this.waiting.indexOf(task) === -1) { 489 | this.requests.push(task); 490 | if (!task.isDone) this.waiting.push(task); 491 | task.addEventListener('emit', this.signal.bind(this, task)); 492 | task.addEventListener('progress', this.update.bind(this)); 493 | this.update(); 494 | } 495 | }); 496 | this.func = func; 497 | if (!this.waiting.length) { 498 | this.schedule(func); 499 | return; 500 | } 501 | } 502 | 503 | signal(task) { 504 | var index = this.waiting.indexOf(task); 505 | if (index === -1) return; 506 | this.waiting.splice(index, 1); 507 | evaluator.schedule(this.func); 508 | } 509 | 510 | cancel() { 511 | this.waiting.forEach(task => { 512 | task.removeEventListener(this); 513 | }); 514 | this.isRunning = false; 515 | this.isStopped = true; 516 | evaluator.unschedule(this.func); 517 | // TODO 518 | } 519 | 520 | progress(loaded, total, lengthComputable) { 521 | if (this.isStopped) return; 522 | this.loaded = loaded; 523 | this.total = total; 524 | this.lengthComputable = lengthComputable; 525 | this.dispatchProgress({ 526 | loaded: loaded, 527 | total: total, 528 | lengthComputable: lengthComputable 529 | }); 530 | if (this.target) { 531 | evaluator.progress(this.target, loaded, total); 532 | } 533 | } 534 | 535 | update() { 536 | if (this.isStopped) return; 537 | var requests = this.requests; 538 | var i = requests.length; 539 | var total = 0; 540 | var loaded = 0; 541 | var lengthComputable = true; 542 | var uncomputable = 0; 543 | var done = 0; 544 | while (i--) { 545 | var r = requests[i]; 546 | loaded += r.loaded; 547 | if (r.isDone) { 548 | total += r.loaded; 549 | done += 1; 550 | } else if (r.lengthComputable) { 551 | total += r.total; 552 | } else { 553 | lengthComputable = false; 554 | uncomputable += 1; 555 | } 556 | } 557 | if (!lengthComputable && uncomputable !== requests.length) { 558 | var each = total / (requests.length - uncomputable) * uncomputable; 559 | i = requests.length; 560 | total = 0; 561 | loaded = 0; 562 | lengthComputable = true; 563 | while (i--) { 564 | var r = requests[i]; 565 | if (r.lengthComputable) { 566 | loaded += r.loaded; 567 | total += r.total; 568 | } else { 569 | total += each; 570 | if (r.isDone) loaded += each; 571 | } 572 | } 573 | } 574 | this.progress(loaded, total, lengthComputable); 575 | } 576 | 577 | } 578 | import {addEvents} from "./events"; 579 | addEvents(Thread, 'emit', 'progress'); 580 | 581 | /*****************************************************************************/ 582 | 583 | var compile = function(obj) { 584 | 585 | var source = ""; 586 | var seen = new Set(); 587 | var deps = []; 588 | 589 | var thing = function(obj) { 590 | seen.add(obj); 591 | for (var i=0; i') { /* Operators */ 96 | 97 | if (typeof e[1] === 'string' && DIGIT.test(e[1]) || typeof e[1] === 'number') { 98 | var less = e[0] === '<'; 99 | var x = e[1]; 100 | var y = e[2]; 101 | } else if (typeof e[2] === 'string' && DIGIT.test(e[2]) || typeof e[2] === 'number') { 102 | var less = e[0] === '>'; 103 | var x = e[2]; 104 | var y = e[1]; 105 | } 106 | var nx = +x; 107 | if (x == null || nx !== nx) { 108 | return '(compare(' + val(e[1]) + ', ' + val(e[2]) + ') === ' + (e[0] === '<' ? -1 : 1) + ')'; 109 | } 110 | return (less ? 'numLess' : 'numGreater') + '(' + nx + ', ' + val(y) + ')'; 111 | 112 | } else if (e[0] === '=') { 113 | 114 | if (typeof e[1] === 'string' && DIGIT.test(e[1]) || typeof e[1] === 'number') { 115 | var x = e[1]; 116 | var y = e[2]; 117 | } else if (typeof e[2] === 'string' && DIGIT.test(e[2]) || typeof e[2] === 'number') { 118 | var x = e[2]; 119 | var y = e[1]; 120 | } 121 | var nx = +x; 122 | if (x == null || nx !== nx) { 123 | return '(equal(' + val(e[1]) + ', ' + val(e[2]) + '))'; 124 | } 125 | return '(numEqual(' + nx + ', ' + val(y) + '))'; 126 | 127 | } 128 | }; 129 | 130 | var bool = function(e) { 131 | if (typeof e === 'boolean') { 132 | return e; 133 | } 134 | if (typeof e === 'number' || typeof e === 'string') { 135 | return +e !== 0 && e !== '' && e !== 'false' && e !== false; 136 | } 137 | var v = boolval(e); 138 | return v != null ? v : 'bool(' + val(e, false, true) + ')'; 139 | }; 140 | 141 | var num = function(e) { 142 | if (typeof e === 'number') { 143 | return e || 0; 144 | } 145 | if (typeof e === 'boolean' || typeof e === 'string') { 146 | return +e || 0; 147 | } 148 | var v = numval(e); 149 | return v != null ? v : '(+' + val(e, true) + ' || 0)'; 150 | }; 151 | 152 | var wait = function(dur) { 153 | source += 'save();\n'; 154 | source += 'R.start = self.now();\n'; 155 | source += 'R.duration = ' + dur + ';\n'; 156 | source += 'R.first = true;\n'; 157 | 158 | var id = label(); 159 | source += 'if (self.now() - R.start < R.duration * 1000 || R.first) {\n'; 160 | source += ' R.first = false;\n'; 161 | queue(id); 162 | source += '}\n'; 163 | 164 | source += 'restore();\n'; 165 | }; 166 | 167 | var compile = function(block) { 168 | if (LOG_PRIMITIVES) { 169 | source += 'console.log(' + val(block[0]) + ');\n'; 170 | } 171 | 172 | if (block[0] === 'doBroadcastAndWait') { 173 | 174 | source += 'save();\n'; 175 | source += 'R.threads = broadcast(' + val(block[1]) + ');\n'; 176 | source += 'if (R.threads.indexOf(BASE) !== -1) return;\n'; 177 | var id = label(); 178 | source += 'if (running(R.threads)) {\n'; 179 | queue(id); 180 | source += '}\n'; 181 | source += 'restore();\n'; 182 | 183 | } else if (block[0] === 'doForever') { 184 | 185 | var id = label(); 186 | seq(block[1]); 187 | queue(id); 188 | 189 | } else if (block[0] === 'doForeverIf') { 190 | 191 | var id = label(); 192 | 193 | source += 'if (' + bool(block[1]) + ') {\n'; 194 | seq(block[2]); 195 | source += '}\n'; 196 | 197 | queue(id); 198 | 199 | // } else if (block[0] === 'doForLoop') { 200 | 201 | } else if (block[0] === 'doIf') { 202 | 203 | source += 'if (' + bool(block[1]) + ') {\n'; 204 | seq(block[2]); 205 | source += '}\n'; 206 | 207 | } else if (block[0] === 'doIfElse') { 208 | 209 | source += 'if (' + bool(block[1]) + ') {\n'; 210 | seq(block[2]); 211 | source += '} else {\n'; 212 | seq(block[3]); 213 | source += '}\n'; 214 | 215 | } else if (block[0] === 'doRepeat') { 216 | 217 | source += 'save();\n'; 218 | source += 'R.count = ' + num(block[1]) + ';\n'; 219 | 220 | var id = label(); 221 | 222 | source += 'if (R.count >= 0.5) {\n'; 223 | source += ' R.count -= 1;\n'; 224 | seq(block[2]); 225 | queue(id); 226 | source += '} else {\n'; 227 | source += ' restore();\n'; 228 | source += '}\n'; 229 | 230 | } else if (block[0] === 'doReturn') { 231 | 232 | source += 'endCall();\n'; 233 | source += 'return;\n'; 234 | 235 | } else if (block[0] === 'doUntil') { 236 | 237 | var id = label(); 238 | source += 'if (!' + bool(block[1]) + ') {\n'; 239 | seq(block[2]); 240 | queue(id); 241 | source += '}\n'; 242 | 243 | } else if (block[0] === 'doWhile') { 244 | 245 | var id = label(); 246 | source += 'if (' + bool(block[1]) + ') {\n'; 247 | seq(block[2]); 248 | queue(id); 249 | source += '}\n'; 250 | 251 | } else if (block[0] === 'doWaitUntil') { 252 | 253 | var id = label(); 254 | source += 'if (!' + bool(block[1]) + ') {\n'; 255 | queue(id); 256 | source += '}\n'; 257 | 258 | } 259 | }; 260 | 261 | var source = ''; 262 | var startfn = object.fns.length; 263 | var fns = [0]; 264 | 265 | for (var i = 1; i < script.length; i++) { 266 | compile(script[i]); 267 | } 268 | 269 | var createContinuation = function(source) { 270 | var result = '(function() {\n'; 271 | var brackets = 0; 272 | var delBrackets = 0; 273 | var shouldDelete = false; 274 | var here = 0; 275 | var length = source.length; 276 | while (here < length) { 277 | var i = source.indexOf('{', here); 278 | var j = source.indexOf('}', here); 279 | if (i === -1 && j === -1) { 280 | if (!shouldDelete) { 281 | result += source.slice(here); 282 | } 283 | break; 284 | } 285 | if (i === -1) i = length; 286 | if (j === -1) j = length; 287 | if (shouldDelete) { 288 | if (i < j) { 289 | delBrackets++; 290 | here = i + 1; 291 | } else { 292 | delBrackets--; 293 | if (!delBrackets) { 294 | shouldDelete = false; 295 | } 296 | here = j + 1; 297 | } 298 | } else { 299 | if (i < j) { 300 | result += source.slice(here, i + 1); 301 | brackets++; 302 | here = i + 1; 303 | } else { 304 | result += source.slice(here, j); 305 | here = j + 1; 306 | if (source.substr(j, 8) === '} else {') { 307 | if (brackets > 0) { 308 | result += '} else {'; 309 | here = j + 8; 310 | } else { 311 | shouldDelete = true; 312 | delBrackets = 0; 313 | } 314 | } else { 315 | if (brackets > 0) { 316 | result += '}'; 317 | brackets--; 318 | } 319 | } 320 | } 321 | } 322 | } 323 | result += '})'; 324 | return runtime.scopedEval(result); 325 | }; 326 | 327 | 328 | source += 'if (true) {\n'; 329 | var id = label(); 330 | source += 'lol();' 331 | source += '} else {\n'; 332 | queue(id); 333 | source += '}\n'; 334 | 335 | for (var i = 0; i < fns.length; i++) { 336 | object.fns.push(createContinuation(source.slice(fns[i]))); 337 | } 338 | 339 | var f = object.fns[startfn]; 340 | }; 341 | 342 | 343 | return function(node) { 344 | 345 | warnings = Object.create(null); 346 | 347 | compileNode(node, []); 348 | 349 | for (var key in warnings) { 350 | console.warn(key + (warnings[key] > 1 ? ' (repeated ' + warnings[key] + ' times)' : '')); 351 | } 352 | 353 | }; 354 | 355 | }()); 356 | export {compile}; 357 | 358 | /*****************************************************************************/ 359 | 360 | var runtime = (function() { 361 | 362 | var self, S, R, STACK, C, WARP, CALLS, BASE, THREAD, IMMEDIATE; 363 | 364 | var bool = function(v) { 365 | return +v !== 0 && v !== '' && v !== 'false' && v !== false; 366 | }; 367 | 368 | var mod = function(x, y) { 369 | var r = x % y; 370 | if (r / y < 0) { 371 | r += y; 372 | } 373 | return r; 374 | }; 375 | 376 | var mathFunc = function(f, x) { 377 | switch (f) { 378 | case 'abs': 379 | return Math.abs(x); 380 | case 'floor': 381 | return Math.floor(x); 382 | case 'sqrt': 383 | return Math.sqrt(x); 384 | case 'ceiling': 385 | return Math.ceil(x); 386 | case 'cos': 387 | return Math.cos(x * Math.PI / 180); 388 | case 'sin': 389 | return Math.sin(x * Math.PI / 180); 390 | case 'tan': 391 | return Math.tan(x * Math.PI / 180); 392 | case 'asin': 393 | return Math.asin(x) * 180 / Math.PI; 394 | case 'acos': 395 | return Math.acos(x) * 180 / Math.PI; 396 | case 'atan': 397 | return Math.atan(x) * 180 / Math.PI; 398 | case 'ln': 399 | return Math.log(x); 400 | case 'log': 401 | return Math.log(x) / Math.LN10; 402 | case 'e ^': 403 | return Math.exp(x); 404 | case '10 ^': 405 | return Math.exp(x * Math.LN10); 406 | } 407 | return 0; 408 | }; 409 | 410 | var save = function() { 411 | STACK.push(R); 412 | R = {}; 413 | }; 414 | 415 | var restore = function() { 416 | R = STACK.pop(); 417 | }; 418 | 419 | // var lastCalls = []; 420 | var call = function(spec, id, values) { 421 | // lastCalls.push(spec); 422 | // if (lastCalls.length > 10000) lastCalls.shift(); 423 | var procedure = S.procedures[spec]; 424 | if (procedure) { 425 | STACK.push(R); 426 | CALLS.push(C); 427 | C = { 428 | base: procedure.fn, 429 | fn: S.fns[id], 430 | args: values, 431 | numargs: [], 432 | boolargs: [], 433 | stack: STACK = [], 434 | warp: procedure.warp 435 | }; 436 | R = {}; 437 | if (C.warp || WARP) { 438 | WARP++; 439 | IMMEDIATE = procedure.fn; 440 | } else { 441 | for (var i = CALLS.length, j = 5; i-- && j--;) { 442 | if (CALLS[i].base === procedure.fn) { 443 | var recursive = true; 444 | break; 445 | } 446 | } 447 | if (recursive) { 448 | self.queue[THREAD] = { 449 | sprite: S, 450 | base: BASE, 451 | fn: procedure.fn, 452 | calls: CALLS 453 | }; 454 | } else { 455 | IMMEDIATE = procedure.fn; 456 | } 457 | } 458 | } else { 459 | IMMEDIATE = S.fns[id]; 460 | } 461 | }; 462 | 463 | var endCall = function() { 464 | if (CALLS.length) { 465 | if (WARP) WARP--; 466 | IMMEDIATE = C.fn; 467 | C = CALLS.pop(); 468 | STACK = C.stack; 469 | R = STACK.pop(); 470 | } 471 | }; 472 | 473 | var queue = function(id) { 474 | self.queue[THREAD] = { 475 | sprite: S, 476 | base: BASE, 477 | fn: S.fns[id], 478 | calls: CALLS 479 | }; 480 | }; 481 | 482 | /***************************************************************************/ 483 | 484 | // Internal definition 485 | class Evaluator { 486 | get framerate() { return 60; } 487 | 488 | initRuntime() { 489 | this.queue = []; 490 | this.onError = this.onError.bind(this); 491 | } 492 | 493 | startThread(sprite, base) { 494 | var thread = new Thread(sprite, base); 495 | for (var i = 0; i < this.queue.length; i++) { 496 | var q = this.queue[i]; 497 | if (q && q.sprite === sprite && q.base === base) { 498 | this.queue[i] = thread; 499 | return; 500 | } 501 | } 502 | this.queue.push(thread); 503 | } 504 | 505 | stopThread(thread) { 506 | var index = this.queue.indexOf(thread); 507 | if (index !== -1) { 508 | this.queue.splice(index, 1); 509 | } 510 | } 511 | 512 | start() { 513 | this.isRunning = true; 514 | if (this.interval) return; 515 | addEventListener('error', this.onError); 516 | this.baseTime = Date.now(); 517 | this.interval = setInterval(this.step.bind(this), 1000 / this.framerate); 518 | } 519 | 520 | pause() { 521 | if (this.interval) { 522 | this.baseNow = this.now(); 523 | clearInterval(this.interval); 524 | delete this.interval; 525 | removeEventListener('error', this.onError); 526 | } 527 | this.isRunning = false; 528 | } 529 | 530 | stopAll() { 531 | this.hidePrompt = false; 532 | this.prompter.style.display = 'none'; 533 | this.promptId = this.nextPromptId = 0; 534 | this.queue.length = 0; 535 | this.resetFilters(); 536 | this.stopSounds(); 537 | for (var i = 0; i < this.children.length; i++) { 538 | var c = this.children[i]; 539 | if (c.isClone) { 540 | c.remove(); 541 | this.children.splice(i, 1); 542 | i -= 1; 543 | } else if (c.isSprite) { 544 | c.resetFilters(); 545 | if (c.saying) c.say(''); 546 | c.stopSounds(); 547 | } 548 | } 549 | } 550 | 551 | now() { 552 | return this.baseNow + Date.now() - this.baseTime; 553 | } 554 | 555 | step() { 556 | self = this; 557 | var start = Date.now(); 558 | do { 559 | var queue = this.queue; 560 | for (THREAD = 0; THREAD < queue.length; THREAD++) { 561 | if (queue[THREAD]) { 562 | S = queue[THREAD].sprite; 563 | IMMEDIATE = queue[THREAD].fn; 564 | BASE = queue[THREAD].base; 565 | CALLS = queue[THREAD].calls; 566 | C = CALLS.pop(); 567 | STACK = C.stack; 568 | R = STACK.pop(); 569 | queue[THREAD] = undefined; 570 | WARP = 0; 571 | while (IMMEDIATE) { 572 | var fn = IMMEDIATE; 573 | IMMEDIATE = null; 574 | fn(); 575 | } 576 | STACK.push(R); 577 | CALLS.push(C); 578 | } 579 | } 580 | for (var i = queue.length; i--;) { 581 | if (!queue[i]) queue.splice(i, 1); 582 | } 583 | } while (Date.now() - start < 1000 / this.framerate && queue.length); 584 | this.syncEmissions(); 585 | S = null; 586 | } 587 | 588 | syncEmissions() { 589 | // TODO process emit queue 590 | } 591 | 592 | onError(e) { 593 | clearInterval(this.interval); 594 | } 595 | 596 | handleError(e) { 597 | console.error(e.stack); 598 | } 599 | 600 | } 601 | 602 | /***************************************************************************/ 603 | 604 | class Thread { 605 | constructor(evaluator, sprite, base) { 606 | this.evaluator = evaluator; 607 | this.sprite = sprite, 608 | this.base = base; 609 | this.fn = base; 610 | this.calls = [{args: [], stack: [{}]}]; 611 | this.isRunning = true; 612 | } 613 | 614 | stop() { 615 | this.evaluator.stopThread(this); 616 | this.isRunning = false; 617 | } 618 | 619 | } 620 | 621 | /***************************************************************************/ 622 | 623 | return { 624 | scopedEval: function(source) { 625 | return eval(source); 626 | } 627 | }; 628 | 629 | 630 | }()); 631 | 632 | 633 | -------------------------------------------------------------------------------- /src/prims.js: -------------------------------------------------------------------------------- 1 | 2 | function assert(x) { 3 | if (!x) throw "Assertion failed!"; 4 | } 5 | 6 | import {BigInteger} from "js-big-integer"; 7 | import Fraction from "fraction.js"; 8 | import tinycolor from "tinycolor2"; 9 | 10 | window.BigInteger = BigInteger; 11 | 12 | class Record { 13 | constructor(schema, values) { 14 | this.schema = schema; 15 | this.values = values; 16 | } 17 | 18 | update(newValues) { 19 | var values = {}; 20 | Object.keys(this.values).forEach(name => { 21 | values[name] = this.values[name]; 22 | }); 23 | Object.keys(newValues).forEach(name => { 24 | values[name] = newValues[name]; 25 | }); 26 | // TODO maintain order 27 | return new Record(null, values); 28 | } 29 | 30 | toJSON() { 31 | return this.values; 32 | } 33 | } 34 | 35 | class Schema { 36 | constructor(name, symbols) { 37 | this.name = name; 38 | this.symbols = symbols; 39 | this.symbolSet = new Set(symbols); 40 | // TODO validation function 41 | } 42 | } 43 | var Time = new Schema('Time', ['hour', 'mins', 'secs']); 44 | var Date_ = new Schema('Date', ['year', 'month', 'day']); 45 | var RGB = new Schema('Rgb', ['red', 'green', 'blue']); 46 | var HSV = new Schema('Hsv', ['hue', 'sat', 'val']); 47 | 48 | class Uncertain { 49 | constructor(mean, stddev) { 50 | this.m = +mean; 51 | this.s = +stddev || 0; 52 | } 53 | 54 | static add(a, b) { 55 | return new Uncertain(a.m + b.m, Math.sqrt(a.s * a.s + b.s * b.s)); 56 | } 57 | 58 | static mul(x, y) { 59 | var a = y.m * x.s; 60 | var b = x.m * y.s; 61 | return new Uncertain(x.m * y.m, Math.sqrt(a * a + b * b)); // TODO 62 | } 63 | 64 | } 65 | 66 | function jsonToRecords(obj) { 67 | if (typeof obj === 'object') { 68 | if (obj.constructor === Array) { 69 | return obj.map(jsonToRecords); 70 | } else { 71 | var values = {}; 72 | Object.keys(obj).forEach(key => { 73 | values[key] = jsonToRecords(obj[key]); 74 | }); 75 | return new Record(null, values); 76 | } 77 | } else { 78 | return obj; 79 | } 80 | } 81 | 82 | 83 | 84 | var literals = [ 85 | ["Int", /^-?[0-9]+$/, BigInteger.parseInt], 86 | 87 | ["Frac", /^-?[0-9]+\/[0-9]+$/, x => new Fraction(x)], 88 | 89 | ["Float", /^[0-9]+(?:\.[0-9]+)?e-?[0-9]+$/, parseFloat], // 123[.123]e[-]123 90 | ["Float", /^(?:0|[1-9][0-9]*)?\.[0-9]+$/, parseFloat], // [123].123 91 | ["Float", /^(?:0|[1-9][0-9]*)\.[0-9]*$/, parseFloat], // 123.[123] 92 | 93 | // ["Text", /^/, x => x], 94 | ]; 95 | 96 | var literalsByType = {}; 97 | literals.forEach(l => { 98 | let [type, pat, func] = l; 99 | if (!literalsByType[type]) literalsByType[type] = []; 100 | literalsByType[type].push([pat, func]); 101 | }); 102 | 103 | 104 | export const literal = (value, types) => { 105 | value = value === undefined ? '' : ''+value; 106 | //for (var i=0; i { 249 | let [category, spec, defaults] = p; 250 | var hash = spec.split(" ").map(word => { 251 | return word === '%%' ? "%" 252 | : word === '%br' ? "BR" 253 | : /^%/.test(word) ? "_" 254 | : word; 255 | }).join(" "); 256 | byHash[hash] = spec; 257 | }); 258 | 259 | 260 | class Input { 261 | } 262 | 263 | class Spec { 264 | constructor(category, words, defaults) { 265 | this.category = category; 266 | this.words = words; 267 | // this.inputs = words.filter(x => x.isInput); 268 | this.defaults = defaults; 269 | } 270 | } 271 | 272 | class Imp { 273 | constructor(spec, types, func) { 274 | 275 | } 276 | } 277 | 278 | function el(type, content) { 279 | return ['text', 'view-' + type, content || ''] 280 | } 281 | 282 | function withValue(value, cb) { 283 | if (value && value.isTask) { 284 | value.withEmit(() => cb(value.result)); 285 | } else { 286 | cb(value); 287 | } 288 | } 289 | 290 | export const functions = { 291 | 292 | "UI <- display Error": x => el('Error', x.message || x), 293 | "UI <- display Text": x => el('Text', x), 294 | "UI <- display Int": x => el('Int', ''+x), 295 | "UI <- display Float": x => { 296 | var r = ''+x; 297 | var index = r.indexOf('.'); 298 | if (index === -1) { 299 | r += '.'; 300 | } else if (index !== -1 && !/e/.test(r)) { 301 | if (r.length - index > 3) { 302 | r = x.toFixed(3); 303 | } 304 | } 305 | return el('Float', r); 306 | }, 307 | "UI <- display Frac": frac => { 308 | return ['block', [ 309 | el('Frac-num', ''+frac.n), 310 | ['rect', '#000', 'auto', 2], 311 | el('Frac-den', ''+frac.d), 312 | ]]; 313 | }, 314 | "UI <- display Bool": x => { 315 | var val = x ? 'yes' : 'no'; 316 | return el(`Symbol view-Bool-${val}`, val); 317 | }, 318 | 319 | "UI Future <- display Record": function(record) { 320 | // TODO use RecordView 321 | var schema = record.schema; 322 | var symbols = schema ? schema.symbols : Object.keys(record.values); 323 | var items = []; 324 | var r = ['table', items]; 325 | if (schema) { 326 | r = ['block', [ 327 | ['text', 'record-title', schema.name, 'auto'], 328 | r, 329 | ]]; 330 | } 331 | 332 | symbols.forEach((symbol, index) => { 333 | var cell = ['cell', 'field', ['text', 'ellipsis', ". . ."]]; 334 | var field = ['row', 'field', index, [ 335 | ['text', 'field-name', symbol], 336 | ['text', 'field-sym', "→"], 337 | cell, 338 | ]]; 339 | items.push(field); 340 | 341 | withValue(record.values[symbol], result => { 342 | var prim = this.evaluator.getPrim("display %s", [result]); 343 | var value = prim.func.call(this, result); 344 | cell[2] = value; 345 | this.emit(r); 346 | }); 347 | }); 348 | this.emit(r); 349 | return r; 350 | }, 351 | 352 | "UI Future <- display List": function(list) { 353 | var items = []; 354 | var l = ['table', items]; 355 | 356 | var ellipsis = ['text', 'ellipsis', ". . ."]; 357 | 358 | if (list.length === 0) { 359 | // TODO empty lists 360 | this.emit(l); 361 | return l; 362 | } 363 | 364 | withValue(list[0], first => { 365 | var isRecordTable = false; 366 | if (first instanceof Record) { 367 | var schema = first.schema; 368 | var symbols = schema ? schema.symbols : Object.keys(first.values); 369 | var headings = symbols.map(text => ['cell', 'header', ['text', 'heading', text], text]); 370 | items.push(['row', 'header', null, headings]); 371 | isRecordTable = true; 372 | } 373 | 374 | // TODO header row for list lists 375 | 376 | list.forEach((item, index) => { 377 | var type = typeOf(item); 378 | if (isRecordTable && /Record/.test(type)) { 379 | items.push(['row', 'record', index, [ellipsis]]); 380 | withValue(item, result => { 381 | var values = symbols.map(sym => { 382 | var value = result.values[sym]; 383 | var prim = this.evaluator.getPrim("display %s", [value]); 384 | return ['cell', 'record', prim.func.call(this, value), sym]; 385 | }); 386 | items[index + 1] = ['row', 'record', index, values]; 387 | this.emit(l); 388 | }); 389 | 390 | } else if (/List$/.test(type)) { 391 | items.push(['row', 'list', index, [ellipsis]]); 392 | withValue(item, result => { 393 | var values = result.map((item2, index2) => { 394 | var prim = this.evaluator.getPrim("display %s", [item2]); 395 | return ['cell', 'list', prim.func.call(this, item2), index2 + 1]; 396 | }); 397 | items[index] = ['row', 'list', index, values]; 398 | }); 399 | 400 | } else { 401 | items.push(['row', 'item', index, [ellipsis]]); 402 | withValue(item, result => { 403 | var prim = this.evaluator.getPrim("display %s", [result]); 404 | var value = ['cell', 'item', prim.func.call(this, result)]; 405 | items[isRecordTable ? index + 1 : index] = ['row', 'item', index, [value]]; 406 | }); 407 | } 408 | }); 409 | }); 410 | this.emit(l); 411 | return l; 412 | }, 413 | 414 | "UI <- display Image": image => { 415 | return ['image', image.cloneNode()]; 416 | }, 417 | "UI <- display Color": color => { 418 | return ['rect', color.toHexString(), 24, 24, 'view-Color']; 419 | }, 420 | "UI <- display Uncertain": uncertain => { 421 | return ['inline', [ 422 | el('Uncertain-mean', uncertain.m), 423 | el('Uncertain-sym', "±"), 424 | el('Uncertain-stddev', uncertain.s), 425 | ]]; 426 | }, 427 | 428 | /* Int */ 429 | "Int <- Int + Int": BigInteger.add, 430 | "Int <- Int – Int": BigInteger.subtract, 431 | "Int <- Int × Int": BigInteger.multiply, 432 | "Int <- Int rem Int": BigInteger.remainder, 433 | "Int <- round Int": x => x, 434 | "Bool <- Int = Int": (a, b) => BigInteger.compareTo(a, b) === 0, 435 | "Bool <- Int < Int": (a, b) => BigInteger.compareTo(a, b) === -1, 436 | "Frac <- Int / Int": (a, b) => new Fraction(a, b), 437 | "Float <- float Int": x => +x.toString(), 438 | 439 | /* Frac */ 440 | "Frac <- Frac + Frac": (a, b) => a.add(b), 441 | "Frac <- Frac – Frac": (a, b) => a.sub(b), 442 | "Frac <- Frac × Frac": (a, b) => a.mul(b), 443 | "Frac <- Frac / Frac": (a, b) => a.div(b), 444 | "Float <- float Frac": x => x.n / x.d, 445 | "Int <- round Frac": x => BigInteger.parseInt(''+Math.round(x.n / x.d)), // TODO 446 | 447 | /* Float */ 448 | "Float <- Float + Float": (a, b) => a + b, 449 | "Float <- Float – Float": (a, b) => a - b, 450 | "Float <- Float × Float": (a, b) => a * b, 451 | "Float <- Float / Float": (a, b) => a / b, 452 | "Float <- Float rem Float": (a, b) => (((a % b) + b) % b), 453 | "Int <- round Float": x => BigInteger.parseInt(''+Math.round(x)), 454 | "Float <- float Float": x => x, 455 | "Bool <- Float = Float": (a, b) => a === b, 456 | "Bool <- Float < Float": (a, b) => a < b, 457 | 458 | "Float <- sqrt of Float": x => { return Math.sqrt(x); }, 459 | "Float <- sin of Float": x => Math.sin(Math.PI / 180 * x), 460 | "Float <- cos of Float": x => Math.sin(Math.PI / 180 * x), 461 | "Float <- tan of Float": x => Math.sin(Math.PI / 180 * x), 462 | 463 | /* Complex */ 464 | // TODO 465 | 466 | /* Decimal */ 467 | // TODO 468 | 469 | /* Uncertain */ 470 | "Uncertain <- Float ± Float": (mean, stddev) => new Uncertain(mean, stddev), 471 | "Int <- round Uncertain": x => x.m | 0, 472 | "Float <- float Uncertain": x => x.m, 473 | "Bool <- Uncertain = Uncertain": (a, b) => a.m === b.m && a.s === b.s, 474 | 475 | "Uncertain <- mean List": list => { 476 | if (!list.length) return; 477 | var s = 0; 478 | var s2 = 0; 479 | var n = list.length; 480 | var u; 481 | for (var i=n; i--; ) { 482 | var x = list[i]; 483 | if (x && x.constructor === Uncertain) { 484 | u = u || 0; 485 | // TODO average over uncertainties?? 486 | x = x.m; 487 | } 488 | s += x; 489 | s2 += x * x; 490 | } 491 | var mean = s / n; 492 | var variance = (s2 / (n - 1)) - mean * mean; 493 | // TODO be actually correct 494 | return new Uncertain(mean, Math.sqrt(variance)); 495 | }, 496 | "Float <- mean Uncertain": x => x.m, 497 | "Float <- stddev Uncertain": x => x.s, 498 | "Uncertain <- Uncertain + Uncertain": Uncertain.add, 499 | "Uncertain <- Uncertain × Uncertain": Uncertain.mul, 500 | 501 | /* Bool */ 502 | "Bool <- Bool and Bool": (a, b) => a && b, 503 | "Bool <- Bool or Bool": (a, b) => a || b, 504 | "Bool <- not Bool": x => !x, 505 | "Bool <- Bool": x => !!x, 506 | "Bool <- Bool = Bool": (a, b) => a === b, 507 | 508 | "Any Future <- Uneval if Bool else Uneval": function(tv, cond, fv) { 509 | var ignore = cond ? fv : tv; 510 | var want = cond ? tv : fv; 511 | if (ignore) ignore.unsubscribe(this.target); 512 | if (want) want.subscribe(this.target); 513 | var thread = want.request(); 514 | this.awaitAll(thread.isTask ? [thread] : [], () => { 515 | var result = thread.isTask ? thread.result : thread; 516 | this.emit(result); 517 | this.isRunning = false; 518 | }); 519 | }, 520 | 521 | "List <- repeat Int times: Any": function(times, obj) { 522 | var out = []; 523 | for (var i=0; i x, 539 | "Int <- literal Int": x => x, 540 | "Frac <- literal Frac": x => x, 541 | "Float <- literal Float": x => x, 542 | 543 | "Bool <- Text = Text": (a, b) => a === b, 544 | "Text <- join Variadic": function(...args) { 545 | var arrays = []; 546 | var vectorise = []; 547 | var len; 548 | for (var index=0; index {}); 575 | return threads; 576 | }, 577 | "Text <- join List with Text": (l, x) => l.join(x), 578 | // "Text <- join words List": x => x.join(" "), 579 | "Text List <- split Text by Text": (x, y) => x.split(y), 580 | // "Text List <- split words Text": x => x.trim().split(/\s+/g), 581 | //"Text List <- split lines Text": x => x.split(/\r|\n|\r\n/g), 582 | "Text <- replace Text with Text in Text": (a, b, c) => { 583 | return c.replace(a, b); 584 | }, 585 | 586 | /* List */ 587 | 588 | "List <- list Variadic": (...rest) => { 589 | return rest; 590 | }, 591 | "List <- List concat List": (a, b) => { 592 | return a.concat(b); 593 | }, 594 | "List <- range Int to Int": (from, to) => { 595 | var result = []; 596 | for (var i=from; i<=to; i++) { 597 | result.push(i); 598 | } 599 | return result; 600 | }, 601 | 602 | "Any Future <- item Int of List": function(index, list) { 603 | var value = list[index - 1]; 604 | if (value && value.isTask) { 605 | this.awaitAll([value], () => { 606 | this.emit(value.result); 607 | }); 608 | } else { 609 | this.emit(value); 610 | } 611 | }, 612 | 613 | "Int <- sum List": function(list) { 614 | // TODO 615 | }, 616 | 617 | "Int <- length of List": function(list) { 618 | return list.length; 619 | }, 620 | "Int <- count List": function(list) { 621 | return list.filter(x => !!x).length; 622 | }, 623 | 624 | /* Record */ 625 | "Record <- record with Variadic": (...pairs) => { 626 | var values = {}; 627 | for (var i=0; i { 634 | var record = record || new Record(null, {}); 635 | if (!(record instanceof Record)) return; 636 | var values = {}; 637 | for (var i=0; i { 645 | return src.update(dest.values); 646 | }, 647 | "Any <- Text of Record": (name, record) => { 648 | if (!(record instanceof Record)) return; 649 | return record.values[name]; 650 | }, 651 | "Record Future <- table headings: List BR rows: List": function(symbols, rows) { 652 | var table = []; 653 | var init = false; 654 | rows.forEach((item, index) => { 655 | table.push(null); 656 | withValue(item, result => { 657 | var rec = {}; 658 | for (var i=0; i { 670 | return JSON.stringify(record); 671 | }, 672 | "Text <- List to JSON": record => { 673 | return JSON.stringify(record); 674 | }, 675 | "Text <- Record to JSON": record => { 676 | return JSON.stringify(record); 677 | }, 678 | 679 | "Record <- from JSON Text": text => { 680 | try { 681 | var json = JSON.parse(text); 682 | } catch (e) { 683 | return new Error("Invalid JSON"); 684 | } 685 | return jsonToRecords(json); 686 | }, 687 | 688 | 689 | /* Color */ 690 | // TODO re-implement in-engine 691 | "Bool <- Color = Color": tinycolor.equals, 692 | "Color <- Color": x => x, 693 | "Color <- color Color": x => x, 694 | "Color <- color Text": x => { 695 | var color = tinycolor(x); 696 | if (!color.isValid()) return; 697 | return color; 698 | }, 699 | "Color <- color Rgb": record => { 700 | var values = record.values; 701 | var color = tinycolor({ r: values.red, g: values.green, b: values.blue }); 702 | if (!color.isValid()) return; 703 | return color; 704 | }, 705 | "Color <- color Hsv": record => { 706 | var values = record.values; 707 | var color = tinycolor({ h: values.hue, s: values.sat, v: values.val }); 708 | if (!color.isValid()) return; 709 | return color; 710 | }, 711 | "Color <- mix Color with Float % of Color": (a, mix, b) => tinycolor.mix(a, b, mix), 712 | //"Color <- r Int g Int b Int": (r, g, b) => { 713 | // return tinycolor({r, g, b}); 714 | //}, 715 | //"Color <- h Int s Int v Int": (h, s, v) => { 716 | // return tinycolor({h, s, v}); 717 | //}, 718 | "Float <- brightness of Color": x => x.getBrightness(), 719 | "Float <- luminance of Color": x => x.getLuminance(), 720 | "Color <- spin Color by Int": (color, amount) => color.spin(amount), 721 | "Color <- complement Color": x => x.complement(), 722 | "Color <- invert Color": x => { 723 | var {r, g, b} = x.toRgb(); 724 | return tinycolor({r: 255 - r, g: 255 - g, b: 255 - b}); 725 | }, 726 | 727 | // TODO menus 728 | "Record <- Color to hex": x => x.toHexString(), 729 | "Record <- Color to rgb": x => { 730 | var o = x.toRgb(); 731 | return new Record(RGB, { red: o.r, green: o.g, blue: o.b }); 732 | }, 733 | "Record <- Color to hsv": x => { 734 | var o = x.toHsv(); 735 | return new Record(HSV, { hue: o.h, sat: o.s, val: o.v }); 736 | }, 737 | 738 | // TODO menus 739 | "List <- analogous colors Color": x => x.analogous(), 740 | "List <- triad colors Color": x => x.triad(), 741 | "List <- monochromatic colors Color": x => x.monochromatic(), 742 | 743 | 744 | /* Async tests */ 745 | 746 | "WebPage Future <- get Text": function(url) { 747 | // TODO cors proxy 748 | //var cors = 'http://crossorigin.me/http://'; 749 | var cors = 'http://localhost:1337/'; 750 | url = cors + url.replace(/^https?\:\/\//, ""); 751 | var xhr = new XMLHttpRequest; 752 | xhr.open('GET', url, true); 753 | xhr.onprogress = e => { 754 | this.progress(e.loaded, e.total, e.lengthComputable); 755 | }; 756 | xhr.onload = () => { 757 | if (xhr.status === 200) { 758 | var r = { 759 | contentType: xhr.getResponseHeader('content-type'), 760 | response: xhr.response, 761 | }; 762 | 763 | var mime = r.contentType.split(";")[0]; 764 | var blob = r.response; 765 | if (/^image\//.test(mime)) { 766 | var img = new Image(); 767 | img.addEventListener('load', e => { 768 | this.emit(img); 769 | }); 770 | img.src = URL.createObjectURL(blob); 771 | } else if (mime === 'application/json' || mime === 'text/json') { 772 | var reader = new FileReader; 773 | reader.onloadend = () => { 774 | try { 775 | var json = JSON.parse(reader.result); 776 | } catch (e) { 777 | this.emit(new Error("Invalid JSON")); 778 | return; 779 | } 780 | this.emit(jsonToRecords(json)); 781 | }; 782 | reader.onprogress = function(e) { 783 | //future.progress(e.loaded, e.total, e.lengthComputable); 784 | }; 785 | reader.readAsText(blob); 786 | } else if (/^text\//.test(mime)) { 787 | var reader = new FileReader; 788 | reader.onloadend = () => { 789 | this.emit(reader.result); 790 | }; 791 | reader.onprogress = function(e) { 792 | //future.progress(e.loaded, e.total, e.lengthComputable); 793 | }; 794 | reader.readAsText(blob); 795 | } else { 796 | this.emit(new Error(`Unknown content type: ${mime}`)); 797 | } 798 | } else { 799 | this.emit(new Error('HTTP ' + xhr.status + ': ' + xhr.statusText)); 800 | } 801 | }; 802 | xhr.onerror = () => { 803 | this.emit(new Error('XHR Error')); 804 | }; 805 | xhr.responseType = 'blob'; 806 | setTimeout(xhr.send.bind(xhr)); 807 | }, 808 | 809 | "Time Future <- time": function() { 810 | var update = () => { 811 | if (this.isStopped) { 812 | clearInterval(interval); 813 | return; 814 | } 815 | var d = new Date(); 816 | this.emit(new Record(Time, { 817 | hour: d.getHours(), 818 | mins: d.getMinutes(), 819 | secs: d.getSeconds(), 820 | })); 821 | this.target.invalidateChildren(); 822 | }; 823 | var interval = setInterval(update, 1000); 824 | update(); 825 | }, 826 | 827 | "Date Future <- date": function() { 828 | var update = () => { 829 | if (this.isStopped) { 830 | clearInterval(interval); 831 | return; 832 | } 833 | var d = new Date(); 834 | this.emit(new Record(Date_, { 835 | year: d.getFullYear(), 836 | month: d.getMonth(), 837 | day: d.getDate(), 838 | })); 839 | this.target.invalidateChildren(); 840 | }; 841 | var interval = setInterval(update, 1000); 842 | update(); 843 | }, 844 | 845 | "Bool <- Time < Time": function(a, b) { 846 | var x = a.values; 847 | var y = b.values; 848 | return x.hour < y.hour && x.mins < y.mins && x.secs < y.secs; 849 | }, 850 | "Bool <- Date < Date": function(a, b) { 851 | var x = a.values; 852 | var y = b.values; 853 | return x.year < y.year && x.month < y.month && x.day < y.day; 854 | }, 855 | 856 | 857 | 858 | // "A Future <- delay A by Float secs": (value, time) => { 859 | // // TODO 860 | // }, 861 | // "B Future List <- do (B <- A) for each (A Future List)": (ring, list) => { 862 | // return runtime.map(ring, list); // TODO 863 | // }, 864 | 865 | }; 866 | 867 | let coercions = { 868 | "Text <- Int": x => x.toString(), 869 | "Text <- Frac": x => x.toString(), 870 | "Text <- Float": x => x.toFixed(2), 871 | "Text <- Empty": x => "", 872 | 873 | "Float <- Text": x => +x, 874 | 875 | "List <- Empty": x => [], 876 | 877 | // "List <- Int": x => [x], 878 | // "List <- Frac": x => [x], 879 | // "List <- Float": x => [x], 880 | // "List <- Bool": x => [x], 881 | // "List <- Text": x => [x], 882 | // "List <- Image": x => [x], 883 | // "List <- Uncertain": x => [x], 884 | 885 | "Frac <- Int": x => new Fraction(x, 1), 886 | "Float <- Int": x => +x.toString(), 887 | 888 | "Bool <- List": x => !!x.length, 889 | 890 | "Any <- Int": x => x, 891 | "Any <- Frac": x => x, 892 | "Any <- Float": x => x, 893 | "Any <- Bool": x => x, 894 | "Any <- Empty": x => x, 895 | "Any <- Text": x => x, 896 | "Any <- Image": x => x, 897 | "Any <- Uncertain": x => x, 898 | "Any <- Record": x => x, 899 | "Any <- Time": x => x, 900 | "Any <- Date": x => x, 901 | 902 | "List <- Record": recordToList, 903 | "List <- Time": recordToList, 904 | "List <- Date": recordToList, 905 | 906 | "Record <- Time": x => x, 907 | "Record <- Date": x => x, 908 | "Record <- Rgb": x => x, 909 | "Record <- Hsv": x => x, 910 | 911 | "Uncertain <- Int": x => new Uncertain(x.toString()), 912 | "Uncertain <- Frac": x => new Uncertain(x.n / x.d), 913 | "Uncertain <- Float": x => new Uncertain(x), 914 | }; 915 | function recordToList(record) { 916 | var schema = record.schema; 917 | var values = record.values; 918 | var symbols = schema ? schema.symbols : Object.keys(values); 919 | return symbols.map(name => values[name]); 920 | }; 921 | 922 | 923 | var coercionsByType = {}; 924 | Object.keys(coercions).forEach(spec => { 925 | var info = parseSpec(spec); 926 | assert(info.inputs.length === 1); 927 | let inp = info.inputs[0]; 928 | let out = info.output; 929 | var byInput = coercionsByType[out] = coercionsByType[out] || []; 930 | byInput.push([inp, coercions[spec]]); 931 | }); 932 | 933 | function parseSpec(spec) { 934 | var words = spec.split(/([A-Za-z:]+|[()]|<-)|\s+/g).filter(x => !!x); 935 | var tok = words[0]; 936 | var i = 0; 937 | function next() { tok = words[++i]; } 938 | function peek() { return words[i + 1]; } 939 | 940 | var isType = (tok => /^[A-Z_][a-z]+/.test(tok)); 941 | 942 | function pSpec() { 943 | var words = []; 944 | while (tok && tok !== '<-') { 945 | words.push(tok); 946 | next(); 947 | } 948 | var outputType = words.join(" "); 949 | 950 | assert(tok === '<-'); 951 | next(); 952 | 953 | var words = []; 954 | var inputTypes = []; 955 | while (tok) { 956 | if (tok === '(' || isType(tok)) { 957 | var type = pType(); 958 | assert(type); 959 | inputTypes.push(type); 960 | words.push("_"); 961 | } else { 962 | words.push(tok); 963 | next(); 964 | } 965 | } 966 | 967 | var hash = words.join(" ") 968 | var spec = byHash[hash]; 969 | if (!spec) throw hash; 970 | return { 971 | spec: spec, 972 | inputs: inputTypes, 973 | output: outputType, 974 | } 975 | } 976 | 977 | function pType() { 978 | if (isType(tok)) { 979 | var type = tok; 980 | next(); 981 | assert(type); 982 | return type; //[type]; 983 | } else if (tok === '(') { 984 | next(); 985 | var words = []; 986 | while (tok !== ')') { 987 | if (tok === '<-') { 988 | words = [words]; 989 | words.push("<-"); 990 | next(); 991 | var type = pType(); 992 | assert(type); 993 | words.push(type); 994 | break; 995 | } else if (tok === '*') { 996 | words.push('*'); 997 | next(); 998 | break; 999 | } 1000 | var type = pType(); 1001 | assert(type); 1002 | words.push(type); 1003 | } 1004 | assert(tok === ')'); 1005 | next(); 1006 | return words; 1007 | } 1008 | } 1009 | 1010 | return pSpec(); 1011 | } 1012 | 1013 | var bySpec = {}; 1014 | 1015 | function coercify(inputs) { 1016 | if (inputs.length === 0) { 1017 | return [{inputs: [], coercions: []}]; 1018 | }; 1019 | inputs = inputs.slice(); 1020 | var last = inputs.pop(); 1021 | var others = coercify(inputs); 1022 | var results = []; 1023 | others.forEach(x => { 1024 | let {inputs, coercions} = x; 1025 | 1026 | results.push({ 1027 | inputs: inputs.concat([last]), 1028 | coercions: coercions.concat([null]), 1029 | }); 1030 | 1031 | var byInput = coercionsByType[last] || []; 1032 | byInput.forEach(c => { 1033 | let [input, coercion] = c; 1034 | results.push({ 1035 | inputs: inputs.concat([input]), 1036 | coercions: coercions.concat([coercion]), 1037 | }); 1038 | }); 1039 | }); 1040 | return results; 1041 | } 1042 | 1043 | Object.keys(functions).forEach(function(spec) { 1044 | var info = parseSpec(spec); 1045 | var byInputs = bySpec[info.spec] = bySpec[info.spec] || {}; 1046 | 1047 | coercify(info.inputs).forEach((c, index) => { 1048 | let {inputs, coercions} = c; 1049 | var hash = inputs.join(", "); 1050 | hash = /Variadic/.test(hash) ? "Variadic" : hash; 1051 | if (byInputs[hash] && index > 0) { 1052 | return; 1053 | } 1054 | byInputs[hash] = { 1055 | inputs: inputs, 1056 | output: info.output, 1057 | func: functions[spec], 1058 | coercions: coercions, 1059 | }; 1060 | }); 1061 | 1062 | }); 1063 | 1064 | export {bySpec}; 1065 | 1066 | export const typeOf = (value => { 1067 | if (value === undefined) return ''; 1068 | if (value === null) return ''; 1069 | switch (typeof value) { 1070 | case 'number': 1071 | if (/^-?[0-9]+$/.test(''+value)) return 'Int'; 1072 | return 'Float'; 1073 | case 'string': 1074 | if (value === '') return 'Empty'; 1075 | return 'Text'; 1076 | case 'boolean': 1077 | return 'Bool'; 1078 | case 'object': 1079 | if (value.isObservable) return 'Uneval'; // TODO 1080 | if (value.isTask) { // TODO 1081 | if (value.isDone) { 1082 | return typeOf(value.result); 1083 | } 1084 | return value.prim ? `${value.prim.output}` : 'Future'; 1085 | } 1086 | switch (value.constructor) { 1087 | case Error: return 'Error'; 1088 | case BigInteger: return 'Int'; 1089 | case Array: return 'List'; 1090 | case Image: return 'Image'; 1091 | case Uncertain: return 'Uncertain'; 1092 | case Record: return value.schema ? value.schema.name : 'Record'; 1093 | } 1094 | if (value instanceof Fraction) return 'Frac'; // TODO 1095 | if (value instanceof tinycolor) return 'Color'; // TODO 1096 | } 1097 | throw "Unknown type: " + value; 1098 | }); 1099 | 1100 | console.log(bySpec); 1101 | 1102 | 1103 | 1104 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | 2 | var isMac = /Mac/i.test(navigator.userAgent); 3 | 4 | RegExp.escape = function(s) { 5 | return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 6 | }; 7 | 8 | import tinycolor from "tinycolor2"; 9 | 10 | function assert(x) { 11 | if (!x) throw "Assertion failed!"; 12 | } 13 | 14 | function extend(src, dest) { 15 | src = src || {}; 16 | dest = dest || {}; 17 | for (var key in src) { 18 | if (src.hasOwnProperty(key) && !dest.hasOwnProperty(key)) { 19 | dest[key] = src[key]; 20 | } 21 | } 22 | return dest; 23 | } 24 | 25 | function clone(val) { 26 | if (val == null) return val; 27 | if (val.constructor == Array) { 28 | return val.map(clone); 29 | } else if (typeof val == "object") { 30 | var result = {} 31 | for (var key in val) { 32 | result[clone(key)] = clone(val[key]); 33 | } 34 | return result; 35 | } else { 36 | return val; 37 | } 38 | } 39 | 40 | function el(tagName, className) { 41 | var d = document.createElement(className ? tagName : 'div'); 42 | d.className = className || tagName || ''; 43 | return d; 44 | } 45 | 46 | /*****************************************************************************/ 47 | 48 | 49 | 50 | var PI12 = Math.PI * 1/2; 51 | var PI = Math.PI; 52 | var PI32 = Math.PI * 3/2; 53 | 54 | function containsPoint(extent, x, y) { 55 | return x >= 0 && y >= 0 && x < extent.width && y < extent.height; 56 | } 57 | 58 | function opaqueAt(context, x, y) { 59 | return containsPoint(context.canvas, x, y) && context.getImageData(x, y, 1, 1).data[3] > 0; 60 | } 61 | 62 | function bezel(context, path, thisArg, inset, scale) { 63 | if (scale == null) scale = 1; 64 | var s = inset ? -1 : 1; 65 | var w = context.canvas.width; 66 | var h = context.canvas.height; 67 | 68 | context.beginPath(); 69 | path.call(thisArg, context); 70 | context.fill(); 71 | // context.clip(); 72 | 73 | context.save(); 74 | context.translate(-10000, -10000); 75 | context.beginPath(); 76 | context.moveTo(-3, -3); 77 | context.lineTo(-3, h+3); 78 | context.lineTo(w+3, h+3); 79 | context.lineTo(w+3, -3); 80 | context.closePath(); 81 | path.call(thisArg, context); 82 | 83 | context.globalCompositeOperation = 'source-atop'; 84 | 85 | context.shadowOffsetX = (10000 + s * -1) * scale; 86 | context.shadowOffsetY = (10000 + s * -1) * scale; 87 | context.shadowBlur = 1.5 * scale; 88 | context.shadowColor = 'rgba(0, 0, 0, .7)'; 89 | context.fill(); 90 | 91 | context.shadowOffsetX = (10000 + s * 1) * scale; 92 | context.shadowOffsetY = (10000 + s * 1) * scale; 93 | context.shadowBlur = 1.5 * scale; 94 | context.shadowColor = 'rgba(255, 255, 255, .4)'; 95 | context.fill(); 96 | 97 | context.restore(); 98 | } 99 | 100 | /*****************************************************************************/ 101 | 102 | import {evaluator, Observable, Computed} from "./eval"; 103 | import {compile} from "./compile"; 104 | window.compile = compile; 105 | 106 | evaluator.sendMessage = onMessage; 107 | 108 | function sendMessage(json) { 109 | //console.log(`=> ${json.action}`, json); 110 | evaluator.onMessage(json); 111 | } 112 | 113 | function onMessage(json) { 114 | //console.log(`<= ${json.action}`, json); 115 | switch (json.action) { 116 | case 'emit': 117 | Node.byId[json.id].emit(json.value); 118 | return; 119 | case 'progress': 120 | Node.byId[json.id].progress(json.loaded, json.total); 121 | return; 122 | } 123 | } 124 | 125 | class Node { 126 | constructor(id, name, literal, isSink) { 127 | this.id = id || ++Node.highestId; 128 | this.name = name; 129 | this.literal = literal || null; 130 | this.isSink = isSink || false; 131 | this.inputs = []; 132 | this.outputs = []; 133 | 134 | //sendMessage({action: 'create', id: this.id, name: this.name, literal: this.literal, isSink: this.isSink}); 135 | Node.byId[this.id] = this; 136 | } 137 | 138 | destroy() { 139 | sendMessage({action: 'destroy', id: this.id}); 140 | delete Node.byId[this.id]; 141 | this.inputs.forEach(node => this.removeInput(this.inputs.indexOf(node))); 142 | this.outputs.forEach(node => node.removeInput(node.inputs.indexOf(this))); 143 | } 144 | 145 | static input(literal) { 146 | var name = "literal _"; 147 | var node = new Node(null, name, literal, false); 148 | sendMessage({action: 'create', id: node.id, name: name, literal: literal}); 149 | return node; 150 | } 151 | static block(name) { 152 | var node = new Node(null, name, null, false); 153 | sendMessage({action: 'create', id: node.id, name: name}); 154 | return node; 155 | } 156 | static repr(node) { 157 | var name = "display %s"; 158 | var repr = new Node(null, name, null, false); 159 | sendMessage({action: 'create', id: repr.id, name: name, isSink: false}); 160 | repr.addInput(0, node); 161 | return repr; 162 | } 163 | 164 | /* * */ 165 | 166 | _addOutput(node) { 167 | if (this.outputs.indexOf(node) !== -1) return; 168 | this.outputs.push(node); 169 | } 170 | 171 | _removeOutput(node) { 172 | var index = this.outputs.indexOf(node); 173 | if (index === -1) return; 174 | this.outputs.splice(index, 1); 175 | } 176 | 177 | addInput(index, node) { 178 | this.removeInput(index); 179 | this.inputs[index] = node; 180 | node._addOutput(this); 181 | sendMessage({action: 'link', from: node.id, index: index, to: this.id}); 182 | } 183 | 184 | removeInput(index) { 185 | var oldNode = this.inputs[index]; 186 | if (oldNode) { 187 | oldNode._removeOutput(this); 188 | sendMessage({action: 'unlink', from: oldNode.id, index: index, to: this.id}); 189 | } 190 | this.inputs[index] = null; 191 | } 192 | 193 | setLiteral(value) { 194 | if (this.literal === value) return; 195 | this.literal = value; 196 | sendMessage({action: 'setLiteral', id: this.id, literal: this.literal}); 197 | } 198 | 199 | setSink(isSink) { 200 | if (this.isSink === isSink) return; 201 | this.isSink = isSink; 202 | sendMessage({action: 'setSink', id: this.id, isSink: this.isSink}); 203 | } 204 | 205 | /* * */ 206 | 207 | emit(value) { 208 | this.value = value; 209 | this.dispatchEmit(value); 210 | } 211 | 212 | progress(loaded, total) { 213 | this.dispatchProgress({loaded, total}); 214 | } 215 | 216 | } 217 | Node.highestId = 0; 218 | Node.byId = {}; 219 | 220 | import {addEvents} from "./events"; 221 | addEvents(Node, 'emit', 'progress'); 222 | 223 | /*****************************************************************************/ 224 | 225 | var density = 2; 226 | 227 | var metricsContainer = el('metrics-container'); 228 | document.body.appendChild(metricsContainer); 229 | 230 | function createMetrics(className) { 231 | var field = el('metrics ' + className); 232 | var node = document.createTextNode(''); 233 | field.appendChild(node); 234 | metricsContainer.appendChild(field); 235 | 236 | var stringCache = Object.create(null); 237 | 238 | return function measure(text) { 239 | if (hasOwnProperty.call(stringCache, text)) { 240 | return stringCache[text]; 241 | } 242 | node.data = text + '\u200B'; 243 | return stringCache[text] = { 244 | width: field.offsetWidth, 245 | height: field.offsetHeight 246 | }; 247 | }; 248 | } 249 | 250 | class Drawable { 251 | constructor() { 252 | this.x = 0; 253 | this.y = 0; 254 | this.width = null; 255 | this.height = null; 256 | this.el = null; 257 | 258 | this.parent = null; 259 | this.dirty = true; 260 | this.graphicsDirty = true; 261 | this.lastTap = 0; 262 | 263 | this._zoom = 1; 264 | } 265 | 266 | moveTo(x, y) { 267 | this.x = x | 0; 268 | this.y = y | 0; 269 | this.transform(); 270 | } 271 | 272 | set zoom(value) { 273 | this._zoom = value; 274 | this.transform(); 275 | } 276 | 277 | transform() { 278 | var t = ''; 279 | t += `translate(${this.x + (this._flip ? 1 : 0)}px, ${this.y}px)`; 280 | if (this._zoom !== 1) t += ` scale(${this._zoom})`; 281 | t += ' translateZ(0)'; 282 | this.el.style.transform = t; 283 | } 284 | 285 | destroy() {} 286 | 287 | moved() {} 288 | 289 | layout() { 290 | if (!this.parent) return; 291 | 292 | this.layoutSelf(); 293 | this.parent.layout(); 294 | } 295 | 296 | layoutChildren() { // assume no children 297 | if (this.dirty) { 298 | this.dirty = false; 299 | this.layoutSelf(); 300 | } 301 | } 302 | 303 | drawChildren() { // assume no children 304 | if (this.graphicsDirty) { 305 | this.graphicsDirty = false; 306 | this.draw(); 307 | } 308 | } 309 | 310 | redraw() { 311 | if (this.workspace) { 312 | this.graphicsDirty = false; 313 | this.draw(); 314 | 315 | // for debugging 316 | this.el.style.width = this.width + 'px'; 317 | this.el.style.height = this.height + 'px'; 318 | } else { 319 | this.graphicsDirty = true; 320 | } 321 | } 322 | 323 | // layoutSelf() {} 324 | // draw() {} 325 | 326 | get app() { 327 | var o = this; 328 | while (o && !o.isApp) { 329 | o = o.parent; 330 | } 331 | return o; 332 | } 333 | 334 | get workspace() { 335 | var o = this; 336 | while (o && !o.isWorkspace) { 337 | o = o.parent; 338 | } 339 | return o; 340 | } 341 | 342 | get workspacePosition() { 343 | var o = this; 344 | var x = 0; 345 | var y = 0; 346 | while (o && !o.isWorkspace) { 347 | x += o.x; 348 | y += o.y; 349 | o = o.parent; 350 | } 351 | return {x, y}; 352 | } 353 | 354 | get screenPosition() { 355 | var o = this; 356 | var x = 0; 357 | var y = 0; 358 | while (o && !o.isWorkspace && !o.isApp) { 359 | x += o.x; 360 | y += o.y; 361 | o = o.parent; 362 | } 363 | if (o && !o.isApp) { 364 | return o.screenPositionOf(x, y); 365 | } 366 | return {x, y}; 367 | } 368 | 369 | get topScript() { 370 | var o = this; 371 | while (o.parent) { 372 | if (o.parent.isWorkspace) return o; 373 | o = o.parent; 374 | } 375 | return null; 376 | } 377 | 378 | click() { 379 | this.lastTap = +new Date(); 380 | } 381 | isDoubleTap() { 382 | return +new Date() - this.lastTap < 400; 383 | } 384 | 385 | setHover(hover) {} 386 | setDragging(dragging) {} 387 | 388 | } 389 | 390 | 391 | class Frame { 392 | constructor() { 393 | this.el = el('frame'); 394 | this.elContents = el('absolute frame-contents'); 395 | this.el.appendChild(this.elContents); 396 | 397 | this.parent = null; 398 | this.scrollX = 0; 399 | this.scrollY = 0; 400 | this.zoom = 1; 401 | this.lastX = 0; 402 | this.lastY = 0; 403 | this.inertiaX = 0; 404 | this.inertiaY = 0; 405 | this.scrolling = false; 406 | setInterval(this.tick.bind(this), 1000 / 60); 407 | 408 | this.contentsLeft = 0; 409 | this.contentsTop = 0; 410 | this.contentsRight = 0; 411 | this.contentsBottom = 0; 412 | } 413 | 414 | get isScrollable() { return true; } 415 | get isZoomable() { return false; } 416 | 417 | toScreen(x, y) { 418 | return { 419 | x: (x - this.scrollX) * this.zoom, 420 | y: (y - this.scrollY) * this.zoom, 421 | }; 422 | }; 423 | 424 | fromScreen(x, y) { 425 | return { 426 | x: (x / this.zoom) + this.scrollX, 427 | y: (y / this.zoom) + this.scrollY, 428 | }; 429 | }; 430 | 431 | resize() { 432 | this.width = this.el.offsetWidth; 433 | this.height = this.el.offsetHeight; 434 | // TODO re-center 435 | this.makeBounds(); 436 | this.transform(); 437 | } 438 | 439 | scrollBy(dx, dy) { 440 | this.scrollX += dx / this.zoom; 441 | this.scrollY += dy / this.zoom; 442 | this.makeBounds(); 443 | this.transform(); 444 | } 445 | 446 | fingerScroll(dx, dy) { 447 | if (!this.scrolling) { 448 | this.inertiaX = 0; 449 | this.inertiaY = 0; 450 | } 451 | this.scrollBy(-dx, -dy); 452 | this.scrolling = true; 453 | } 454 | 455 | fingerScrollEnd() { 456 | this.scrolling = false; 457 | } 458 | 459 | tick() { 460 | if (this.scrolling) { 461 | this.inertiaX = (this.inertiaX * 4 + (this.scrollX - this.lastX)) / 5; 462 | this.inertiaY = (this.inertiaY * 4 + (this.scrollY - this.lastY)) / 5; 463 | this.lastX = this.scrollX; 464 | this.lastY = this.scrollY; 465 | } else { 466 | if (this.inertiaX !== 0 || this.inertiaY !== 0) { 467 | this.scrollBy(this.inertiaX, this.inertiaY); 468 | this.inertiaX *= 0.95; 469 | this.inertiaY *= 0.95; 470 | if (Math.abs(this.inertiaX) < 0.01) this.inertiaX = 0; 471 | if (Math.abs(this.inertiaY) < 0.01) this.inertiaY = 0; 472 | } 473 | } 474 | } 475 | 476 | fixZoom(zoom) { 477 | return zoom; 478 | } 479 | 480 | zoomBy(factor, x, y) { 481 | if (!this.isZoomable) return; 482 | var oldCursor = this.fromScreen(x, y); 483 | this.zoom *= factor; 484 | this.zoom = this.fixZoom(this.zoom * factor); 485 | this.makeBounds(); 486 | var newCursor = this.fromScreen(x, y); 487 | this.scrollX += oldCursor.x - newCursor.x; 488 | this.scrollY += oldCursor.y - newCursor.y; 489 | this.makeBounds(); 490 | this.transform(); 491 | } 492 | 493 | // TODO pinch zoom 494 | 495 | canScroll(dx, dy) { 496 | if (this.isInfinite) return true; 497 | var sx = this.scrollX + dx; 498 | var sy = this.scrollY + dy; 499 | return this.contentsLeft <= sx && sx <= this.contentsRight * this.zoom - this.width && this.contentsTop <= sy && sy <= this.contentsBottom * this.zoom - this.height; 500 | // TODO check zoom calculations 501 | } 502 | 503 | makeBounds() { 504 | if (!this.isInfinite) { 505 | this.scrollX = Math.min( 506 | Math.max(this.scrollX, this.contentsLeft), 507 | Math.max(0, this.contentsRight * this.zoom - this.width) 508 | ); 509 | this.scrollY = Math.min( 510 | Math.max(this.scrollY, this.contentsTop), 511 | Math.max(0, this.contentsBottom * this.zoom - this.height) 512 | ); 513 | assert(!isNaN(this.scrollY)); 514 | } 515 | 516 | this.bounds = { 517 | left: this.scrollX - (this.width / 2) / this.zoom + 0.5| 0, 518 | right: this.scrollX + (this.width / 2) / this.zoom + 0.5| 0, 519 | bottom: this.scrollY - (this.height / 2) / this.zoom + 0.5 | 0, 520 | top: this.scrollY + (this.height / 2) / this.zoom + 0.5 | 0, 521 | }; 522 | } 523 | 524 | transform() { 525 | this.elContents.style.transform = `scale(${this.zoom}) translate(${-this.scrollX}px, ${-this.scrollY}px)`; 526 | } 527 | 528 | } 529 | 530 | 531 | /*****************************************************************************/ 532 | 533 | 534 | class Label extends Drawable { 535 | constructor(text) { 536 | assert(typeof text === 'string'); 537 | super(); 538 | this.el = el('absolute label'); 539 | this.text = text; 540 | } 541 | 542 | get text() { return this._text; } 543 | set text(value) { 544 | this._text = value; 545 | this.el.textContent = value; 546 | var metrics = Label.measure(value); 547 | this.width = metrics.width; 548 | this.height = metrics.height * 1.2 | 0; 549 | this.layout(); 550 | } 551 | 552 | objectFromPoint(x, y) { return null; } 553 | 554 | copy() { 555 | return new Label(this.text); 556 | } 557 | 558 | layoutSelf() {} 559 | draw() {} 560 | 561 | get dragObject() { 562 | return this.parent.dragObject; 563 | } 564 | } 565 | Label.prototype.isLabel = true; 566 | Label.measure = createMetrics('label'); 567 | 568 | 569 | class Input extends Drawable { 570 | constructor(value, shape) { 571 | super(); 572 | 573 | this.el = el('absolute'); 574 | this.el.appendChild(this.canvas = el('canvas', 'absolute')); 575 | this.context = this.canvas.getContext('2d'); 576 | 577 | this.el.appendChild(this.field = el('input', 'absolute field text-field')); 578 | 579 | this.shape = shape; 580 | 581 | this.field.addEventListener('input', this.change.bind(this)); 582 | this.field.addEventListener('keydown', this.keyDown.bind(this)); 583 | 584 | this.node = Node.input(value); 585 | this.value = value; 586 | } 587 | 588 | get isInput() { return true; } 589 | get isArg() { return true; } 590 | 591 | get isDraggable() { 592 | return this.workspace && this.workspace.isPalette; 593 | } 594 | get dragObject() { 595 | return this.parent.dragObject; 596 | } 597 | 598 | get value() { return this._value; } 599 | set value(value) { 600 | value = ''+value; 601 | this._value = value; 602 | if (this.field.value !== value) { 603 | this.field.value = value; 604 | } 605 | if (this.shape === 'Color') { 606 | value = tinycolor(value); 607 | } else { 608 | value = literal(value); 609 | } 610 | this.node.setLiteral(value); 611 | this.layout(); 612 | } 613 | 614 | get shape() { return this._shape; } 615 | set shape(value) { 616 | this._shape = value; 617 | this.color = '#fff'; 618 | this.pathIcon = null; 619 | switch (value) { 620 | case 'Num': 621 | this.pathFn = this.pathCircle; 622 | break; 623 | case 'Menu': 624 | case 'Color': 625 | this.pathFn = this.pathSquare; 626 | break; 627 | case 'Symbol': 628 | this.pathFn = this.pathTag; 629 | break; 630 | case 'List': 631 | this.pathFn = this.pathObj; 632 | this.pathIcon = this.pathListIcon; 633 | break; 634 | case 'Record': 635 | this.pathFn = this.pathObj; 636 | this.pathIcon = this.pathRecordIcon; 637 | break; 638 | default: 639 | this.pathFn = this.pathRounded; 640 | } 641 | } 642 | 643 | change(e) { 644 | this.value = this.field.value; 645 | this.layout(); 646 | assert(this.parent); 647 | }; 648 | keyDown(e) { 649 | // TODO up-down to increment number 650 | } 651 | 652 | copy() { 653 | return new Input(this._value, this.shape); 654 | } 655 | 656 | replaceWith(other) { 657 | this.parent.replace(this, other); 658 | } 659 | 660 | click() { 661 | super.click(); 662 | if (this.shape === 'Color') { 663 | this.field.focus(); 664 | return; 665 | } 666 | this.field.select(); 667 | this.field.setSelectionRange(0, this.field.value.length); 668 | } 669 | 670 | objectFromPoint(x, y) { 671 | return opaqueAt(this.context, x * density, y * density) ? this : null; 672 | } 673 | 674 | draw() { 675 | this.canvas.width = this.width * density; 676 | this.canvas.height = this.height * density; 677 | this.canvas.style.width = this.width + 'px'; 678 | this.canvas.style.height = this.height + 'px'; 679 | this.context.scale(density, density); 680 | this.drawOn(this.context); 681 | } 682 | 683 | drawOn(context) { 684 | context.fillStyle = this.color; 685 | bezel(context, this.pathFn, this, true, density); 686 | 687 | if (this.pathIcon) { 688 | context.save(); 689 | context.fillStyle = 'rgba(255, 255, 255, 0.5)'; 690 | this.pathIcon(context); 691 | context.closePath(); 692 | context.fill(); 693 | context.restore(); 694 | } 695 | } 696 | 697 | pathRounded(context, r) { 698 | var w = this.width; 699 | var h = this.height; 700 | var r = r !== undefined ? r : 4; 701 | context.moveTo(0, r + .5); 702 | context.arc(r, r + .5, r, PI, PI32, false); 703 | context.arc(w - r, r + .5, r, PI32, 0, false); 704 | context.arc(w - r, h - r - .5, r, 0, PI12, false); 705 | context.arc(r, h - r - .5, r, PI12, PI, false); 706 | } 707 | 708 | pathCircle(context) { 709 | this.pathRounded(context, this.height / 2); 710 | } 711 | 712 | pathObj(context) { 713 | this.pathRounded(context, density * 2); 714 | }; 715 | 716 | pathSquare(context) { 717 | this.pathRounded(context, density); 718 | } 719 | 720 | pathTag(context) { 721 | var w = this.width; 722 | var h = this.height; 723 | var r = h / 2; 724 | context.moveTo(0, 0); 725 | context.lineTo(w - r, 0); 726 | context.lineTo(w, r); 727 | context.lineTo(w - r, h); 728 | context.lineTo(0, h); 729 | } 730 | 731 | pathRecordIcon(context) { 732 | var s = 22 / 16; 733 | context.translate(0, 1); 734 | context.scale(s, s); 735 | 736 | for (var y=0; y<=7; y+=7) { 737 | context.moveTo(1, y + 3); 738 | context.lineTo(4, y + 3); 739 | context.lineTo(4, y + 1.5); 740 | context.lineTo(7, y + 4); 741 | context.lineTo(4, y + 6.5); 742 | context.lineTo(4, y + 5); 743 | context.lineTo(1, y + 5); 744 | 745 | context.moveTo(9, y + 2); 746 | context.lineTo(12.5, y + 2); 747 | context.lineTo(15, y + 2); 748 | context.lineTo(15, y + 6); 749 | context.lineTo(9, y + 6); 750 | } 751 | } 752 | 753 | pathListIcon(context) { 754 | var s = 22 / 16; 755 | context.scale(s, s); 756 | 757 | for (var y=4; y<=12; y += 4) { 758 | context.moveTo(2, 3); 759 | context.arc(3.5, y, 1.5, 0, 2 * Math.PI); 760 | 761 | context.rect(6.5, y - 1, 8, 2); 762 | } 763 | } 764 | 765 | layoutSelf() { 766 | var isColor = false; 767 | if (this.shape === 'Menu' || this.shape === 'Record' || this.shape === 'List') { 768 | var can = document.createElement('canvas'); 769 | var c = can.getContext('2d'); 770 | c.fillStyle = this.parent.color; 771 | c.fillRect(0, 0, 1, 1); 772 | c.fillStyle = 'rgba(0,0,0, .15)'; 773 | c.fillRect(0, 0, 1, 1); 774 | var d = c.getImageData(0, 0, 1, 1).data; 775 | var s = (d[0] * 0x10000 + d[1] * 0x100 + d[2]).toString(16); 776 | this.color = '#' + '000000'.slice(s.length) + s; 777 | } else if (this.shape === 'Color') { 778 | this.color = this.value; 779 | isColor = true; 780 | } else { 781 | this.color = '#f7f7f7'; 782 | } 783 | 784 | var metrics = isColor ? { width: 12, height: 20 } 785 | : Input.measure(this.field.value); 786 | this.height = metrics.height + 3; 787 | 788 | var pl = 0; 789 | var pr = 0; 790 | if (this.pathFn === this.pathTag) { 791 | pr = this.height / 2 - 4; 792 | } 793 | 794 | var w = Math.max(this.height, Math.max(this.minWidth, metrics.width) + this.fieldPadding * 2); 795 | this.width = w + pl + pr; 796 | 797 | this.field.className = 'absolute field text-field field-' + this.shape; 798 | this.field.type = isColor ? 'color' : ''; 799 | if (isColor) { 800 | this.field.style.width = `${this.width}px`; 801 | this.field.style.height = `${this.height}px`; 802 | this.field.style.left = '0'; 803 | } else { 804 | this.field.style.width = `${w}px`; 805 | this.field.style.height = `${this.height}px`; 806 | this.field.style.left = `${pl}px`; 807 | } 808 | 809 | this.redraw(); 810 | } 811 | 812 | pathShadowOn(context) { 813 | this.pathFn(context); 814 | context.closePath(); 815 | } 816 | 817 | } 818 | Input.measure = createMetrics('field'); 819 | 820 | Input.prototype.minWidth = 8; 821 | Input.prototype.fieldPadding = 4; 822 | 823 | 824 | 825 | class Break extends Drawable { 826 | constructor() { 827 | super(); 828 | this.el = el('br', ''); 829 | } 830 | 831 | get isBreak() { return true; } 832 | 833 | copy() { 834 | return new Break(); 835 | } 836 | 837 | layoutSelf() {} 838 | draw() {} 839 | objectFromPoint() {} 840 | 841 | } 842 | 843 | 844 | 845 | class Switch extends Drawable { 846 | constructor(value) { 847 | super(); 848 | 849 | this.el = el('absolute switch'); 850 | this.el.appendChild(this.canvas = el('canvas', 'absolute')); 851 | this.context = this.canvas.getContext('2d'); 852 | 853 | this.knob = new SwitchKnob(this); 854 | this.el.appendChild(this.knob.el); 855 | 856 | this.node = Node.input(value); 857 | this.value = value; 858 | } 859 | 860 | get isSwitch() { return true; } 861 | get isArg() { return true; } 862 | 863 | copy() { 864 | return new Switch(this.value); 865 | } 866 | 867 | replaceWith(other) { 868 | this.parent.replace(this, other); 869 | } 870 | 871 | get isDraggable() { 872 | return true; 873 | } 874 | get dragObject() { 875 | return this.parent.dragObject; 876 | } 877 | 878 | objectFromPoint(x, y) { 879 | return (this.knob.objectFromPoint(x - this.knob.x, y - this.knob.y) || opaqueAt(this.context, x * density, y * density) ? this : null); 880 | } 881 | 882 | get value() { return this._value; } 883 | set value(value) { 884 | if (this._value === value) return; 885 | this._value = value; 886 | this.node.setLiteral(value); 887 | this.knob.moveTo(this._value ? 32 - 20 + 3 : -3, -2); 888 | this.color = this._value ? '#64c864' : '#b46464'; 889 | this.redraw(); 890 | } 891 | 892 | click() { 893 | super.click(); 894 | this.value = !this.value; 895 | } 896 | 897 | layoutSelf() { 898 | this.width = 32; 899 | this.height = 14; 900 | this.redraw(); 901 | } 902 | 903 | layoutChildren() { 904 | this.knob.layout(); 905 | this.layoutSelf(); 906 | } 907 | 908 | draw() { 909 | this.canvas.width = this.width * density; 910 | this.canvas.height = this.height * density; 911 | this.canvas.style.width = this.width + 'px'; 912 | this.canvas.style.height = this.height + 'px'; 913 | this.context.scale(density, density); 914 | this.drawOn(this.context); 915 | } 916 | 917 | drawOn(context) { 918 | context.fillStyle = this.color; 919 | bezel(context, this.pathFn, this, true, density); 920 | } 921 | 922 | pathFn(context) { 923 | var w = this.width; 924 | var h = this.height; 925 | var r = 8; 926 | 927 | context.moveTo(0, r + .5); 928 | context.arc(r, r + .5, r, PI, PI32, false); 929 | context.arc(w - r, r + .5, r, PI32, 0, false); 930 | context.arc(w - r, h - r - .5, r, 0, PI12, false); 931 | context.arc(r, h - r - .5, r, PI12, PI, false); 932 | } 933 | 934 | pathShadowOn(context) { 935 | this.pathFn(context); 936 | context.closePath(); 937 | } 938 | 939 | } 940 | 941 | class SwitchKnob extends Drawable { 942 | constructor(parent) { 943 | super(); 944 | this.parent = parent; 945 | 946 | this.el = el('absolute switch-knob'); 947 | this.el.appendChild(this.canvas = el('canvas', 'absolute')); 948 | this.context = this.canvas.getContext('2d'); 949 | 950 | this.color = '#bbc'; 951 | this.layoutSelf(); 952 | } 953 | 954 | objectFromPoint(x, y) { 955 | return opaqueAt(this.context, x * density, y * density) ? this : null; 956 | } 957 | 958 | get isDraggable() { 959 | return true; 960 | } 961 | get dragObject() { 962 | return this.parent.dragObject; 963 | } 964 | 965 | click() { 966 | super.click(); 967 | this.parent.click(); 968 | } 969 | 970 | layoutSelf() { 971 | this.width = 20; 972 | this.height = 20; 973 | this.redraw(); 974 | } 975 | 976 | draw() { 977 | this.canvas.width = this.width * density; 978 | this.canvas.height = this.height * density; 979 | this.canvas.style.width = this.width + 'px'; 980 | this.canvas.style.height = this.height + 'px'; 981 | this.context.scale(density, density); 982 | this.drawOn(this.context); 983 | } 984 | 985 | drawOn(context) { 986 | context.fillStyle = this.color; 987 | bezel(context, this.pathFn, this, false, density); 988 | } 989 | 990 | pathFn(context) { 991 | var w = this.width; 992 | var h = this.height; 993 | var r = 10; 994 | 995 | context.moveTo(0, r + .5); 996 | context.arc(r, r + .5, r, PI, PI32, false); 997 | context.arc(w - r, r + .5, r, PI32, 0, false); 998 | context.arc(w - r, h - r - .5, r, 0, PI12, false); 999 | context.arc(r, h - r - .5, r, PI12, PI, false); 1000 | } 1001 | } 1002 | 1003 | 1004 | 1005 | class Arrow extends Drawable { 1006 | constructor(icon, action) { 1007 | super(); 1008 | this.icon = icon; 1009 | this.pathFn = icon === '▶' ? this.pathAddInput 1010 | : icon === '◀' ? this.pathDelInput : assert(false); 1011 | this.action = action; 1012 | 1013 | this.el = el('absolute'); 1014 | this.el.appendChild(this.canvas = el('canvas', 'absolute')); 1015 | this.context = this.canvas.getContext('2d'); 1016 | this.setHover(false); 1017 | 1018 | this.layoutSelf(); 1019 | } 1020 | 1021 | get isArrow() { return true; } 1022 | 1023 | objectFromPoint(x, y) { 1024 | var px = 4; 1025 | var py = 4; 1026 | var touchExtent = {width: this.width + px * 3, height: this.height + py * 3}; 1027 | if (containsPoint(touchExtent, x + px, y + py)) { 1028 | return this; 1029 | } 1030 | } 1031 | 1032 | setHover(hover) { 1033 | this.color = hover ? '#5B57C5' : '#333'; 1034 | } 1035 | 1036 | get color() { return this._color; } 1037 | set color(value) { 1038 | this._color = value; 1039 | this.redraw(); 1040 | } 1041 | 1042 | get isDraggable() { 1043 | return true; 1044 | } 1045 | get dragObject() { 1046 | return this.parent.dragObject; 1047 | } 1048 | copy() { 1049 | return new Arrow(this.icon, this.action); 1050 | } 1051 | 1052 | click() { 1053 | super.click(); 1054 | this.action.call(this.parent); 1055 | } 1056 | 1057 | layoutSelf() { 1058 | this.width = 14; 1059 | this.height = 14; 1060 | this.redraw(); 1061 | } 1062 | 1063 | draw() { 1064 | this.canvas.width = this.width * density; 1065 | this.canvas.height = this.height * density; 1066 | this.canvas.style.width = this.width + 'px'; 1067 | this.canvas.style.height = this.height + 'px'; 1068 | this.context.scale(density, density); 1069 | this.drawOn(this.context); 1070 | } 1071 | 1072 | drawOn(context) { 1073 | context.strokeStyle = this.color; 1074 | context.lineWidth = 0.5 * density; 1075 | this.pathFn(context); 1076 | context.closePath(); 1077 | context.stroke(); 1078 | } 1079 | 1080 | pathCircle(context) { 1081 | var w = this.width; 1082 | var h = this.height; 1083 | var r = h / 2; 1084 | context.moveTo(0, r + 1); 1085 | context.arc(r, r + 1, r, PI, PI32, false); 1086 | context.arc(w - r, r + 1, r, PI32, 0, false); 1087 | context.arc(w - r, h - r - 1, r, 0, PI12, false); 1088 | context.arc(r, h - r - 1, r, PI12, PI, false); 1089 | } 1090 | 1091 | pathDelInput(context) { 1092 | var w = this.width; 1093 | var h = this.height; 1094 | var t = 1.5 * density; 1095 | this.pathCircle(context); 1096 | context.moveTo(t, h / 2); 1097 | context.lineTo(w - t, h / 2); 1098 | } 1099 | 1100 | pathAddInput(context) { 1101 | var w = this.width; 1102 | var h = this.height; 1103 | var t = 1.5 * density; 1104 | this.pathCircle(context); 1105 | context.moveTo(t, h / 2); 1106 | context.lineTo(w - t, h / 2); 1107 | context.moveTo(w / 2, t); 1108 | context.lineTo(w / 2, h - t); 1109 | } 1110 | 1111 | } 1112 | 1113 | 1114 | class Block extends Drawable { 1115 | constructor(info, parts) { 1116 | super(); 1117 | 1118 | this.el = el('absolute'); 1119 | this.el.appendChild(this.canvas = el('canvas', 'absolute')); 1120 | this.context = this.canvas.getContext('2d'); 1121 | 1122 | this.parts = []; 1123 | this.labels = []; 1124 | this.args = []; 1125 | 1126 | this.node = Node.block(info.spec); 1127 | this.repr = Node.repr(this.node); 1128 | 1129 | this.info = info; 1130 | for (var i=0; i part.isArg); 1134 | 1135 | this.color = info.color; //'#7a48c3'; 1136 | 1137 | this.outputs = []; 1138 | this.curves = []; 1139 | this.blob = new Blob(this); 1140 | this.bubble = new Bubble(this); 1141 | this.el.appendChild(this.bubble.el); 1142 | this.addOutput(this.bubble); 1143 | this.bubble.parent = this; 1144 | 1145 | this.wrap = false; 1146 | } 1147 | 1148 | get isBlock() { return true; } 1149 | get isArg() { return true; } 1150 | get isDraggable() { return true; } 1151 | 1152 | get parent() { return this._parent; } 1153 | set parent(value) { 1154 | this._parent = value; 1155 | if (!this.outputs) return; 1156 | this.updateSinky(); 1157 | } 1158 | 1159 | get color() { return this._color } 1160 | set color(value) { 1161 | this._color = value; 1162 | this.redraw(); 1163 | } 1164 | 1165 | add(part) { 1166 | this.insert(part, this.parts.length); 1167 | } 1168 | 1169 | insert(part, index) { 1170 | assert(part !== this); 1171 | if (part.parent) part.parent.remove(part); 1172 | part.parent = this; 1173 | part.zoom = 1; 1174 | this.parts.splice(index, 0, part); 1175 | if (this.parent) part.layoutChildren(); // TODO 1176 | this.layout(); 1177 | this.el.appendChild(part.el); 1178 | 1179 | var array = part.isArg ? this.args : this.labels; 1180 | array.push(part); // TODO 1181 | 1182 | if (part.isArg) { 1183 | var index = array.length - 1; 1184 | this.node.addInput(index, part.node); 1185 | } 1186 | } 1187 | 1188 | replace(oldPart, newPart) { 1189 | assert(newPart !== this); 1190 | if (oldPart.parent !== this) return; 1191 | if (newPart.parent) newPart.parent.remove(newPart); 1192 | oldPart.parent = null; 1193 | newPart.parent = this; 1194 | newPart.zoom = 1; 1195 | 1196 | var index = this.parts.indexOf(oldPart); 1197 | this.parts.splice(index, 1, newPart); 1198 | 1199 | var array = oldPart.isArg ? this.args : this.labels; 1200 | var index = array.indexOf(oldPart); 1201 | array.splice(index, 1, newPart); 1202 | 1203 | newPart.layoutChildren(); 1204 | newPart.redraw(); 1205 | this.layout(); 1206 | if (this.workspace) newPart.drawChildren(); 1207 | 1208 | this.el.replaceChild(newPart.el, oldPart.el); 1209 | 1210 | this.node.addInput(index, newPart.node); 1211 | }; 1212 | 1213 | remove(part) { 1214 | if (part.parent !== this) return; 1215 | if (part.isBubble) { 1216 | if (this.bubble === part) { 1217 | this.removeBubble(part); 1218 | } 1219 | return; 1220 | } 1221 | 1222 | part.parent = null; 1223 | var index = this.parts.indexOf(part); 1224 | this.parts.splice(index, 1); 1225 | this.layout(); 1226 | this.el.removeChild(part.el); 1227 | 1228 | var array = part.isArg ? this.args : this.labels; 1229 | var index = array.indexOf(part); 1230 | array.splice(index, 1); 1231 | 1232 | this.node.removeInput(index); 1233 | // TODO shift up others?? 1234 | } 1235 | 1236 | addOutput(output) { 1237 | this.outputs.push(output); 1238 | output.target = this; 1239 | 1240 | var curve = new Curve(this, output); 1241 | output.curve = curve; 1242 | this.curves.push(curve); 1243 | 1244 | this.layoutBubble(output); 1245 | } 1246 | 1247 | removeOutput(output) { 1248 | var index = this.outputs.indexOf(output); 1249 | this.curves[index].destroy(); 1250 | this.outputs.splice(index, 1); 1251 | this.curves.splice(index, 1); 1252 | output.parent = null; 1253 | if (index === 0 && this.outputs.length === 0) { 1254 | // TODO if there's no bubble, make one 1255 | // this.bubble = new Bubble(this); 1256 | // // this.el.appendChild(this.bubble.el); 1257 | // this.addOutput(this.bubble); 1258 | // this.bubble.parent = null; 1259 | // this.addBubble(bubble); 1260 | } 1261 | } 1262 | 1263 | addBubble(bubble) { 1264 | assert(bubble); 1265 | assert(this.bubble === this.blob); 1266 | if (this.outputs.length > 1) { 1267 | // destroy bubble 1268 | bubble.curve.parent.remove(bubble.curve); 1269 | bubble.parent.remove(bubble); 1270 | this.removeOutput(bubble); 1271 | this.updateSinky(); 1272 | return; 1273 | } 1274 | bubble.zoom = 1; 1275 | this.el.removeChild(this.blob.el); 1276 | this.bubble = bubble; 1277 | bubble.parent = this; 1278 | this.el.appendChild(bubble.el); 1279 | this.layoutBubble(bubble); 1280 | 1281 | this.updateSinky(); 1282 | } 1283 | 1284 | removeBubble(bubble) { 1285 | assert(this.bubble === bubble); 1286 | this.bubble = this.blob; 1287 | this.el.appendChild(this.blob.el); 1288 | this.blob.layoutSelf(); 1289 | this.layoutBubble(this.bubble); 1290 | this.el.removeChild(bubble.el); 1291 | 1292 | this.updateSinky(); 1293 | } 1294 | 1295 | reset(arg) { 1296 | if (arg.parent !== this || arg.isLabel) return this; 1297 | 1298 | assert(arg.isArg); 1299 | var i = this.args.indexOf(arg); 1300 | this.replace(arg, this.inputs[i]); 1301 | }; 1302 | 1303 | detach() { 1304 | if (this.isDoubleTap()) { 1305 | return this.copy(); 1306 | } 1307 | if (this.workspace.isPalette) { 1308 | var block = this.copy(); 1309 | block.repr.setSink(true); 1310 | return block; 1311 | } 1312 | if (this.parent.isBlock) { 1313 | this.parent.reset(this); 1314 | } 1315 | return this; 1316 | } 1317 | 1318 | copy() { 1319 | var b = new Block(this.info, this.parts.map(c => c.copy())); 1320 | b.inputs = this.inputs.map(part => { 1321 | var index = this.parts.indexOf(part); 1322 | if (index === -1) { 1323 | return part.copy(); 1324 | } 1325 | return b.parts[index]; 1326 | }); 1327 | b.count = this.count; 1328 | b.wrap = this.wrap; 1329 | return b; 1330 | } 1331 | 1332 | destroy() { 1333 | this.parts.forEach(part => part.destroy()); 1334 | } 1335 | 1336 | replaceWith(other) { 1337 | this.parent.replace(this, other); 1338 | } 1339 | 1340 | moveTo(x, y) { 1341 | super.moveTo(x, y); 1342 | this.moved(); 1343 | } 1344 | 1345 | moved() { 1346 | this.parts.forEach(p => p.moved()); 1347 | this.curves.forEach(c => c.layoutSelf()); 1348 | } 1349 | 1350 | get bubbleVisible() { 1351 | return !this.parent.isBlock && !(this.workspace && this.workspace.isPalette); 1352 | } 1353 | 1354 | objectFromPoint(x, y) { 1355 | if (this.bubble && this.bubbleVisible) { 1356 | var o = this.bubble.objectFromPoint(x - this.bubble.x, y - this.bubble.y) 1357 | if (o) return o; 1358 | } 1359 | for (var i = this.parts.length; i--;) { 1360 | var arg = this.parts[i]; 1361 | var o = arg.objectFromPoint(x - arg.x, y - arg.y); 1362 | if (o) return o; 1363 | } 1364 | return opaqueAt(this.context, x * density, y * density) ? this : null; 1365 | } 1366 | 1367 | get dragObject() { 1368 | return this; 1369 | } 1370 | 1371 | layoutChildren() { 1372 | this.parts.forEach(c => c.layoutChildren()); 1373 | this.bubble.layoutChildren(); 1374 | if (this.dirty) { 1375 | this.dirty = false; 1376 | this.layoutSelf(); 1377 | } 1378 | } 1379 | 1380 | drawChildren() { 1381 | this.parts.forEach(c => c.drawChildren()); 1382 | this.outputs.forEach(o => o.drawChildren()); // TODO ew 1383 | if (this.graphicsDirty) { 1384 | this.graphicsDirty = false; 1385 | this.draw(); 1386 | } 1387 | } 1388 | 1389 | minDistance(part) { 1390 | if (part.isSwitch) { 1391 | return 16; 1392 | } 1393 | if (part.shape === 'Color') { 1394 | return 10; 1395 | } 1396 | if (part.isBubble || part.isSource) { 1397 | return 0; 1398 | } 1399 | if (part.isBlock) { 1400 | return 6; 1401 | } 1402 | if (part.shape === 'Symbol') { 1403 | return 9; 1404 | } 1405 | return -2 + part.height/2 | 0; 1406 | } 1407 | 1408 | layoutSelf() { 1409 | var px = 4; 1410 | 1411 | var lineX = 0; 1412 | var width = 0; 1413 | var height = 28; 1414 | 1415 | var lines = [[]]; 1416 | var lineXs = [[0]]; 1417 | var lineHeights = [28]; 1418 | var line = 0; 1419 | 1420 | var parts = this.parts; 1421 | var length = parts.length; 1422 | var wrap = this.wrap; 1423 | var canWrap = false; 1424 | for (var i=0; i 512) { 1471 | this.wrap = true; 1472 | this.layoutSelf(); 1473 | return; 1474 | } 1475 | 1476 | var y = 0; 1477 | for (var i=0; i c.layoutSelf()); 1502 | this.redraw(); 1503 | } 1504 | 1505 | layoutBubble(bubble) { 1506 | if (!bubble) return; 1507 | var x = (this.width - bubble.width) / 2; 1508 | var y = this.height - 1; 1509 | bubble.moveTo(x, y); 1510 | } 1511 | 1512 | pathBlock(context) { 1513 | var w = this.ownWidth; 1514 | var h = this.ownHeight; 1515 | var r = 12; 1516 | 1517 | context.moveTo(0, r + .5); 1518 | context.arc(r, r + .5, r, PI, PI32, false); 1519 | context.arc(w - r, r + .5, r, PI32, 0, false); 1520 | context.arc(w - r, h - r - .5, r, 0, PI12, false); 1521 | context.arc(r, h - r - .5, r, PI12, PI, false); 1522 | } 1523 | 1524 | draw() { 1525 | this.canvas.width = this.ownWidth * density; 1526 | this.canvas.height = this.ownHeight * density; 1527 | this.canvas.style.width = this.ownWidth + 'px'; 1528 | this.canvas.style.height = this.ownHeight + 'px'; 1529 | this.context.scale(density, density); 1530 | this.drawOn(this.context); 1531 | 1532 | this.bubble.el.style.visibility = this.bubbleVisible ? 'visible' : 'hidden'; 1533 | if (this.bubble.curve) { 1534 | this.bubble.curve.el.style.visibility = this.bubbleVisible ? 'visible' : 'hidden'; 1535 | } 1536 | } 1537 | 1538 | drawOn(context) { 1539 | context.fillStyle = this._color; 1540 | bezel(context, this.pathBlock, this, false, density); 1541 | } 1542 | 1543 | /* * */ 1544 | 1545 | updateSinky() { 1546 | var isSink = this.outputs.filter(bubble => { 1547 | if (!bubble.parent || !this.parent) return; 1548 | return !bubble.parent.isBlock || (this.bubbleVisible && bubble.parent === this); 1549 | }).length; 1550 | this.repr.setSink(!!isSink); 1551 | } 1552 | 1553 | setDragging(dragging) { 1554 | this.bubble.el.style.visibility = !dragging && this.bubbleVisible ? 'visible' : 'hidden'; 1555 | } 1556 | 1557 | } 1558 | 1559 | /*****************************************************************************/ 1560 | 1561 | 1562 | class Source extends Drawable { 1563 | constructor(node, repr) { 1564 | super(); 1565 | 1566 | this.el = el('absolute source'); 1567 | this.el.appendChild(this.canvas = el('canvas', 'absolute')); 1568 | this.context = this.canvas.getContext('2d'); 1569 | 1570 | //this.node = Node.input(value); 1571 | this.node = node; 1572 | this.repr = repr; 1573 | if (!this.repr) { 1574 | this.repr = Node.repr(this.node); 1575 | this.repr.setSink(true); 1576 | } 1577 | 1578 | this.repr.onProgress(this.onProgress.bind(this)); 1579 | this.el.appendChild(this.progress = el('progress absolute')); 1580 | 1581 | this.result = new Result(this, this.repr); 1582 | this.el.appendChild(this.result.el); 1583 | 1584 | if (this.constructor === Source) { 1585 | this.outputs = []; 1586 | this.curves = []; 1587 | this.blob = this.bubble = new Blob(this); 1588 | this.el.appendChild(this.blob.el); 1589 | } 1590 | } 1591 | 1592 | static value(value) { 1593 | return new Source(Node.input(value)); 1594 | } 1595 | 1596 | addBubble(bubble) { 1597 | bubble.curve.parent.remove(bubble.curve); 1598 | bubble.parent.remove(bubble); 1599 | this.removeOutput(bubble); 1600 | } 1601 | 1602 | get isSource() { return true; } 1603 | get isDraggable() { return true; } 1604 | get isArg() { return true; } 1605 | 1606 | onProgress(e) { 1607 | this.fraction = e.loaded / e.total; 1608 | if (this.fraction < 1) { 1609 | this.progress.classList.add('progress-loading'); 1610 | } 1611 | this.drawProgress(); 1612 | } 1613 | 1614 | drawProgress() { 1615 | var f = this.fraction; // 0.1 + (this.fraction * 0.9); 1616 | var pw = this.width - 2 * Bubble.radius; 1617 | this.progress.style.width = `${f * pw}px`; 1618 | } 1619 | 1620 | objectFromPoint(x, y) { 1621 | if (opaqueAt(this.context, x * density, y * density)) return this.result; 1622 | var o = this.blob.objectFromPoint(x - this.blob.x, y - this.blob.y) 1623 | if (o) return o; 1624 | return null; 1625 | } 1626 | 1627 | get dragObject() { 1628 | return this; 1629 | } 1630 | 1631 | detach() { 1632 | if (this.isDoubleTap()) { 1633 | return this.copy(); 1634 | } 1635 | if (this.parent.isBlock) { 1636 | this.parent.reset(this); 1637 | } 1638 | return this; 1639 | } 1640 | 1641 | copy() { 1642 | // TODO this doesn't work 1643 | return Source.value(this.node.value); 1644 | } 1645 | 1646 | layoutChildren() { 1647 | this.blob.layoutChildren(); 1648 | this.result.layout(); 1649 | this.layoutSelf(); 1650 | } 1651 | 1652 | drawChildren() { 1653 | this.blob.drawChildren(); 1654 | this.result.draw(); 1655 | this.draw(); 1656 | } 1657 | 1658 | click() { 1659 | super.click(); 1660 | } 1661 | 1662 | layoutSelf() { 1663 | var px = Bubble.paddingX; 1664 | var py = Bubble.paddingY; 1665 | 1666 | var w = this.result.width; 1667 | var h = this.result.height; 1668 | this.width = Math.max(Bubble.minWidth, w + 4); 1669 | var t = 0; 1670 | var x = (this.width - w) / 2; 1671 | var y = t + py; 1672 | this.result.moveTo(x, y); 1673 | this.height = h + 2 * py + t; 1674 | 1675 | this.ownWidth = this.width; 1676 | this.ownHeight = this.height; 1677 | this.layoutBubble(this.blob); 1678 | this.moved(); 1679 | this.redraw(); 1680 | } 1681 | 1682 | layoutBubble(bubble) { 1683 | var x = (this.width - bubble.width) / 2; 1684 | var y = this.height - 1; 1685 | bubble.moveTo(x, y); 1686 | } 1687 | 1688 | pathBubble(context) { 1689 | var w = this.width; 1690 | var h = this.height; 1691 | var r = Bubble.radius; 1692 | var w12 = this.width / 2; 1693 | 1694 | context.moveTo(1, r + 1); 1695 | context.arc(r + 1, r + 1, r, PI, PI32, false); 1696 | context.arc(w - r - 1, r + 1, r, PI32, 0, false); 1697 | context.arc(w - r - 1, h - r - 1, r, 0, PI12, false); 1698 | context.arc(r + 1, h - r - 1, r, PI12, PI, false); 1699 | } 1700 | 1701 | draw() { 1702 | this.canvas.width = this.width * density; 1703 | this.canvas.height = this.height * density; 1704 | this.canvas.style.width = this.width + 'px'; 1705 | this.canvas.style.height = this.height + 'px'; 1706 | this.context.scale(density, density); 1707 | this.drawOn(this.context); 1708 | 1709 | this.drawProgress(); 1710 | } 1711 | 1712 | drawOn(context) { 1713 | this.pathBubble(context); 1714 | context.closePath(); 1715 | context.fillStyle = this.invalid ? '#aaa' : '#fff'; 1716 | context.fill(); 1717 | context.strokeStyle = '#555'; 1718 | context.lineWidth = density; 1719 | context.stroke(); 1720 | } 1721 | 1722 | pathShadowOn(context) { 1723 | this.pathBubble(context); 1724 | context.closePath(); 1725 | } 1726 | 1727 | moveTo(x, y) { 1728 | super.moveTo(x, y); 1729 | this.moved(); 1730 | } 1731 | 1732 | moved() { 1733 | this.curves.forEach(c => c.layoutSelf()); 1734 | } 1735 | 1736 | updateSinky() {} 1737 | 1738 | addOutput(output) { 1739 | this.outputs.push(output); 1740 | output.target = this; 1741 | 1742 | var curve = new Curve(this, output); 1743 | output.curve = curve; 1744 | this.curves.push(curve); 1745 | 1746 | this.layoutBubble(output); 1747 | } 1748 | 1749 | removeOutput(output) { 1750 | var index = this.outputs.indexOf(output); 1751 | this.outputs.splice(index, 1); 1752 | output.parent = null; 1753 | } 1754 | 1755 | } 1756 | 1757 | 1758 | 1759 | class Bubble extends Source { 1760 | constructor(target) { 1761 | super(target.node, target.repr); 1762 | this.el.className = 'absolute bubble'; 1763 | 1764 | this.target = target; 1765 | this.curve = null; 1766 | 1767 | if (target.workspace) target.workspace.add(this); 1768 | } 1769 | 1770 | get isSource() { return false; } 1771 | get isBubble() { return true; } 1772 | get isDraggable() { return true; } 1773 | 1774 | get parent() { return this._parent; } 1775 | set parent(value) { 1776 | this._parent = value; 1777 | if (this.target) this.target.updateSinky(); 1778 | } 1779 | 1780 | detach() { 1781 | if (this.isDoubleTap()) { 1782 | return this.makeSource(); 1783 | } 1784 | if (this.parent.isBlock) { 1785 | if (this.parent.bubble !== this) { 1786 | this.parent.reset(this); // TODO leave our value behind 1787 | } 1788 | } 1789 | return this; 1790 | } 1791 | 1792 | makeSource() { 1793 | return Source.value(this.node.value); 1794 | } 1795 | 1796 | copy() { 1797 | var r = new Bubble(this.target); 1798 | this.target.addOutput(r); 1799 | return r; 1800 | } 1801 | 1802 | destroy() { 1803 | this.target.removeOutput(this); 1804 | } 1805 | 1806 | objectFromPoint(x, y) { 1807 | return opaqueAt(this.context, x * density, y * density) ? this.result.objectFromPoint(x - this.result.x, y - this.result.y) : null; 1808 | } 1809 | 1810 | replaceWith(other) { 1811 | assert(this.isInside); 1812 | var obj = this.parent; 1813 | obj.replace(this, other); 1814 | if (other === this.target) { 1815 | assert(this.target.bubble.isBlob); 1816 | other.addBubble(this); 1817 | other.layoutChildren(); 1818 | } 1819 | } 1820 | 1821 | click() { 1822 | super.click(); 1823 | } 1824 | 1825 | moved() { 1826 | if (this.curve) this.curve.layoutSelf(); 1827 | } 1828 | 1829 | layoutChildren() { 1830 | // if (this.dirty) { 1831 | // this.dirty = false; 1832 | this.layoutSelf(); 1833 | } 1834 | 1835 | drawChildren() { 1836 | // if (this.dirty) { 1837 | // this.dirty = false; 1838 | this.layoutSelf(); 1839 | } 1840 | 1841 | layoutSelf() { 1842 | var px = Bubble.paddingX; 1843 | var py = Bubble.paddingY; 1844 | 1845 | var w = this.result.width; 1846 | var h = this.result.height; 1847 | this.width = Math.max(Bubble.minWidth, w + 4); 1848 | var t = Bubble.tipSize; 1849 | var x = (this.width - w) / 2; 1850 | var y = t + py; 1851 | this.result.moveTo(x, y - 1); 1852 | this.height = h + 2 * py + t - 1; 1853 | 1854 | this.moved(); 1855 | this.redraw(); 1856 | } 1857 | 1858 | pathBubble(context) { 1859 | var t = Bubble.tipSize; 1860 | var w = this.width; 1861 | var h = this.height; 1862 | var r = Bubble.radius; 1863 | var w12 = this.width / 2; 1864 | 1865 | context.moveTo(1, t + r); 1866 | context.arc(r + 1, t + r, r, PI, PI32, false); 1867 | context.lineTo(w12 - t, t); 1868 | context.lineTo(w12, 1); 1869 | context.lineTo(w12 + t, t); 1870 | context.arc(w - r - 1, t + r, r, PI32, 0, false); 1871 | context.arc(w - r - 1, h - r - 1, r, 0, PI12, false); 1872 | context.arc(r + 1, h - r - 1, r, PI12, PI, false); 1873 | } 1874 | 1875 | get isInside() { 1876 | return this.parent.isBlock && this.parent.bubble !== this; 1877 | } 1878 | 1879 | } 1880 | Bubble.measure = createMetrics('result-label'); 1881 | 1882 | Bubble.tipSize = 7; 1883 | Bubble.radius = 6; 1884 | Bubble.paddingX = 4; 1885 | Bubble.paddingY = 2; 1886 | Bubble.minWidth = 32; //26; 1887 | 1888 | 1889 | 1890 | class Result extends Frame { 1891 | constructor(bubble, repr) { 1892 | super(); 1893 | this.parent = bubble; 1894 | this.el.className += ' result'; 1895 | this.elContents.className += ' result-contents'; 1896 | 1897 | assert(repr instanceof Node); 1898 | this.repr = repr; 1899 | this.view = null; 1900 | this.display(); 1901 | setTimeout(() => this.display(this.repr.value)); 1902 | this.repr.onEmit(this.onEmit.bind(this)); 1903 | } 1904 | 1905 | get isDraggable() { return true; } 1906 | get dragObject() { 1907 | return this.parent; 1908 | } 1909 | get isZoomable() { return true; } 1910 | get isScrollable() { 1911 | return this.contentsRight > Result.maxWidth 1912 | || this.contentsBottom > Result.maxHeight 1913 | || this.view instanceof ImageView; 1914 | } 1915 | 1916 | get result() { 1917 | return this; 1918 | } 1919 | 1920 | objectFromPoint(x, y) { 1921 | var o = this.view.objectFromPoint(x - this.view.x, y - this.view.y) 1922 | return o ? o : this; 1923 | } 1924 | detach() { 1925 | return this.parent.detach(); 1926 | } 1927 | cloneify(cloneify) { 1928 | var bubble = this.result.parent; 1929 | if (cloneify) { 1930 | if (bubble.isInside) { 1931 | return bubble.copy(); 1932 | } else { 1933 | return bubble.detach(); 1934 | } 1935 | } else { 1936 | return bubble.makeSource(); 1937 | } 1938 | } 1939 | 1940 | fixZoom(zoom) { 1941 | return Math.max(1, zoom); 1942 | } 1943 | 1944 | click() { 1945 | this.parent.click(); 1946 | } 1947 | 1948 | display(value) { 1949 | this.elContents.innerHTML = ''; 1950 | this.view = View.fromJSON(value); 1951 | this.view.layoutChildren(); 1952 | this.view.drawChildren(); 1953 | this.view.parent = this; 1954 | this.elContents.appendChild(this.view.el); 1955 | this.layout(); 1956 | } 1957 | 1958 | onEmit(value) { 1959 | if (value === null) { 1960 | this.elContents.classList.add('result-invalid'); 1961 | return; 1962 | } 1963 | this.elContents.classList.remove('result-invalid'); 1964 | this.display(value); 1965 | if (this.fraction === 0) this.fraction = 1; 1966 | this.parent.drawProgress(); 1967 | setTimeout(() => { 1968 | this.parent.progress.classList.remove('progress-loading'); 1969 | }); 1970 | } 1971 | 1972 | layout() { 1973 | if (!this.parent) return; 1974 | this.layoutSelf(); 1975 | this.parent.layout(); 1976 | } 1977 | 1978 | layoutSelf() { 1979 | var px = this.view.marginX; 1980 | var pt, pb; 1981 | if (this.view.isBlock) { 1982 | pt = pb = this.view.marginY; 1983 | } else { 1984 | pt = 1; 1985 | pb = -1; 1986 | } 1987 | var w = this.view.width + 2 * px; 1988 | var h = Math.max(12, this.view.height + pt + pb); 1989 | this.view.moveTo(px, pt); 1990 | this.contentsRight = w; 1991 | this.contentsBottom = h; 1992 | this.width = Math.min(Result.maxWidth, w); 1993 | this.height = Math.min(Result.maxHeight, h); 1994 | this.makeBounds(); 1995 | this.draw(); 1996 | } 1997 | 1998 | draw() { 1999 | this.el.style.width = `${this.width}px`; 2000 | this.el.style.height = `${this.height}px`; 2001 | } 2002 | 2003 | moveTo(x, y) { 2004 | this.x = x | 0; 2005 | this.y = y | 0; 2006 | this.el.style.transform = `translate(${this.x}px, ${this.y}px)`; 2007 | } 2008 | 2009 | } 2010 | Result.maxWidth = 512; 2011 | Result.maxHeight = 512; 2012 | 2013 | /*****************************************************************************/ 2014 | 2015 | class View extends Drawable { 2016 | constructor(width, height) { 2017 | super(); 2018 | this.width = width; 2019 | this.height = height; 2020 | this.widthMode = this.width === 'auto' ? 'auto' : 'natural'; 2021 | //: this.width ? 'fixed' : 'natural'; 2022 | this.heightMode = this.height === 'auto' ? 'auto' : 'natural'; 2023 | //: this.height ? 'fixed' : 'natural'; 2024 | } 2025 | 2026 | get result() { 2027 | return this.parent.result; 2028 | } 2029 | 2030 | setHover(hover) { 2031 | if (this.parent.setHover) { 2032 | this.parent.setHover(hover); 2033 | } 2034 | } 2035 | 2036 | get isDraggable() { 2037 | return true; 2038 | } 2039 | get dragObject() { 2040 | return this; 2041 | } 2042 | detach() { 2043 | if (this.isDoubleTap()) { 2044 | return this.cloneify(); 2045 | } 2046 | return this.result.detach(); 2047 | } 2048 | cloneify(cloneify) { 2049 | return this.parent.cloneify(cloneify); 2050 | } 2051 | 2052 | setWidth(width) { 2053 | this.width = width; 2054 | this.layoutSelf(); 2055 | } 2056 | setHeight(height) { 2057 | this.height = height; 2058 | this.layoutSelf(); 2059 | } 2060 | 2061 | layoutView(w, h) { 2062 | this.naturalWidth = w; 2063 | this.naturalHeight = h; 2064 | var wm = this.widthMode; 2065 | var hm = this.heightMode; 2066 | this.width = wm === 'natural' ? w : wm === 'auto' ? null : this.width; 2067 | this.height = hm === 'natural' ? h : hm === 'auto' ? null : this.height; 2068 | } 2069 | 2070 | layoutSelf() { 2071 | this.layoutView(this.width, this.height); 2072 | this.redraw(); 2073 | } 2074 | 2075 | get marginX() { return 4; } 2076 | get marginY() { return 2; } 2077 | 2078 | static fromJSON(args) { 2079 | if (!args) return new TextView("null", ""); 2080 | args = args.slice(); 2081 | var selector = args.shift(); 2082 | var cls = View.classes[selector]; 2083 | return cls.fromArgs.apply(cls, args); 2084 | } 2085 | 2086 | static fromArgs(...args) { 2087 | return new this(...args); 2088 | } 2089 | 2090 | objectFromPoint(x, y) { 2091 | return containsPoint(this, x, y) ? this : null; 2092 | } 2093 | 2094 | draw() {} 2095 | } 2096 | 2097 | class RectView extends View { 2098 | constructor(fill, width, height, cls) { 2099 | super(width, height); 2100 | this.el = el('div', 'rect ' + (cls || '')); 2101 | this.fill = fill; 2102 | } 2103 | get isBlock() { return true; } 2104 | 2105 | get fill() { return this._fill; } 2106 | set fill(value) { 2107 | this._fill = value; 2108 | this.redraw(); 2109 | } 2110 | 2111 | draw() { 2112 | this.el.style.width = `${this.width}px`; 2113 | this.el.style.height = `${this.height}px`; 2114 | this.el.style.background = this.fill; 2115 | } 2116 | } 2117 | 2118 | class TextView extends View { 2119 | constructor(cls, text, width, height) { 2120 | super(width, height); 2121 | this.cls = cls || ''; 2122 | this.el = el('absolute text ' + cls); 2123 | this.text = text; 2124 | } 2125 | get isInline() { return true; } 2126 | 2127 | get text() { return this._text; } 2128 | set text(text) { 2129 | this._text = text = ''+text; 2130 | this.el.textContent = text; 2131 | 2132 | if (!TextView.measure[this.cls]) { 2133 | TextView.measure[this.cls] = createMetrics('text ' + this.cls); 2134 | } 2135 | var metrics = TextView.measure[this.cls](text); 2136 | this.layoutView(metrics.width, metrics.height | 0); 2137 | this.layout(); 2138 | } 2139 | 2140 | draw() { 2141 | this.el.style.width = `${this.width}px`; 2142 | this.el.style.height = `${this.height}px`; 2143 | } 2144 | } 2145 | TextView.measure = {}; 2146 | 2147 | class InlineView extends View { 2148 | constructor(children, width, height) { 2149 | super(width, height); 2150 | this.el = el('absolute view-inline'); 2151 | 2152 | this.children = children; 2153 | children.forEach(child => { 2154 | child.parent = this; 2155 | this.el.appendChild(child.el); 2156 | }); 2157 | } 2158 | get isInline() { return true; } 2159 | 2160 | static fromArgs(children, ...rest) { 2161 | return new this(children.map(View.fromJSON), ...rest); 2162 | } 2163 | 2164 | layoutChildren() { 2165 | this.children.forEach(c => c.layoutChildren()); 2166 | if (this.dirty) { 2167 | this.dirty = false; 2168 | this.layoutSelf(); 2169 | } 2170 | } 2171 | 2172 | drawChildren() { 2173 | this.children.forEach(c => c.drawChildren()); 2174 | if (this.graphicsDirty) { 2175 | this.graphicsDirty = false; 2176 | this.draw(); 2177 | } 2178 | } 2179 | 2180 | layoutSelf() { 2181 | var children = this.children; 2182 | var length = children.length; 2183 | var x = 0; 2184 | var h = 0; 2185 | var xs = []; 2186 | for (var i=0; i 0) y += Math.max(child.marginY, lastMargin); 2238 | lastMargin = child.marginY; 2239 | ys.push(y); 2240 | y += child.height; 2241 | if (child.width !== null) { 2242 | w = Math.max(w, child.width); 2243 | } 2244 | } 2245 | this.width = w; 2246 | this.height = y; 2247 | // TODO layoutView 2248 | this._marginY = child ? Math.max(child.marginY, children[0].marginY) : 4; 2249 | 2250 | for (var i=0; i 0) y += Math.max(row.marginY, lastMargin); 2502 | lastMargin = row.marginY; 2503 | ys.push(y); 2504 | y += row.height; 2505 | assert(row instanceof RowView); 2506 | 2507 | if (!cls) { 2508 | cls = row.cls === 'header' ? 'record' : row.cls; 2509 | } else if (row.cls !== cls) { 2510 | w = Math.max(w, row.width); 2511 | row.layoutRow([]); 2512 | continue; 2513 | } 2514 | 2515 | var cells = row.children; 2516 | cols = Math.max(cols, cells.length); 2517 | for (var j=0; j { 2850 | let [category, spec, defaults] = p; 2851 | var def = (defaults || []).slice(); 2852 | var color = colors[category] || '#555'; 2853 | var words = spec.split(/ /g); 2854 | var i = 0; 2855 | var add; 2856 | var addSize; 2857 | var parts = []; 2858 | words.forEach((word, index) => { 2859 | if (word === '%r') { 2860 | parts.push(ringBlock.copy()); 2861 | 2862 | } else if (word === '%b') { 2863 | var value = def.length ? def.shift() : !!(i++ % 2); 2864 | parts.push(new Switch(value)); 2865 | 2866 | } else if (word === '%fields') { 2867 | add = function() { 2868 | return [ 2869 | new Break(), 2870 | new Input("name", 'Symbol'), 2871 | new Input("", 'Text'), 2872 | ]; 2873 | } 2874 | addSize = 3; 2875 | 2876 | } else if (word === '%exp') { 2877 | add = function() { 2878 | return [ 2879 | new Break(), 2880 | new Input(def[this.parts.length - index - 3] || "", 'Text'), 2881 | ]; 2882 | } 2883 | addSize = 2; 2884 | parts.push(new Break()); 2885 | parts.push(new Input(def.length ? def.shift() : "", 'Text')); 2886 | 2887 | } else if (word === '%%') { 2888 | parts.push(new Label("%")); 2889 | 2890 | } else if (word === '%br') { 2891 | parts.push(new Break()); 2892 | 2893 | } else if (/^%/.test(word)) { 2894 | var value = def.length ? def.shift() : word === '%c' ? "#007de0" : ""; 2895 | parts.push(new Input(value, { 2896 | '%n': 'Num', 2897 | '%o': 'Record', 2898 | '%l': 'List', 2899 | '%c': 'Color', 2900 | '%m': 'Menu', 2901 | '%q': 'Symbol', 2902 | }[word])); 2903 | 2904 | } else { 2905 | parts.push(new Label(word)); 2906 | } 2907 | }); 2908 | 2909 | var isRing = category === 'ring'; 2910 | var b = new Block({spec, category, color, isRing}, parts); 2911 | blocksBySpec[spec] = b; 2912 | 2913 | if (add) { 2914 | b.count = 1 + def.length; 2915 | def.forEach(value => { 2916 | var obj = new Input(value, 'Text'); 2917 | b.add(new Break()); 2918 | b.add(obj); 2919 | b.inputs.push(obj); 2920 | }); 2921 | var delInput = new Arrow("◀", function() { 2922 | if (this === b) return; 2923 | for (var i=0; i { 2945 | this.insert(obj, this.parts.length - 2); 2946 | this.inputs.push(obj); 2947 | if (index === 1) obj.click(); 2948 | }); 2949 | })); 2950 | } 2951 | 2952 | if (isRing) { 2953 | ringBlock = b; 2954 | return; 2955 | } 2956 | if (category === 'hidden') { 2957 | return; 2958 | } 2959 | paletteContents.push(b); 2960 | }); 2961 | 2962 | class Search extends Drawable { 2963 | constructor(parent) { 2964 | super(); 2965 | this.parent = parent; 2966 | 2967 | this.el = el('input', 'absolute search'); 2968 | this.el.setAttribute('type', 'search'); 2969 | this.el.setAttribute('placeholder', 'search'); 2970 | this.el.style.left = '8px'; 2971 | this.el.style.top = '8px'; 2972 | this.el.style.width = '240px'; 2973 | 2974 | this.el.addEventListener('input', this.change.bind(this)); 2975 | this.el.addEventListener('keydown', this.keyDown.bind(this)); 2976 | this.layout(); 2977 | } 2978 | 2979 | click() { 2980 | this.el.focus(); 2981 | } 2982 | 2983 | layout() { 2984 | this.width = 242; 2985 | this.height = 28; 2986 | } 2987 | 2988 | change(e) { 2989 | this.parent.filter(this.el.value); 2990 | } 2991 | 2992 | keyDown(e) { 2993 | if (e.keyCode === 13) { 2994 | // TODO insert selected block into world 2995 | } 2996 | } 2997 | 2998 | } 2999 | 3000 | class Palette extends Workspace { 3001 | constructor() { 3002 | super(); 3003 | this.el.className += ' palette'; 3004 | 3005 | this.search = new Search(this); 3006 | this.elContents.appendChild(this.search.el); 3007 | 3008 | this.blocks = paletteContents; 3009 | this.blocks.forEach(o => { 3010 | this.add(o); 3011 | }); 3012 | } 3013 | 3014 | layout() {} 3015 | 3016 | filter(query) { 3017 | var words = query.split(/ /g).map(word => new RegExp(RegExp.escape(word), 'i')); 3018 | function matches(o) { 3019 | if (!query) return true; 3020 | for (var i=0; i { 3033 | if (matches(o)) { 3034 | o.moveTo(8, y); 3035 | w = Math.max(w, o.width); 3036 | o.el.style.visibility = 'visible'; 3037 | o.hidden = false; 3038 | y += o.height + 8; 3039 | } else { 3040 | o.el.style.visibility = 'hidden'; 3041 | o.hidden = true; 3042 | } 3043 | }); 3044 | this.contentsBottom = y; 3045 | this.contentsRight = w + 16; 3046 | this.scrollY = 0; 3047 | this.makeBounds(); 3048 | this.transform(); 3049 | } 3050 | 3051 | objectFromPoint(x, y) { 3052 | var pos = this.fromScreen(x, y); 3053 | if (containsPoint(this.search, pos.x - this.search.x, pos.y - this.search.y)) { 3054 | return this.search; 3055 | } 3056 | var scripts = this.scripts; 3057 | for (var i=scripts.length; i--;) { 3058 | var script = scripts[i]; 3059 | if (script.hidden) continue; 3060 | var o = script.objectFromPoint(pos.x - script.x, pos.y - script.y); 3061 | if (o) return o; 3062 | } 3063 | return this; 3064 | } 3065 | 3066 | get isPalette() { return true; } 3067 | } 3068 | 3069 | /*****************************************************************************/ 3070 | 3071 | class World extends Workspace { 3072 | constructor() { 3073 | super(); 3074 | this.el.className += ' world'; 3075 | this.elContents.className += ' world-contents'; 3076 | } 3077 | 3078 | get isWorld() { return true; } 3079 | get isInfinite() { return true; } 3080 | get isZoomable() { return true; } 3081 | 3082 | fixZoom(zoom) { 3083 | return Math.min(4.0, this.zoom); 3084 | } 3085 | 3086 | } 3087 | 3088 | /*****************************************************************************/ 3089 | 3090 | class App { 3091 | constructor() { 3092 | this.el = el('app'); 3093 | this.workspaces = []; 3094 | document.body.appendChild(this.el); 3095 | document.body.appendChild(this.elScripts = el('absolute dragging')); 3096 | 3097 | this.world = new World(this.elWorld = el('')); 3098 | this.palette = new Palette(this.elPalette = el('')); 3099 | this.workspaces = [this.world, this.palette]; 3100 | this.el.appendChild(this.world.el); 3101 | this.el.appendChild(this.palette.el); 3102 | 3103 | this.world.app = this; // TODO 3104 | 3105 | this.resize(); 3106 | this.palette.filter(""); 3107 | this.palette.search.el.focus(); 3108 | 3109 | this.fingers = []; 3110 | this.feedbackPool = []; 3111 | this.feedback = this.createFeedback(); 3112 | 3113 | document.addEventListener('touchstart', this.touchStart.bind(this)); 3114 | document.addEventListener('touchmove', this.touchMove.bind(this)); 3115 | document.addEventListener('touchend', this.touchEnd.bind(this)); 3116 | document.addEventListener('touchcancel', this.touchEnd.bind(this)); 3117 | document.addEventListener('mousedown', this.mouseDown.bind(this)); 3118 | document.addEventListener('mousemove', this.mouseMove.bind(this)); 3119 | document.addEventListener('mouseup', this.mouseUp.bind(this)); 3120 | // TODO pointer events 3121 | 3122 | this.scroll = null; 3123 | window.addEventListener('resize', this.resize.bind(this)); 3124 | document.addEventListener('wheel', this.wheel.bind(this)); 3125 | document.addEventListener('mousewheel', this.wheel.bind(this)); 3126 | document.addEventListener('gesturestart', this.gestureStart.bind(this)); 3127 | document.addEventListener('gesturechange', this.gestureChange.bind(this)); 3128 | document.addEventListener('gestureend', this.gestureEnd.bind(this)); 3129 | // TODO gesture events 3130 | 3131 | document.addEventListener('keydown', this.keyDown.bind(this)); 3132 | } 3133 | 3134 | get isApp() { return true; } 3135 | get app() { return this; } 3136 | 3137 | layout() {} 3138 | 3139 | resize(e) { 3140 | this.workspaces.forEach(w => w.resize()); 3141 | } 3142 | 3143 | keyDown(e) { 3144 | if (e.altKey) return; 3145 | if (isMac && e.ctrlKey) return; 3146 | if (isMac ? e.metaKey : e.ctrlKey) { 3147 | if (e.keyCode === 70) { 3148 | this.palette.search.el.focus(); 3149 | e.preventDefault(); 3150 | } 3151 | } 3152 | 3153 | if (e.target === document.body) { 3154 | if (e.keyCode === 8) { 3155 | e.preventDefault(); 3156 | } 3157 | } 3158 | } 3159 | 3160 | wheel(e) { 3161 | // TODO trackpad should scroll vertically; mouse scroll wheel should zoom! 3162 | if (!this.scroll) { 3163 | var w = this.frameFromPoint(e.clientX, e.clientY); 3164 | if (!e.ctrlKey) { 3165 | var o = w; 3166 | while (o && !o.canScroll(e.deltaX, e.deltaY)) { 3167 | do { 3168 | o = o.parent; 3169 | } while (o && !o.isScrollable); 3170 | } 3171 | if (o) w = o; 3172 | } 3173 | this.scroll = { 3174 | frame: w, 3175 | }; 3176 | } 3177 | 3178 | if (this.scroll.timeout) clearTimeout(this.scroll.timeout); 3179 | this.scroll.timeout = setTimeout(this.endScroll.bind(this), 200); 3180 | 3181 | var w = this.scroll.frame; 3182 | if (e.ctrlKey) { 3183 | if (w.isScrollable) { 3184 | e.preventDefault(); 3185 | var factor = Math.pow(1.01, -e.deltaY); 3186 | w.zoomBy(factor, e.clientX, e.clientY); 3187 | } 3188 | } else if (w.isScrollable) { 3189 | e.preventDefault(); 3190 | w.scrollBy(e.deltaX, e.deltaY); 3191 | } 3192 | } 3193 | 3194 | endScroll() { 3195 | this.scroll = null; 3196 | } 3197 | 3198 | workspaceScrolled() { 3199 | if (this.dragging) { 3200 | this.dragScript.moved(); 3201 | } 3202 | this.fingers.forEach(g => { 3203 | if (g.dragging) { 3204 | g.dragScript.moved(); 3205 | } 3206 | }); 3207 | } 3208 | 3209 | gestureStart(e) { 3210 | e.preventDefault(); 3211 | if (isNaN(e.scale)) return; 3212 | var w = this.frameFromPoint(e.clientX, e.clientY); 3213 | if (w) { 3214 | if (w.isScrollable) { 3215 | this.gesture = { 3216 | frame: w, 3217 | lastScale: 1.0, 3218 | }; 3219 | } 3220 | } 3221 | } 3222 | 3223 | gestureChange(e) { 3224 | e.preventDefault(); 3225 | if (!this.gesture) return; 3226 | if (isNaN(e.scale) || !isFinite(e.scale)) { 3227 | return; 3228 | } 3229 | var p = this.gesture; 3230 | p.frame.zoomBy(e.scale / p.lastScale, e.clientX, e.clientY); 3231 | p.lastScale = e.scale; 3232 | } 3233 | 3234 | gestureEnd(e) { 3235 | this.gesture = null; 3236 | } 3237 | 3238 | 3239 | mouseDown(e) { 3240 | var p = {clientX: e.clientX, clientY: e.clientY, identifier: this}; 3241 | if (!this.startFinger(p, e)) return; 3242 | this.fingerDown(p, e); 3243 | } 3244 | mouseMove(e) { 3245 | var p = {clientX: e.clientX, clientY: e.clientY, identifier: this}; 3246 | this.fingerMove(p, e); 3247 | } 3248 | mouseUp(e) { 3249 | var p = {clientX: e.clientX, clientY: e.clientY, identifier: this}; 3250 | this.fingerUp(p, e); 3251 | } 3252 | 3253 | touchStart(e) { 3254 | var touch = e.changedTouches[0]; 3255 | var p = {clientX: touch.clientX, clientY: touch.clientY, identifier: touch.identifier}; 3256 | if (!this.startFinger(p, e)) return; 3257 | this.fingerDown(p, e); 3258 | for (var i = e.changedTouches.length; i-- > 1;) { 3259 | touch = e.changedTouches[i]; 3260 | this.fingerDown({clientX: touch.clientX, clientY: touch.clientY, identifier: touch.identifier}, e); 3261 | } 3262 | } 3263 | 3264 | touchMove(e) { 3265 | var touch = e.changedTouches[0]; 3266 | var p = {clientX: touch.clientX, clientY: touch.clientY, identifier: touch.identifier}; 3267 | this.fingerMove(p, e); 3268 | for (var i = e.changedTouches.length; i-- > 1;) { 3269 | var touch = e.changedTouches[i]; 3270 | this.fingerMove({clientX: touch.clientX, clientY: touch.clientY, identifier: touch.identifier}, e); 3271 | } 3272 | } 3273 | 3274 | touchEnd(e) { 3275 | var touch = e.changedTouches[0]; 3276 | var p = {clientX: touch.clientX, clientY: touch.clientY, identifier: touch.identifier}; 3277 | this.fingerUp(p, e); 3278 | for (var i = e.changedTouches.length; i-- > 1;) { 3279 | var touch = e.changedTouches[i]; 3280 | this.fingerUp({clientX: touch.clientX, clientY: touch.clientY, identifier: touch.identifier}, e); 3281 | } 3282 | } 3283 | 3284 | createFinger(id) { 3285 | if (id === this) { 3286 | var g = this; 3287 | } else { 3288 | this.destroyFinger(id); 3289 | g = this.getFinger(id); 3290 | } 3291 | return g; 3292 | } 3293 | 3294 | getFinger(id) { 3295 | if (id === this) return this; 3296 | var g = this.fingers[id]; 3297 | if (g) return g; 3298 | return this.fingers[id] = {feedback: this.createFeedback()}; 3299 | } 3300 | 3301 | destroyFinger(id) { 3302 | var g = id === this ? this : this.fingers[id]; 3303 | if (g) { 3304 | if (g.dragging) this.drop(g); // TODO remove 3305 | this.destroyFeedback(g.feedback); 3306 | 3307 | // TODO set things 3308 | g.pressed = false; 3309 | g.pressObject = null; 3310 | g.dragging = false; 3311 | g.scrolling = false; 3312 | g.resizing = false; 3313 | g.shouldDrag = false; 3314 | g.dragScript = null; 3315 | if (g.hoverScript) g.hoverScript.setHover(false); 3316 | g.hoverScript = null; 3317 | 3318 | delete this.fingers[id]; 3319 | } 3320 | } 3321 | 3322 | startFinger(p, e) { 3323 | return true; 3324 | } 3325 | 3326 | objectFromPoint(x, y) { 3327 | var w = this.workspaceFromPoint(x, y) 3328 | if (!w) return null; 3329 | var pos = w.screenPosition; 3330 | return w.objectFromPoint(x - pos.x, y - pos.y); 3331 | } 3332 | 3333 | frameFromPoint(x, y) { 3334 | return this.frameFromObject(this.objectFromPoint(x, y)); 3335 | } 3336 | 3337 | frameFromObject(o) { 3338 | while (!o.isScrollable) { 3339 | o = o.parent; 3340 | } 3341 | return o; 3342 | } 3343 | 3344 | workspaceFromPoint(x, y) { 3345 | var workspaces = this.workspaces; 3346 | for (var i = workspaces.length; i--;) { 3347 | var w = workspaces[i]; 3348 | var pos = w.screenPosition; 3349 | if (containsPoint(w, x - pos.x, y - pos.y)) return w; 3350 | } 3351 | return null; 3352 | } 3353 | 3354 | fingerDown(p, e) { 3355 | var g = this.createFinger(p.identifier); 3356 | g.pressX = g.mouseX = p.clientX; 3357 | g.pressY = g.mouseY = p.clientY; 3358 | g.pressObject = this.objectFromPoint(g.pressX, g.pressY); 3359 | g.shouldDrag = false; 3360 | g.shouldScroll = false; 3361 | 3362 | if (g.pressObject) { 3363 | var leftClick = e.button === 0 || e.button === undefined; 3364 | if (e.button === 2 || leftClick && e.ctrlKey) { 3365 | // right-click 3366 | } else if (leftClick) { 3367 | g.canFingerScroll = e.button === undefined; 3368 | g.shouldDrag = g.pressObject.isDraggable; 3369 | g.shouldScroll = g.pressObject.isScrollable && g.canFingerScroll; 3370 | // TODO disable drag-scrolling using mouse 3371 | } 3372 | } 3373 | 3374 | if (g.shouldDrag || g.shouldScroll) { 3375 | document.activeElement.blur(); 3376 | e.preventDefault(); 3377 | } 3378 | 3379 | if (g.shouldScroll) { 3380 | g.pressObject.fingerScroll(0, 0); 3381 | } 3382 | 3383 | g.pressed = true; 3384 | g.dragging = false; 3385 | g.scrolling = false; 3386 | } 3387 | 3388 | fingerMove(p, e) { 3389 | var g = this.getFinger(p.identifier); 3390 | g.mouseX = p.clientX; 3391 | g.mouseY = p.clientY; 3392 | 3393 | if (g.pressed && g.shouldDrag && !g.dragging && g.canFingerScroll) { 3394 | var obj = g.pressObject.dragObject; 3395 | var frame = this.frameFromObject(g.pressObject); 3396 | var dx = g.mouseX - g.pressX; 3397 | var dy = g.mouseY - g.pressY; 3398 | 3399 | var canScroll = ( 3400 | (frame.isPalette && Math.abs(dx) < Math.abs(dy) 3401 | || (!frame.isWorkspace && frame.canScroll(-dx, -dy))) 3402 | ); 3403 | if (canScroll) { 3404 | g.shouldDrag = false; 3405 | g.shouldScroll = true; 3406 | g.pressObject = frame; 3407 | } 3408 | } 3409 | 3410 | if (g.pressed && g.shouldDrag && !g.dragging) { 3411 | this.drop(g); 3412 | g.shouldScroll = false; 3413 | var obj = g.pressObject.dragObject; 3414 | var pos = obj.screenPosition; 3415 | g.dragging = true; 3416 | g.dragWorkspace = obj.workspace; 3417 | g.dragX = pos.x - g.pressX; 3418 | g.dragY = pos.y - g.pressY; 3419 | assert(''+g.dragX !== 'NaN'); 3420 | g.dragScript = obj.detach(); 3421 | if (obj.dragOffset) { 3422 | var offset = obj.dragOffset(g.dragScript); 3423 | g.dragX += offset.x * this.world.zoom; 3424 | g.dragY += offset.y * this.world.zoom; 3425 | } 3426 | if (g.dragScript.parent) { 3427 | g.dragScript.parent.remove(g.dragScript); 3428 | } 3429 | g.dragScript.parent = this; 3430 | g.dragScript.zoom = this.world.zoom; 3431 | this.elScripts.appendChild(g.dragScript.el); 3432 | g.dragScript.layoutChildren(); 3433 | g.dragScript.drawChildren(); 3434 | g.dragScript.setDragging(true); 3435 | // TODO add shadow 3436 | 3437 | } else if (g.pressed && g.shouldScroll && !g.scrolling) { 3438 | g.scrolling = true; 3439 | g.shouldScroll = false; 3440 | g.scrollX = g.pressX; 3441 | g.scrollY = g.pressY; 3442 | 3443 | } 3444 | 3445 | if (g.scrolling || g.dragging) { 3446 | if (g.hoverScript) g.hoverScript.setHover(false); 3447 | g.hoverScript = null; 3448 | } 3449 | 3450 | if (g.scrolling) { 3451 | g.pressObject.fingerScroll(g.mouseX - g.scrollX, g.mouseY - g.scrollY) 3452 | g.scrollX = g.mouseX; 3453 | g.scrollY = g.mouseY; 3454 | e.preventDefault(); 3455 | } else if (g.dragging) { 3456 | g.dragScript.moveTo((g.dragX + g.mouseX), (g.dragY + g.mouseY)); 3457 | this.showFeedback(g); 3458 | e.preventDefault(); 3459 | } 3460 | 3461 | var obj = this.objectFromPoint(g.mouseX, g.mouseY); 3462 | if (!obj || !obj.setHover) obj = null; 3463 | if (obj !== g.hoverScript) { 3464 | if (g.hoverScript) g.hoverScript.setHover(false); 3465 | g.hoverScript = obj; 3466 | if (g.hoverScript) g.hoverScript.setHover(true); 3467 | } 3468 | } 3469 | 3470 | fingerUp(p, e) { 3471 | var g = this.getFinger(p.identifier); 3472 | 3473 | if (g.scrolling) { 3474 | g.pressObject.fingerScrollEnd(); 3475 | } else if (g.dragging) { 3476 | this.drop(g); 3477 | } else if (g.shouldDrag || g.shouldResize) { 3478 | g.pressObject.click(g.pressX, g.pressY); 3479 | } 3480 | 3481 | // TODO 3482 | 3483 | this.destroyFinger(p.identifier); 3484 | } 3485 | 3486 | drop(g) { 3487 | if (!g) g = this.getGesture(this); 3488 | if (!g.dragging) return; 3489 | g.feedback.canvas.style.display = 'none'; 3490 | 3491 | g.dragScript.setDragging(false); 3492 | if (g.feedbackInfo) { 3493 | var info = g.feedbackInfo; 3494 | info.obj.replaceWith(g.dragScript); 3495 | } else { 3496 | g.dropWorkspace = this.workspaceFromPoint(g.dragX + g.mouseX, g.dragY + g.mouseY) || this.world; 3497 | var d = g.dragScript; 3498 | var canDelete = false; 3499 | if (d.isBlock || d.isSource) { 3500 | canDelete = d.outputs.filter(bubble => { 3501 | return bubble.parent !== d && bubble.parent.isBlock; 3502 | }).length === 0; 3503 | } 3504 | // TODO don't delete if inputs 3505 | if (g.dropWorkspace.isPalette && canDelete) { 3506 | this.remove(d); 3507 | d.outputs.forEach(bubble => { 3508 | if (bubble.parent === this.world) this.world.remove(bubble); 3509 | if (bubble.curve.parent === this.world) this.world.remove(bubble.curve); 3510 | }); 3511 | d.destroy(); 3512 | } else { 3513 | g.dropWorkspace = this.world; 3514 | var pos = g.dropWorkspace.worldPositionOf(g.dragX + g.mouseX, g.dragY + g.mouseY); 3515 | g.dropWorkspace.add(d); 3516 | d.moveTo(pos.x, pos.y); 3517 | } 3518 | } 3519 | 3520 | g.dragging = false; 3521 | g.dragPos = null; 3522 | g.dragState = null; 3523 | g.dragWorkspace = null; 3524 | g.dragScript = null; 3525 | g.dropWorkspace = null; 3526 | g.feedbackInfo = null; 3527 | g.commandScript = null; 3528 | } 3529 | 3530 | remove(o) { 3531 | this.elScripts.removeChild(o.el); 3532 | // TODO 3533 | } 3534 | 3535 | 3536 | createFeedback() { 3537 | if (this.feedbackPool.length) { 3538 | return this.feedbackPool.pop(); 3539 | } 3540 | var feedback = el('canvas', 'absolute feedback'); 3541 | var feedbackContext = feedback.getContext('2d'); 3542 | feedback.style.display = 'none'; 3543 | document.body.appendChild(feedback); 3544 | return feedbackContext; 3545 | }; 3546 | 3547 | destroyFeedback(feedback) { 3548 | if (feedback) { 3549 | this.feedbackPool.push(feedback); 3550 | } 3551 | }; 3552 | 3553 | showFeedback(g) { 3554 | g.feedbackDistance = Infinity; 3555 | g.feedbackInfo = null; 3556 | //g.dropWorkspace = null; 3557 | 3558 | var w = this.workspaceFromPoint(g.mouseX, g.mouseY); 3559 | if (w === this.world) { 3560 | var pos = w.screenPositionOf(0, 0); 3561 | w.scripts.forEach(script => this.addFeedback(g, pos.x, pos.y, script)); 3562 | } 3563 | 3564 | if (g.feedbackInfo) { 3565 | this.renderFeedback(g); 3566 | g.feedback.canvas.style.display = 'block'; 3567 | } else { 3568 | g.feedback.canvas.style.display = 'none'; 3569 | } 3570 | } 3571 | 3572 | addFeedback(g, x, y, obj) { 3573 | if (obj.isCurve) return; 3574 | 3575 | assert(''+x !== 'NaN'); 3576 | x += obj.x * this.world.zoom; 3577 | y += obj.y * this.world.zoom; 3578 | if (obj.isBlock) { 3579 | obj.parts.forEach(child => this.addFeedback(g, x, y, child)); 3580 | if (obj.bubble.isBlob) { 3581 | this.addFeedback(g, x, y, obj.blob); 3582 | } 3583 | } 3584 | if (obj.isSource) { 3585 | this.addFeedback(g, x, y, obj.blob); 3586 | } 3587 | 3588 | var gx = g.dragScript.x; 3589 | var gy = g.dragScript.y; 3590 | var canDrop = false; 3591 | if (g.dragScript.isBubble && obj.isBlob && obj.target === g.dragScript.target) { 3592 | gx += g.dragScript.width / 2; 3593 | canDrop = true; 3594 | } else if (obj.isInput || obj.isSwitch) { 3595 | if (g.dragScript.isBlock) { 3596 | canDrop = g.dragScript.outputs.length === 1 && g.dragScript.bubble.isBubble; 3597 | } else if (g.dragScript.isBubble) { 3598 | canDrop = g.dragScript.target !== obj.parent; 3599 | } else { 3600 | canDrop = true; 3601 | } 3602 | } else if (obj.isBubble) { 3603 | if (g.dragScript.isBlock) { 3604 | canDrop = obj.isInside && g.dragScript === obj.target && g.dragScript.outputs.length === 1; 3605 | } 3606 | } 3607 | 3608 | if (canDrop) { 3609 | var dx = x - gx; 3610 | var dy = y - gy; 3611 | var d2 = dx * dx + dy * dy; 3612 | if (Math.abs(dx) > this.feedbackRange || Math.abs(dy) > this.feedbackRange || d2 > g.feedbackDistance) return; 3613 | g.feedbackDistance = d2; 3614 | g.feedbackInfo = {x, y, obj}; 3615 | } 3616 | } 3617 | 3618 | renderFeedback(g) { 3619 | var feedbackColor = '#ffa'; 3620 | var info = g.feedbackInfo; 3621 | var context = g.feedback; 3622 | var canvas = g.feedback.canvas; 3623 | var l = this.feedbackLineWidth; 3624 | var r = l/2; 3625 | 3626 | var l = 6; 3627 | var x = info.x - l; 3628 | var y = info.y - l; 3629 | var w = info.obj.width * this.world.zoom; 3630 | var h = info.obj.height * this.world.zoom; 3631 | canvas.width = w + l * 2; 3632 | canvas.height = h + l * 2; 3633 | 3634 | context.translate(l, l); 3635 | var s = this.world.zoom; 3636 | context.scale(s, s); 3637 | 3638 | info.obj.pathShadowOn(context); 3639 | 3640 | context.lineWidth = l / 1; 3641 | context.lineCap = 'round'; 3642 | context.lineJoin = 'round'; 3643 | context.strokeStyle = feedbackColor; 3644 | context.stroke(); 3645 | 3646 | context.globalCompositeOperation = 'destination-out'; 3647 | context.beginPath(); 3648 | info.obj.pathShadowOn(context); 3649 | context.fill(); 3650 | context.globalCompositeOperation = 'source-over'; 3651 | context.globalAlpha = .7; 3652 | context.fillStyle = feedbackColor; 3653 | context.fill(); 3654 | 3655 | canvas.style.transform = 'translate('+x+'px,'+y+'px)'; 3656 | } 3657 | 3658 | get feedbackRange() { 3659 | return 26 * this.world.zoom; 3660 | } 3661 | 3662 | } 3663 | 3664 | window.app = new App(); 3665 | 3666 | window.onbeforeunload = e => "AAAAA"; 3667 | 3668 | --------------------------------------------------------------------------------