├── .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 |
--------------------------------------------------------------------------------