├── README.md
├── package.json
├── src
├── animPoly.js
├── index.html
├── index_release.html
├── main.js
└── touchy.js
└── tiramisu_ss.png
/README.md:
--------------------------------------------------------------------------------
1 | # tiramisu
2 | A touch screen tree editor. The root is displayed at the bottom, its children are placed horizontally directly above it.
3 |
4 | 
5 |
6 | ## Demo
7 | * See it in action [on YouTube](https://youtu.be/Rr1tofDc_DQ)
8 | * Try it out at https://gashlin.net/tests/tiramisu/
9 |
10 | ## Instructions
11 | * Tap a node to edit text
12 | * Drag a node up to add a parent
13 | * Drag a node left or right to add a sibling node
14 | * Drag a node down to delete a node
15 | * Long press to cut a node, it can be pasted from the edit screen
16 | * Pinch to zoom out
17 |
18 | ## Credits
19 | Inspired by a diagram on [a recipe by Michael Chu](http://www.cookingforengineers.com/recipe/60/The-Classic-Tiramisu-original-recipe) by way of Li Haoyi's article [What's Functional Programming All About?](http://www.lihaoyi.com/post/WhatsFunctionalProgrammingAllAbout.html).
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tiramisu",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "build": "mkdir -p release && cat src/animPoly.js src/touchy.js src/main.js | babel --out-file release/combined.js --presets=env && cp -v src/index_release.html release/index.html"
6 | },
7 | "files": [
8 | "animPoly.js",
9 | "touchy.js",
10 | "main.js"
11 | ],
12 | "devDependencies": {
13 | "babel-cli": "^6.24.1",
14 | "babel-preset-env": "^1.5.2"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/animPoly.js:
--------------------------------------------------------------------------------
1 | // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
2 | // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
3 |
4 | // requestAnimationFrame polyfill by Erik Möller
5 | // fixes from Paul Irish and Tino Zijdel
6 | (function() {
7 | var lastTime = 0;
8 | var vendors = ['ms', 'moz', 'webkit', 'o'];
9 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
10 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
11 | window.cancelAnimationFrame =
12 | window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
13 | }
14 |
15 | if (!window.requestAnimationFrame)
16 | window.requestAnimationFrame = function(callback, element) {
17 | var currTime = new Date().getTime();
18 | var timeToCall = Math.max(0, 16 - (currTime - lastTime));
19 | var id = window.setTimeout(function() { callback(currTime + timeToCall); },
20 | timeToCall);
21 | lastTime = currTime + timeToCall;
22 | return id;
23 | };
24 |
25 | if (!window.cancelAnimationFrame)
26 | window.cancelAnimationFrame = function(id) {
27 | clearTimeout(id);
28 | };
29 | }());
30 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
48 | tiramisu tree editor
49 |
50 |
51 |
52 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/index_release.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
48 | tiramisu tree editor
49 |
50 |
51 |
52 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | 'use strict';
3 |
4 | //// graphics setup
5 |
6 | const dpr = window.devicePixelRatio ? window.devicePixelRatio : 1;
7 | const cnv = document.getElementById('cnv');
8 | const ctx = cnv.getContext('2d');
9 |
10 | let WIDE = window.innerWidth;
11 | let HIGH = window.innerHeight;
12 |
13 | let drawRequested = false;
14 | const draw = function(t) {
15 | drawRequested = false;
16 |
17 | ctx.fillStyle = 'white';
18 | ctx.fillRect(0, 0, WIDE, HIGH);
19 |
20 | drawObjects(ctx);
21 | };
22 |
23 | const requestDraw = function() {
24 | if (!drawRequested) {
25 | drawRequested = true;
26 |
27 | window.requestAnimationFrame(draw);
28 | }
29 | };
30 |
31 | const resize = function() {
32 | WIDE = window.innerWidth;
33 | HIGH = window.innerHeight;
34 |
35 | cnv.width = WIDE * dpr;
36 | cnv.height = HIGH * dpr;
37 |
38 | cnv.style.width = `${WIDE}px`;
39 | cnv.style.height = `${HIGH}px`;
40 |
41 | ctx.setTransform(1, 0, 0, 1, 0, 0);
42 | ctx.scale(dpr, dpr);
43 |
44 | requestDraw();
45 | };
46 |
47 | window.addEventListener('resize', resize);
48 | window.addEventListener('focus', requestDraw);
49 |
50 | let CB_WIDE = 0;
51 | let CB_HIGH = 0;
52 |
53 | const cbCnv = document.getElementById('clipboard');
54 | const cbCtx = cbCnv.getContext('2d');
55 |
56 | const drawClipboard = function() {
57 | drawClipboardObjects(cbCtx);
58 |
59 | cbCtx.strokeStyle = 'black';
60 | cbCtx.lineWidth = 3;
61 | cbCtx.strokeRect(1, 1, CB_WIDE - 2, CB_HIGH - 2);
62 | };
63 |
64 | const resizeClipboard = function(wide, high) {
65 | CB_WIDE = wide;
66 | CB_HIGH = high;
67 | cbCnv.width = CB_WIDE * dpr;
68 | cbCnv.height = CB_HIGH * dpr;
69 |
70 | cbCnv.style.width = `${CB_WIDE}px`;
71 | cbCnv.style.height = `${CB_HIGH}px`;
72 |
73 | cbCtx.setTransform(1, 0, 0, 1, 0, 0);
74 | cbCtx.scale(dpr, dpr);
75 | };
76 |
77 | //// mid-level touch handlers
78 |
79 | const dragDist = 10;
80 | let TOUCH_BEGAN = null;
81 | let TOUCH_NODE = null;
82 | let NEW_NODE = null;
83 | let EDITING_NODE = null;
84 | let DRAG_MODE = null;
85 | let DRAG_FEEL_X = 0;
86 | let DRAG_FEEL_Y = 0;
87 | let DRAG_LEFT_START_WIDTH = 0;
88 | let PINCH_BEGAN = null;
89 | let LONG_PRESS_TIMEOUT = null;
90 |
91 | const touchStart = function({x, y}){
92 | x = (x - SCROLL.x) / ZOOM.z;
93 | y = (y - SCROLL.y) / ZOOM.z;
94 |
95 | TOUCH_BEGAN = {x, y};
96 | TOUCH_NODE = nodeAt(0, 0, {x, y}, TREE);
97 |
98 | initDrag();
99 |
100 | LONG_PRESS_TIMEOUT = window.setTimeout(longPress, 750);
101 |
102 | requestDraw();
103 | };
104 |
105 | const touchMove = function({x, y}){
106 | if (!TOUCH_BEGAN) {
107 | return;
108 | }
109 | x = (x - SCROLL.x) / ZOOM.z;
110 | y = (y - SCROLL.y) / ZOOM.z;
111 |
112 | doDrag({x, y});
113 |
114 | requestDraw();
115 | };
116 |
117 | const touchEnd = function({x, y}){
118 | if (!TOUCH_BEGAN) {
119 | return;
120 | }
121 | x = (x - SCROLL.x) / ZOOM.z;
122 | y = (y - SCROLL.y) / ZOOM.z;
123 |
124 | if (DRAG_MODE) {
125 | doDrag({x, y});
126 | dragDrop();
127 | } else {
128 | // just a click
129 | doClick(TOUCH_BEGAN);
130 | }
131 |
132 | if (LONG_PRESS_TIMEOUT) {
133 | clearTimeout(LONG_PRESS_TIMEOUT);
134 | LONG_PRESS_TIMEOUT = null;
135 | }
136 |
137 | TOUCH_BEGAN = null;
138 | TOUCH_NODE = null;
139 |
140 | requestDraw();
141 | };
142 |
143 | const touchCancel = function() {
144 | if (!TOUCH_BEGAN) {
145 | return;
146 | }
147 |
148 | if (DRAG_MODE) {
149 | dragDrop()
150 | }
151 |
152 | TOUCH_BEGAN = null;
153 | TOUCH_NODE = null;
154 |
155 | if (LONG_PRESS_TIMEOUT) {
156 | clearTimeout(LONG_PRESS_TIMEOUT);
157 | LONG_PRESS_TIMEOUT = null;
158 | }
159 |
160 | requestDraw();
161 | };
162 |
163 | const pinchStart = function({x: x1, y: y1}, {x: x2, y: y2}) {
164 | PINCH_BEGAN = { p1: {x: x1, y: y1}, p2: {x: x2, y: y2} };
165 |
166 | requestDraw();
167 | };
168 |
169 | const pinchMove = function({x: x1, y: y1}, {x: x2, y: y2}) {
170 | if (!PINCH_BEGAN) {
171 | return;
172 | }
173 |
174 | changeZoom({
175 | ox1: PINCH_BEGAN.p1.x, oy1: PINCH_BEGAN.p1.y,
176 | ox2: PINCH_BEGAN.p2.x, oy2: PINCH_BEGAN.p2.y,
177 | nx1: x1, ny1: y1,
178 | nx2: x2, ny2: y2});
179 |
180 | requestDraw();
181 | };
182 |
183 | const pinchEnd = function({x: x1, y: y1}, {x: x2, y: y2}) {
184 | if (!PINCH_BEGAN) {
185 | return;
186 | }
187 |
188 | pinchMove({x: x1, y: y1}, {x: x2, y: y2});
189 | finishZoom();
190 |
191 | PINCH_BEGAN = null;
192 |
193 | requestDraw();
194 | };
195 |
196 | //// register touch handlers
197 | GET_TOUCHY(cnv, {
198 | touchStart,
199 | touchMove,
200 | touchEnd,
201 | touchCancel,
202 | pinchStart,
203 | pinchMove,
204 | pinchEnd,
205 | });
206 |
207 | window.addEventListener('wheel', function (e) {
208 | e.preventDefault();
209 |
210 | const cx = e.pageX;
211 | const cy = e.pageY;
212 | let delta = -e.deltaY;
213 |
214 | if (e.deltaMode === 0x01) {
215 | delta *= 20;
216 | }
217 | if (e.deltaMode === 0x02) {
218 | delta *= 20 * 10;
219 | }
220 |
221 | changeZoomMouse({delta, cx, cy});
222 |
223 | requestDraw();
224 | }, {passive: false});
225 |
226 | ////
227 |
228 | const initDrag = function() {
229 | DRAG_MODE = null;
230 | DRAG_FEEL_X = 0;
231 | DRAG_FEEL_Y = 0;
232 | };
233 |
234 | const doDrag = function({x, y}) {
235 | const dx = x - TOUCH_BEGAN.x;
236 | const dy = y - TOUCH_BEGAN.y;
237 | DRAG_FEEL_X = dx;
238 | DRAG_FEEL_Y = dy;
239 | if (!DRAG_MODE) {
240 |
241 | if (LONG_PRESS_TIMEOUT) {
242 | clearTimeout(LONG_PRESS_TIMEOUT);
243 | LONG_PRESS_TIMEOUT = null;
244 | }
245 | // starting drag
246 | if (TOUCH_NODE) {
247 | // dragging a node
248 | if (dx > dragDist || dx < -dragDist) {
249 | if (dx > dragDist) {
250 | console.log('drag right');
251 | DRAG_MODE = 'right';
252 | } else {
253 | console.log('drag left');
254 | DRAG_LEFT_START_WIDTH = TREE.width;
255 | DRAG_MODE = 'left';
256 | }
257 |
258 | const p = TOUCH_NODE.handle ? null : findParent(TOUCH_NODE, TREE);
259 | if (p == null) {
260 | // root or handle can have no siblings
261 | DRAG_MODE = 'pan';
262 | } else {
263 | NEW_NODE = {name: '', children: [], slidOver: 0};
264 | if (dx > dragDist) {
265 | addSiblingBefore(p, TOUCH_NODE, NEW_NODE);
266 | } else {
267 | addSiblingAfter(p, TOUCH_NODE, NEW_NODE);
268 | }
269 | }
270 | } else if (dy > dragDist) {
271 | console.log('drag down');
272 | DRAG_MODE = 'down';
273 |
274 | if (TOUCH_NODE.handle) {
275 | // can't delete handles
276 | DRAG_MODE = 'pan';
277 | } else if (!findParent(TOUCH_NODE, TREE)) {
278 | // don't want to make it easy to delete the whole tree
279 | DRAG_MODE = 'pan';
280 | } else {
281 | TOUCH_NODE.slidOut = 0;
282 | TOUCH_NODE.slideUnder = true;
283 | }
284 | } else if (dy < -dragDist) {
285 | console.log('drag up');
286 | DRAG_MODE = 'up';
287 |
288 | const p = TOUCH_NODE.handle ? TOUCH_NODE.p : findParent(TOUCH_NODE, TREE);
289 | if (p == null) {
290 | // new root
291 | NEW_NODE = {name: '', children: [TREE], slidOut: 0};
292 | TREE = NEW_NODE;
293 | } else if (TOUCH_NODE.handle) {
294 | NEW_NODE = {name: '', children: [], slidOut: 0};
295 | p.children = [NEW_NODE];
296 | } else {
297 | NEW_NODE = {name: '', children: [TOUCH_NODE], slidOut: 0};
298 | replaceChild(p, TOUCH_NODE, NEW_NODE);
299 | }
300 | }
301 | } else {
302 | // dragging nothing, just pan
303 | DRAG_MODE = 'pan';
304 | }
305 | }
306 |
307 | if (DRAG_MODE) {
308 | DRAG_FEEL_X = 0;
309 | DRAG_FEEL_Y = 0;
310 | }
311 |
312 | if (DRAG_MODE == 'left' || DRAG_MODE == 'right') {
313 | const slidOver =
314 | Math.max(0, Math.min(lineHeight, DRAG_MODE == 'right' ? dx : -dx));
315 | NEW_NODE.slidOver = slidOver;
316 |
317 | measureTree(ctx, TREE);
318 |
319 | if (DRAG_MODE == 'left') {
320 | SCROLL.tx = DRAG_LEFT_START_WIDTH - TREE.width;
321 | }
322 | } else if (DRAG_MODE == 'down') {
323 | const slidOut = Math.max(0, Math.min(lineHeight, lineHeight - dy));
324 | TOUCH_NODE.slidOut = slidOut;
325 | measureTree(ctx, TREE);
326 | } else if (DRAG_MODE == 'up') {
327 | const slidOut = Math.max(0, Math.min(lineHeight, -dy));
328 | NEW_NODE.slidOut = slidOut;
329 | measureTree(ctx, TREE);
330 | } else if (DRAG_MODE == 'pan') {
331 | SCROLL.tx = dx * ZOOM.z;
332 | SCROLL.ty = dy * ZOOM.z;
333 | }
334 | };
335 |
336 | const dragDrop = function() {
337 | if (!DRAG_MODE) {
338 | return;
339 | }
340 |
341 | if (DRAG_MODE == 'left' || DRAG_MODE == 'right') {
342 | if (NEW_NODE.slidOver < lineHeight) {
343 | const p = findParent(NEW_NODE, TREE);
344 | removeChild(p, NEW_NODE);
345 | } else {
346 | SCROLL.x += SCROLL.tx;
347 |
348 | showEditScreen(NEW_NODE);
349 | }
350 | SCROLL.tx = 0;
351 |
352 | NEW_NODE.slidOver = null;
353 | NEW_NODE = null;
354 | } else if (DRAG_MODE == 'down') {
355 | if (TOUCH_NODE.slidOut == 0) {
356 | const p = findParent(TOUCH_NODE, TREE);
357 | let lastChild = TOUCH_NODE;
358 |
359 | // copy my children as siblings
360 | for (let i = 0; i < TOUCH_NODE.children.length; ++i) {
361 | addSiblingAfter(p, lastChild, TOUCH_NODE.children[i]);
362 | lastChild = TOUCH_NODE.children[i];
363 | }
364 | TOUCH_NODE.children = [];
365 |
366 | removeChild(p, TOUCH_NODE);
367 | }
368 |
369 | TOUCH_NODE.slidOut = null;
370 | TOUCH_NODE.slideUnder = false;
371 | } else if (DRAG_MODE == 'up') {
372 | if (NEW_NODE.slidOut < lineHeight) {
373 | // cancel new node
374 | if (NEW_NODE == TREE) {
375 | TREE = NEW_NODE.children[0];
376 | } else if (TOUCH_NODE.handle) {
377 | TOUCH_NODE.p.children = [];
378 | } else {
379 | const p = findParent(NEW_NODE, TREE);
380 | replaceChild(p, NEW_NODE, TOUCH_NODE);
381 | }
382 | } else {
383 | showEditScreen(NEW_NODE);
384 | }
385 |
386 | NEW_NODE.slidOut = null;
387 | NEW_NODE = null;
388 | } else if (DRAG_MODE == 'pan') {
389 | SCROLL.x += SCROLL.tx;
390 | SCROLL.y += SCROLL.ty;
391 | SCROLL.tx = 0;
392 | SCROLL.ty = 0;
393 | }
394 |
395 | DRAG_MODE = null;
396 | DRAG_FEEL_X = 0;
397 | DRAG_FEEL_Y = 0;
398 | };
399 |
400 | let CANCEL_PROMPT = null;
401 | const PROMPT = document.getElementById('prompt');
402 | const PROMPT_FORM = document.getElementById('prompt-form');
403 | const PROMPT_MSG = document.getElementById('prompt-msg');
404 | const PROMPT_INPUT = document.getElementById('prompt-input');
405 |
406 | const CLIPBOARD_MSG = document.getElementById('clipboard-msg');
407 |
408 | const promptText = function(init, msg, cb, cbc) {
409 | if (typeof init !== 'string') {
410 | init = '';
411 | }
412 |
413 | if (CANCEL_PROMPT) {
414 | CANCEL_PROMPT();
415 | }
416 |
417 | PROMPT_MSG.textContent = msg;
418 | PROMPT.style.visibility = 'visible';
419 |
420 | const submitHandler = function(e) {
421 | const value = PROMPT_INPUT.value;
422 | cancelPromptText(submitHandler);
423 | PROMPT_INPUT.blur();
424 | e.preventDefault();
425 |
426 | cb(value);
427 | };
428 |
429 | PROMPT_FORM.addEventListener('submit', submitHandler);
430 |
431 | PROMPT_INPUT.value = init;
432 | PROMPT_INPUT.focus();
433 |
434 | CANCEL_PROMPT = function () {
435 | cancelPromptText(submitHandler);
436 | if (cbc) {
437 | cbc();
438 | }
439 | };
440 | };
441 |
442 | const cancelPromptText = function(submitHandler) {
443 | PROMPT_INPUT.blur();
444 | PROMPT_INPUT.value = '';
445 | PROMPT.style.visibility = 'hidden'
446 | PROMPT_FORM.removeEventListener('submit', submitHandler);
447 | CANCEL_PROMPT = null;
448 | };
449 |
450 | const doClick = function({x, y}) {
451 | const node = nodeAt(0, 0, {x, y}, TREE);
452 |
453 | if (!node || node.handle) {
454 | return;
455 | }
456 |
457 | showEditScreen(node);
458 | };
459 |
460 | const showEditScreen = function(node) {
461 | EDITING_NODE = node;
462 |
463 | drawClipboard(cbCtx);
464 | if (CLIPBOARD) {
465 | CLIPBOARD_MSG.textContent = 'Clipboard';
466 | } else {
467 | CLIPBOARD_MSG.textContent = '';
468 | }
469 |
470 | const takeClipboard = function(e) {
471 | CANCEL_PROMPT();
472 | cbCnv.removeEventListener('click', takeClipboard);
473 |
474 | if (CLIPBOARD) {
475 |
476 | const newTree = copyTree(CLIPBOARD);
477 |
478 | const p = findParent(node, TREE);
479 | if (!p) {
480 | TREE = newTree;
481 | } else {
482 | replaceChild(p, node, newTree);
483 | }
484 |
485 | measureTree(ctx, TREE);
486 | }
487 |
488 | requestDraw();
489 | };
490 |
491 | cbCnv.addEventListener('click', takeClipboard);
492 |
493 | // display prompt
494 | promptText(node.name, 'Enter text', function(name) {
495 | node.name = name;
496 | node.textWidth = null;
497 | measureTree(ctx, TREE);
498 | cbCnv.removeEventListener('click', takeClipboard);
499 |
500 | EDITING_NODE = null;
501 | requestDraw();
502 | }, function() {
503 | EDITING_NODE = null;
504 | });
505 | };
506 |
507 | const longPress = function() {
508 | const node = TOUCH_NODE;
509 |
510 | if (!node || node.handle) {
511 | return;
512 | }
513 |
514 | // long press: delete/cut
515 | const p = findParent(node, TREE);
516 |
517 | if (!p) {
518 | // don't cut root
519 | return;
520 | }
521 |
522 | removeChild(p, node);
523 | CLIPBOARD = node;
524 |
525 | TOUCH_NODE = null;
526 | TOUCH_BEGAN = null;
527 |
528 | requestDraw();
529 | };
530 |
531 | //// zooming
532 | const changeZoom = function({ox1, oy1, ox2, oy2, nx1, ny1, nx2, ny2}) {
533 | // "real" locations of the original zooming points
534 | const x1r = (ox1 - SCROLL.x) / ZOOM.z;
535 | const y1r = (oy1 - SCROLL.y) / ZOOM.z;
536 | const x2r = (ox2 - SCROLL.x) / ZOOM.z;
537 | const y2r = (oy2 - SCROLL.y) / ZOOM.z;
538 | const dxr = x1r - x2r;
539 | const dyr = y1r - y2r;
540 |
541 | // old distance
542 | const rd2 = dxr * dxr + dyr * dyr;
543 | // new distance
544 | const ndx = nx1 - nx2;
545 | const ndy = ny1 - ny2;
546 | const nd2 = ndx * ndx + ndy * ndy;
547 | // desired new zoom
548 | const z = Math.min(1, Math.sqrt(nd2 / rd2));
549 | ZOOM.tz = z / ZOOM.z;
550 |
551 | // "real" location of original center
552 | const cxr = (x1r + x2r) / 2;
553 | const cyr = (y1r + y2r) / 2;
554 | // new center
555 | const ncx = (nx1 + nx2) / 2;
556 | const ncy = (ny1 + ny2) / 2;
557 |
558 | // desired new scroll
559 | const sx = ncx - cxr * z;
560 | const sy = ncy - cyr * z;
561 | SCROLL.tx = sx - SCROLL.x;
562 | SCROLL.ty = sy - SCROLL.y;
563 | };
564 |
565 | const changeZoomMouse = function({delta, cx, cy}) {
566 | // desired new zoom
567 | const z = Math.min(1, ZOOM.z * Math.pow(2, delta / 100));
568 |
569 | // "real" location of original center
570 | const cxr = (cx - SCROLL.x) / ZOOM.z;
571 | const cyr = (cy - SCROLL.y) / ZOOM.z;
572 |
573 | // desired new scroll
574 | const sx = cx - cxr * z;
575 | const sy = cy - cyr * z;
576 | SCROLL.x = sx;
577 | SCROLL.y = sy;
578 | ZOOM.z = z;
579 | };
580 |
581 | const finishZoom = function() {
582 | SCROLL.x += SCROLL.tx;
583 | SCROLL.y += SCROLL.ty;
584 | SCROLL.tx = 0;
585 | SCROLL.ty = 0;
586 | ZOOM.z *= ZOOM.tz;
587 | ZOOM.tz = 1;
588 | };
589 |
590 | //// tree manipulation
591 | const fontSize = 24;
592 | const lineHeight = fontSize * 2;
593 |
594 | /*
595 | const recursiveCall =
596 | {name: '+',
597 | children: [
598 | {name: 'fib is the name of this thing',
599 | children: [
600 | {name: '-',
601 | children: [
602 | {name: 'n'},
603 | {name: '1'},
604 | ]},
605 | {name: '-',
606 | children: [
607 | {name: 'n'},
608 | {name: '2'},
609 | ]},
610 | ]},
611 | ]
612 | };
613 |
614 | const defunFib =
615 | {name: 'defun',
616 | children: [
617 | {name: 'fib'},
618 | {name: '()',
619 | children: [
620 | {name: 'x'},
621 | ]},
622 | {name: 'if',
623 | children: [
624 | {name: '<',
625 | children: [
626 | {name: 'n'},
627 | {name: '2'},
628 | ]},
629 | {name: '1'},
630 | recursiveCall,
631 | ]},
632 | ]
633 | };
634 | */
635 |
636 | let CLIPBOARD = null;
637 | let TREE = {name: '', children: []};
638 | let SCROLL = {x: 100.5, y: 100.5, tx: 0, ty: 0};
639 | let ZOOM = {z: 1, tz: 1};
640 |
641 | const measureTree = function(ctx, tree) {
642 | const measureTextWidth = function(text) {
643 | ctx.font = `${fontSize}px monospace`;
644 | return ctx.measureText(text).width;
645 | };
646 |
647 | if (typeof tree.textWidth != 'number') {
648 | tree.textWidth = measureTextWidth(tree.name);
649 | }
650 |
651 | let nameWidth = Math.max(lineHeight, tree.textWidth + fontSize);
652 | if (typeof tree.slidOver == 'number') {
653 | nameWidth = Math.abs(tree.slidOver);
654 | }
655 | tree.childrenWidth = 0;
656 | if (tree.children) {
657 | tree.children.forEach(function(child) {
658 | measureTree(ctx, child);
659 | tree.childrenWidth += child.width;
660 | });
661 | }
662 |
663 | if (nameWidth <= tree.childrenWidth) {
664 | tree.width = tree.childrenWidth;
665 | } else {
666 | tree.width = nameWidth;
667 |
668 | if (tree.children) {
669 | widenTree(tree);
670 | }
671 | }
672 | };
673 |
674 | const widenTree = function(tree) {
675 | if (tree.childrenWidth < tree.width && tree.children) {
676 | let nonSlidingChildren = 0;
677 |
678 | tree.children.forEach(function(child) {
679 | if (typeof child.slidOver != 'number') {
680 | ++ nonSlidingChildren;
681 | }
682 | });
683 | const adjust = (tree.width - tree.childrenWidth) / nonSlidingChildren;
684 |
685 | tree.children.forEach(function(child) {
686 | if (typeof child.slidOver != 'number') {
687 | child.width += adjust;
688 | widenTree(child);
689 | }
690 | });
691 | tree.childrenWidth += adjust * nonSlidingChildren;
692 | }
693 | };
694 |
695 | const drawTree = function(tree, x, y, idx, depth, layers, layerSolid, layerLines) {
696 | if (tree == TOUCH_NODE) {
697 | if (tree.slideUnder) {
698 | layerSolid = layers.bgSolid;
699 | layerLines = layers.bgLines;
700 | } else {
701 | x += DRAG_FEEL_X;
702 | y += DRAG_FEEL_Y;
703 | layerSolid = layers.fgSolid;
704 | layerLines = layers.fgLines;
705 | }
706 | }
707 |
708 | let height = lineHeight;
709 | if (typeof tree.slidOut == 'number') {
710 | height = tree.slidOut;
711 | }
712 |
713 | if (tree.children) {
714 | let childXOffset = 0;
715 | tree.children.forEach(function(child, childIdx) {
716 | drawTree(child, x + childXOffset, y - height, childIdx, depth + 1,
717 | layers, layerSolid, layerLines);
718 | childXOffset += child.width;
719 | });
720 | }
721 |
722 | if (!tree.children || tree.children.length == 0) {
723 | // handle
724 | layerSolid.push(
725 | {op: 'strokeRect', strokeStyle: '#f0f0f0', lineWidth: 3,
726 | x: x + 1,
727 | y: y - height - lineHeight + 1,
728 | w: tree.width-2,
729 | h: lineHeight-2
730 | });
731 |
732 | // highlight handle if it is being dragged
733 | if (TOUCH_NODE && TOUCH_NODE.handle &&
734 | (TOUCH_NODE.p == tree ||
735 | (TOUCH_NODE.p.children && TOUCH_NODE.p.children[0] == tree))) {
736 | layers.fgLines.push(
737 | {op: 'strokeRect', strokeStyle: 'black',
738 | lineWidth: 3,
739 | x: x,
740 | y: y - height - lineHeight,
741 | w: tree.width,
742 | h: lineHeight
743 | });
744 | }
745 | }
746 |
747 | // main body of the node
748 | const boxFillStyle = depth % 2 == 0 ? '#f0f0f0' : '#e0e0e0';
749 | layerSolid.push(
750 | {op: 'fillRect', fillStyle: boxFillStyle,
751 | x: x,
752 | y: y - height,
753 | w: tree.width,
754 | h: height
755 | });
756 |
757 | // text label
758 | layerLines.push(
759 | {op: 'fillText', fillStyle: 'black',
760 | font: `${fontSize}px monospace`,
761 | textAlign: 'center',
762 | textBaseline: 'middle',
763 | msg: tree.name,
764 | cx: x + tree.width / 2,
765 | cy: y - height + lineHeight / 2
766 | });
767 |
768 | // dividing line
769 | layerLines.push(
770 | {op: 'stroke', strokeStyle: 'black',
771 | lineWidth: 1,
772 | path: [[{x: x + tree.width, y: y - height},
773 | {x: x + tree.width, y: y}]]
774 | });
775 |
776 | // highlight node if it is being dragged
777 | // or if this is a new node and it is locked in (slid completely)
778 | if (tree == TOUCH_NODE || tree == EDITING_NODE ||
779 | (tree == NEW_NODE &&
780 | (tree.slidOver == lineHeight || tree.slidOut == lineHeight))) {
781 | layers.fgLines.push(
782 | {op: 'strokeRect', strokeStyle: 'black',
783 | lineWidth: 3,
784 | x: x,
785 | y: y - height,
786 | w: tree.width,
787 | h: height
788 | });
789 | }
790 | };
791 |
792 | const renderLayer = function(ctx, layer, sx, sy, z) {
793 | layer.forEach(function(cmd) {
794 | switch (cmd.op) {
795 | case 'fillText':
796 | ctx.save();
797 | ctx.fillStyle = cmd.fillStyle;
798 | ctx.font = cmd.font;
799 | ctx.textAlign = cmd.textAlign;
800 | ctx.textBaseline = cmd.textBaseline;
801 |
802 | ctx.scale(z, z);
803 | ctx.translate(cmd.cx + sx / z, cmd.cy + sy / z);
804 | ctx.fillText(cmd.msg, 0, 0);
805 | ctx.restore();
806 | break;
807 | case 'fillRect':
808 | ctx.fillStyle = cmd.fillStyle;
809 | ctx.fillRect(cmd.x * z + sx, cmd.y * z + sy, cmd.w * z, cmd.h * z);
810 | break;
811 | case 'strokeRect':
812 | ctx.strokeStyle = cmd.strokeStyle;
813 | ctx.lineWidth = cmd.lineWidth * z;
814 | ctx.strokeRect(cmd.x * z + sx, cmd.y * z + sy, cmd.w * z, cmd.h * z);
815 | break;
816 | case 'stroke':
817 | ctx.beginPath();
818 | cmd.path.forEach(function(segment) {
819 | ctx.moveTo(segment[0].x * z + sx, segment[0].y * z + sy);
820 | for (let i = 1; i < segment.length; ++i) {
821 | ctx.lineTo(segment[i].x * z + sx, segment[i].y * z + sy);
822 | }
823 | });
824 | ctx.strokeStyle = cmd.strokeStyle;
825 | ctx.lineWidth = cmd.lineWidth * z;
826 | ctx.stroke();
827 | break;
828 | }
829 | });
830 | };
831 |
832 | const drawObjects = function(ctx) {
833 | measureTree(ctx, TREE);
834 |
835 | const layers =
836 | {
837 | bgSolid: [],
838 | bgLines: [],
839 | midSolid: [],
840 | midLines: [],
841 | fgSolid: [],
842 | fgLines: [],
843 | };
844 |
845 | drawTree(TREE, 0, 0, 0, 0, layers, layers.midSolid, layers.midLines);
846 |
847 | const sx = SCROLL.x + SCROLL.tx;
848 | const sy = SCROLL.y + SCROLL.ty;
849 | const z = ZOOM.z * ZOOM.tz;
850 |
851 | renderLayer(ctx, layers.bgSolid, sx, sy, z);
852 | renderLayer(ctx, layers.bgLines, sx, sy, z);
853 | renderLayer(ctx, layers.midSolid, sx, sy, z);
854 | renderLayer(ctx, layers.midLines, sx, sy, z);
855 | renderLayer(ctx, layers.fgSolid, sx, sy, z);
856 | renderLayer(ctx, layers.fgLines, sx, sy, z);
857 | };
858 |
859 | const drawClipboardObjects = function (ctx) {
860 | if (!CLIPBOARD) {
861 | return;
862 | }
863 |
864 | measureTree(cbCtx, CLIPBOARD);
865 | const depth = treeMaxDepth(CLIPBOARD, 1);
866 | const z = 0.5;
867 |
868 | const cw = Math.max(lineHeight, CLIPBOARD.width * z);
869 | const ch = Math.max(lineHeight, depth * lineHeight * z);
870 | resizeClipboard(cw, ch);
871 |
872 | cbCtx.fillStyle = 'white';
873 | cbCtx.fillRect(0, 0, CB_WIDE, CB_HIGH);
874 |
875 | const sx = 0;
876 | const sy = CB_HIGH;
877 |
878 | const layers = { midSolid: [], midLines: [] };
879 |
880 | drawTree(CLIPBOARD, 0, 0, 0, 0, layers, layers.midSolid, layers.midLines);
881 |
882 | renderLayer(ctx, layers.midSolid, sx, sy, z);
883 | renderLayer(ctx, layers.midLines, sx, sy, z);
884 | };
885 |
886 | const nodeAt = function(treeX, treeY, {x, y}, tree) {
887 | if (x >= treeX && x < treeX + tree.width &&
888 | y >= treeY - lineHeight && y < treeY) {
889 | return tree;
890 | }
891 |
892 | if (!tree.children || tree.children.length == 0) {
893 | // check for handle
894 | if (x >= treeX && x < treeX + tree.width &&
895 | y >= treeY - lineHeight * 2 && y < treeY - lineHeight) {
896 | return {handle: true, p: tree};
897 | }
898 | return null;
899 | }
900 |
901 | let childXOffset = 0;
902 | for (let i = 0; i < tree.children.length; ++i) {
903 | const child = tree.children[i];
904 | const result = nodeAt(treeX + childXOffset, treeY - lineHeight, {x, y}, child);
905 | if (result) {
906 | return result;
907 | }
908 | childXOffset += child.width;
909 | }
910 |
911 | return null;
912 | };
913 |
914 | const findParent = function(searchNode, tree) {
915 | if (!tree.children) {
916 | return null;
917 | }
918 | for (let i = 0; i < tree.children.length; ++i) {
919 | const child = tree.children[i];
920 | if (child == searchNode) {
921 | return tree;
922 | }
923 | const result = findParent(searchNode, child);
924 | if (result) {
925 | return result;
926 | }
927 | }
928 | return null;
929 | };
930 |
931 | const replaceChild = function(parentNode, oldNode, newNode) {
932 | if (!parentNode.children) {
933 | return;
934 | }
935 |
936 | for (let i = 0; i < parentNode.children.length; ++i) {
937 | const child = parentNode.children[i];
938 |
939 | if (child == oldNode) {
940 | parentNode.children[i] = newNode;
941 | return;
942 | }
943 | }
944 | };
945 |
946 | const removeChild = function(parentNode, node) {
947 | if (!parentNode.children) {
948 | return;
949 | }
950 |
951 | for (let i = 0; i < parentNode.children.length; ++i) {
952 | const child = parentNode.children[i];
953 |
954 | if (child == node) {
955 | parentNode.children.splice(i, 1);
956 | return;
957 | }
958 | }
959 | };
960 |
961 | const addSiblingBefore = function(parentNode, node, newNode) {
962 | if (!parentNode.children) {
963 | return;
964 | }
965 |
966 | for (let i = 0; i < parentNode.children.length; ++i) {
967 | const child = parentNode.children[i];
968 | if (child == node) {
969 | parentNode.children.splice(i, 0, newNode);
970 | return;
971 | }
972 | }
973 | };
974 |
975 | const addSiblingAfter = function(parentNode, node, newNode) {
976 | if (!parentNode.children) {
977 | return;
978 | }
979 |
980 | for (let i = 0; i < parentNode.children.length; ++i) {
981 | const child = parentNode.children[i];
982 | if (child == node) {
983 | parentNode.children.splice(i + 1, 0, newNode);
984 | return;
985 | }
986 | }
987 | };
988 |
989 | const copyTree = function(tree) {
990 | const newTree = {name: tree.name};
991 |
992 | if (tree.children) {
993 | newTree.children = [];
994 | tree.children.forEach(function(child) {
995 | newTree.children.push(copyTree(child));
996 | });
997 | }
998 |
999 | // all metrics get computed elsewhere
1000 | return newTree;
1001 | };
1002 |
1003 | const treeMaxDepth = function(tree, depth) {
1004 | let maxDepth = depth;
1005 |
1006 | if (tree.children && tree.children.length > 0) {
1007 | tree.children.forEach(function(child) {
1008 | const childDepth = treeMaxDepth(child, depth + 1);
1009 | maxDepth = Math.max(childDepth, maxDepth);
1010 | });
1011 | }
1012 | return maxDepth
1013 | };
1014 |
1015 |
1016 | //// kick off first draw
1017 | resize();
1018 | resizeClipboard(CB_WIDE, CB_HIGH);
1019 | })();
1020 |
--------------------------------------------------------------------------------
/src/touchy.js:
--------------------------------------------------------------------------------
1 | /* exported GET_TOUCHY */
2 |
3 | const GET_TOUCHY = function (elem, cb){
4 | 'use strict';
5 |
6 | let curTouches = [];
7 | let primaryIdx = -1;
8 | let pinchIdx1 = -1;
9 | let pinchIdx2 = -1;
10 |
11 | const startTouch = function (x, y, id, mouse) {
12 | const obj = { x, y };
13 | if (mouse) {
14 | obj.mouse = true;
15 | } else {
16 | obj.touch = id;
17 | }
18 |
19 | curTouches.push(obj);
20 | if (primaryIdx !== -1) {
21 | cb.touchCancel();
22 | }
23 |
24 | if (curTouches.length === 2 && (pinchIdx1 === -1 && pinchIdx2 === -1)) {
25 | pinchIdx1 = 0;
26 | pinchIdx2 = 1;
27 | primaryIdx = -1;
28 | cb.pinchStart(curTouches[pinchIdx1], curTouches[pinchIdx2]);
29 | } else {
30 | primaryIdx = curTouches.length - 1;
31 | cb.touchStart(obj);
32 | }
33 | }
34 |
35 | const updateTouch = function (idx, x, y) {
36 | const obj = curTouches[idx];
37 | obj.x = x;
38 | obj.y = y;
39 |
40 | if (idx === primaryIdx) {
41 | cb.touchMove(obj);
42 | }
43 | };
44 |
45 | const finishUpdateTouch = function () {
46 | if (curTouches.length === 2 && pinchIdx1 !== -1 && pinchIdx2 !== -1) {
47 | cb.pinchMove(curTouches[pinchIdx1], curTouches[pinchIdx2]);
48 | }
49 | };
50 |
51 | const endTouch = function (idx, x, y, cancelled = false) {
52 | if (idx === primaryIdx) {
53 | if (cancelled) {
54 | cb.touchCancel();
55 | } else {
56 | cb.touchEnd({x,y});
57 | }
58 | primaryIdx = -1;
59 | } else if (primaryIdx !== -1 && primaryIdx > idx) {
60 | primaryIdx -= 1;
61 | }
62 |
63 | // slight inaccuracy here as the other pinch point may not have had a chance
64 | // to update yet this event
65 | if (idx === pinchIdx1) {
66 | if (pinchIdx2 !== -1) {
67 | cb.pinchEnd(curTouches[pinchIdx1], curTouches[pinchIdx2]);
68 | }
69 | pinchIdx1 = -1;
70 | } else if (pinchIdx1 !== -1 && pinchIdx1 > idx) {
71 | pinchIdx1 -= 1;
72 | }
73 |
74 | if (idx === pinchIdx2) {
75 | if (pinchIdx1 !== -1) {
76 | cb.pinchEnd(curTouches[pinchIdx1], curTouches[pinchIdx2]);
77 | }
78 | pinchIdx2 = -1;
79 | } else if (pinchIdx2 !== -1 && pinchIdx2 > idx) {
80 | pinchIdx2 -= 1;
81 | }
82 |
83 | curTouches.splice(idx, 1);
84 | }
85 |
86 | const touchIdx = function (id) {
87 | for (let i = 0; i < curTouches.length; i ++) {
88 | if (curTouches[i].touch === id) {
89 | return i;
90 | }
91 | }
92 |
93 | return -1;
94 | };
95 |
96 | const mouseIdx = function () {
97 | for (let i = 0; i < curTouches.length; i ++) {
98 | if (curTouches[i].mouse) {
99 | return i;
100 | }
101 | }
102 |
103 | return -1;
104 | };
105 |
106 | let sawPointerEvent = false;
107 |
108 | const handleTouchStart = function (e) {
109 | e.preventDefault();
110 | e.stopPropagation();
111 |
112 | for (let i = 0; i < e.changedTouches.length; i++) {
113 | const t = e.changedTouches[i];
114 | startTouch(t.pageX, t.pageY, t.identifier);
115 | }
116 | };
117 |
118 | const handlePointerDown = function (e) {
119 | e.preventDefault();
120 | e.stopPropagation();
121 | if (e.preventManipulation) {
122 | e.preventManipulation();
123 | }
124 |
125 | if (!sawPointerEvent) {
126 | sawPointerEvent = true;
127 | curTouches.splice(0, curTouches.length);
128 | elem.removeEventListener('touchstart', handleTouchStart, {passive: false});
129 | elem.removeEventListener('touchmove', handleTouchMove, {passive: false});
130 | elem.removeEventListener('touchend', handleTouchEnd, {passive: false});
131 | elem.removeEventListener('touchcancel', handleTouchCancel, {passive: false});
132 | }
133 |
134 | startTouch(e.pageX, e.pageY, e.pointerId);
135 | };
136 |
137 | const handleMouseDown = function (e) {
138 | if (e.button !== 0) {
139 | return;
140 | }
141 |
142 | e.preventDefault();
143 | e.stopPropagation();
144 |
145 | startTouch(e.pageX, e.pageY, null, true);
146 | };
147 |
148 | const handleTouchMove = function (e) {
149 | e.preventDefault();
150 | e.stopPropagation();
151 |
152 | for (let i = 0; i < e.changedTouches.length; i++) {
153 | const t = e.changedTouches[i];
154 | const idx = touchIdx(t.identifier);
155 | if (idx === -1) {
156 | continue;
157 | }
158 |
159 | updateTouch(idx, t.pageX, t.pageY);
160 | }
161 |
162 | finishUpdateTouch();
163 | };
164 |
165 | const handlePointerMove = function (e) {
166 | e.preventDefault();
167 | e.stopPropagation();
168 | if (e.preventManipulation) {
169 | e.preventManipulation();
170 | }
171 |
172 | const idx = touchIdx(e.pointerId);
173 | if (idx !== -1) {
174 | updateTouch(idx, e.pageX, e.pageY);
175 | }
176 |
177 | finishUpdateTouch();
178 | };
179 |
180 | const handleMouseMove = function (e) {
181 | const idx = mouseIdx();
182 |
183 | if (idx === -1) {
184 | return;
185 | }
186 |
187 | e.preventDefault();
188 | e.stopPropagation();
189 |
190 | updateTouch(idx, e.pageX, e.pageY);
191 | };
192 |
193 | const handleTouchEnd = function (e) {
194 | e.preventDefault();
195 | e.stopPropagation();
196 |
197 | for (let i = 0; i < e.changedTouches.length; i++) {
198 | const t = e.changedTouches[i];
199 | const idx = touchIdx(t.identifier);
200 | if (idx === -1) {
201 | continue;
202 | }
203 |
204 | endTouch(idx, t.pageX, t.pageY);
205 | }
206 | };
207 |
208 | const handlePointerUp = function (e) {
209 | e.preventDefault();
210 | e.stopPropagation();
211 | if (e.preventManipulation) {
212 | e.preventManipulation();
213 | }
214 |
215 | const idx = touchIdx(e.pointerId);
216 | if (idx !== -1) {
217 | endTouch(idx, e.pageX, e.pageY);
218 | }
219 | };
220 |
221 | const handleMouseUp = function (e) {
222 | if (e.button !== 0) {
223 | return;
224 | }
225 |
226 | const idx = mouseIdx();
227 |
228 | if (idx === -1) {
229 | return;
230 | }
231 |
232 | e.preventDefault();
233 | e.stopPropagation();
234 |
235 | endTouch(idx, e.pageX, e.pageY);
236 | };
237 |
238 | const handleTouchCancel = function (e) {
239 | const touches = e.changedTouches;
240 |
241 | for (let i = 0; i < e.changedTouches.length; i++) {
242 | const t = e.changedTouches[i];
243 | const idx = touchIdx(t.identifier);
244 | if (idx === -1) {
245 | continue;
246 | }
247 |
248 | endTouch(idx, t.pageX, t.pageY, true);
249 | }
250 | };
251 |
252 | const handlePointerOut = function (e) {
253 | const idx = touchIdx(e.pointerId);
254 |
255 | if (idx !== -1) {
256 | endTouch(idx, e.pageX, e.pageY, true);
257 | }
258 | };
259 |
260 | const handleMouseLeave = function (e) {
261 | const idx = mouseIdx();
262 |
263 | if (idx === -1) {
264 | return;
265 | }
266 |
267 | e.stopPropagation();
268 |
269 | endTouch(idx, e.pageX, e.pageY, true);
270 | };
271 |
272 | elem.addEventListener('touchstart', handleTouchStart, {passive: false});
273 | elem.addEventListener('touchmove', handleTouchMove, {passive: false});
274 | elem.addEventListener('touchend', handleTouchEnd, {passive: false});
275 | elem.addEventListener('touchcancel', handleTouchCancel, {passive: false});
276 |
277 | elem.addEventListener('pointerdown', handlePointerDown, {passive: false});
278 | elem.addEventListener('pointermove', handlePointerMove, {passive: false});
279 | elem.addEventListener('pointerup', handlePointerUp, {passive: false});
280 | elem.addEventListener('pointerout', handlePointerOut, {passive: false});
281 |
282 | elem.addEventListener('mousedown', handleMouseDown, {passive: false});
283 | elem.addEventListener('mousemove', handleMouseMove, {passive: false});
284 | elem.addEventListener('mouseup', handleMouseUp, {passive: false});
285 | elem.addEventListener('mouseleave', handleMouseLeave, {passive: false});
286 |
287 | return curTouches;
288 |
289 | };
290 |
--------------------------------------------------------------------------------
/tiramisu_ss.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hcs64/tiramisu/96f1c82eef0a10b7b175553cf67bfd9fbaeae721/tiramisu_ss.png
--------------------------------------------------------------------------------