git @ Cat's Eye Technologies Wunnel / 8dc4364
Merge pull request #2 from catseye/develop-1.0-2018.01 Develop 1.0 2018.01 Chris Pressey authored 7 years ago GitHub committed 7 years ago
13 changed file(s) with 1471 addition(s) and 743 deletion(s). Raw diff Collapse all Expand all
22 <meta charset="utf-8">
33 <title>Wunnel</title>
44 <style>
5 #program { display: none; }
6 #load { display: none; }
5 #display_container {
6 display: flex;
7 }
78 #program_display {
8 color: white;
9 background: blue;
10 border: 3px solid blue;
11 font-family: monospace;
12 float: left;
9 color: white;
10 background: blue;
11 border: 3px solid blue;
12 font-family: monospace;
13 display: inline-block;
14 flex: 1 1 auto;
15 }
16 textarea {
17 display: inline-block;
18 flex: 1 1 auto;
1319 }
1420 #state_display {
15 float: left;
16 margin-left: 1em;
21 margin-left: 1em;
22 display: inline-block;
23 flex: 0 1 auto;
1724 }
1825 #op_table_display {
19 color: black;
20 background: white;
21 border: 3px solid red;
22 font-family: monospace;
23 text-align: center;
26 color: black;
27 background: white;
28 border: 3px solid red;
29 font-family: monospace;
30 text-align: center;
2431 }
2532 #tape_display {
26 border: 1px solid black;
33 border: 1px solid black;
2734 }
2835 #output {
29 color: white;
30 background: #008000;
31 border: 3px solid #008000;
32 font-family: monospace;
36 color: white;
37 background: #008000;
38 border: 3px solid #008000;
39 font-family: monospace;
3340 }
34 #canvas { border: 1px solid blue; display: none; }
41 #canvas {
42 border: 1px solid blue; display: none;
43 }
3544 </style>
3645 </head>
3746 <body>
3847
3948 <h1>Wunnel</h1>
4049
41 <button id="load">Load</button>
42 <button id="edit">Edit</button>
43 <button id="start">Start</button>
44 <button id="stop">Stop</button>
45 <button id="reset">Reset</button>
46 <button id="step">Step</button>
47 Speed: <input id="speed" type="range" min="0" max="200" value="0" />
48
49 <div>
50 example source: <select id="select_source"></select>
51 </div>
52
53 <div id="display_container">
54 <pre id="program_display"></pre>
55 <div id="state_display">
56 <pre id="op_table_display"></pre>
57 Tape: <canvas id="tape_display" width="400" height="100"></canvas><br />
58 Input: <input id="input"></input><br />
59 Output: <div id="output"></div>
60 </div>
61 </div>
62
63 <textarea id="program" rows="25" cols="40"></textarea>
50 <div id="container"></div>
6451
6552 </body>
66 <script src="../src/yoob/playfield.js"></script>
67 <script src="../src/yoob/playfield-html-view.js"></script>
68 <script src="../src/yoob/cursor.js"></script>
69 <script src="../src/yoob/tape.js"></script>
70 <script src="../src/yoob/controller.js"></script>
71 <script src="../src/yoob/preset-manager.js"></script>
72 <script src="../src/wunnel-controller.js"></script>
53 <script src="../src/wunnel-launcher.js"></script>
7354 <script src="../../../eg/index.js"></script>
7455 <script>
75 var programView = new yoob.PlayfieldHTMLView().init(
76 null, document.getElementById('program_display')
77 );
78 var opTableView = new yoob.PlayfieldHTMLView().init(
79 null, document.getElementById('op_table_display')
80 );
81 opTableView.render = function(value) {
82 return ' ' + value + ' ';
83 };
84 var c = (new WunnelController()).init({
85 programView: programView,
86 opTableView: opTableView,
87 tapeCanvas: document.getElementById("tape_display"),
88 inputElem: document.getElementById("input"),
89 outputElem: document.getElementById("output")
90 });
91 c.connect({
92 'start': 'start',
93 'stop': 'stop',
94 'reset': 'reset',
95 'step': 'step',
96 'load': 'load',
97 'edit': 'edit',
98 'speed': 'speed',
99 'source': 'program',
100 'display': 'display_container'
101 });
102 c.click_load();
103 var p = (new yoob.PresetManager()).init({
104 selectElem: document.getElementById('select_source'),
105 controller: c
106 });
107 function makeCallback(sourceText) {
108 return function(id) {
109 c.loadSource(sourceText);
110 }
111 }
112 for (var i = 0; i < examplePrograms.length; i++) {
113 p.add(examplePrograms[i][0], makeCallback(examplePrograms[i][1]));
114 }
115 p.select(examplePrograms[0][0]);
56 launch('../src/', 'container');
11657 </script>
+0
-184
impl/wunnel.js/src/wunnel-controller.js less more
0 /*
1 * requires yoob.Controller
2 * requires yoob.Playfield
3 * requires yoob.Cursor
4 * requires yoob.Tape
5 */
6 function WunnelPlayfield() {
7 this.setDefault(' ');
8
9 this.inBounds = function(x, y) {
10 return (x >= this.minX && x <= this.maxX &&
11 y >= this.minY && y <= this.maxY);
12 };
13 };
14 WunnelPlayfield.prototype = new yoob.Playfield();
15
16
17 function OperationTable() {
18 this.init = function() {
19 this.clear();
20 var table =
21 "RRS-+N\n" +
22 "<S>0N0\n" +
23 ">I<N+-\n" +
24 "NOSS<E\n" +
25 "SEN>SE\n" +
26 "RNRRRR\n";
27 var map = {
28 'R': 'ROT',
29 'S': 'SHU',
30 'N': 'NOP',
31 '+': 'PLU',
32 '-': 'NEG',
33 '0': 'BLA',
34 '<': 'LEF',
35 '>': 'RIG',
36 'E': 'END',
37 'O': 'OUT',
38 'I': 'INP'
39 };
40 this.load(0, 0, table, function(x) { return map[x] });
41 };
42 };
43 OperationTable.prototype = new yoob.Playfield();
44
45
46 function OpTableCursor() {
47 this.advance = function() {
48 this.x += this.dx;
49 if (this.x < 0) this.x = 5;
50 if (this.x > 5) this.x = 0;
51 this.y += this.dy;
52 if (this.y < 0) this.y = 5;
53 if (this.y > 5) this.y = 0;
54 };
55 };
56 OpTableCursor.prototype = new yoob.Cursor();
57
58
59 function WunnelController() {
60 var pf;
61 var ip;
62
63 var optab;
64 var opp;
65
66 var tape;
67 var head;
68
69 this.init = function(cfg) {
70 this.programView = cfg.programView;
71 this.opTableView = cfg.opTableView;
72 this.tapeCanvas = cfg.tapeCanvas;
73 this.inputElem = cfg.inputElem;
74 this.outputElem = cfg.outputElem;
75
76 pf = new WunnelPlayfield();
77 ip = new yoob.Cursor(0, 0, 1, 1);
78 this.programView.pf = pf;
79 this.programView.setCursors([ip]);
80
81 optab = new OperationTable();
82 optab.init();
83 opp = new OpTableCursor(0, 0, 1, 1);
84 this.opTableView.pf = optab;
85 this.opTableView.setCursors([opp]);
86
87 return this;
88 };
89
90 this.positiveGenus = "0689@%&QROPADBqeopadb";
91
92 this.genusMoreThanZero = function(c) {
93 for (var i = 0; i < this.positiveGenus.length; i++) {
94 if (this.positiveGenus.charAt(i) === c)
95 return true;
96 }
97 return false;
98 };
99
100 this.step = function() {
101 var instruction = pf.get(ip.x, ip.y);
102 var k = optab.get(opp.x, opp.y);
103
104 if (this.genusMoreThanZero(instruction)) {
105 if (k === 'END') {
106 return 'stop';
107 } else if (k === 'NOP') {
108 } else if (k === 'SHU') {
109 if (ip.isHeaded(-1, 0)) {
110 ip.setY(ip.getY() - tape.get(head));
111 } else if (ip.isHeaded(1, 0)) {
112 ip.setY(ip.getY() + tape.get(head));
113 } else if (ip.isHeaded(0, -1)) {
114 ip.setX(ip.getX() + tape.get(head));
115 } else if (ip.isHeaded(0, 1)) {
116 ip.setX(ip.getX() - tape.get(head));
117 }
118 } else if (k === 'ROT') {
119 ip.rotateCounterclockwise();
120 ip.rotateCounterclockwise();
121 opp.rotateCounterclockwise();
122 opp.rotateCounterclockwise();
123 } else if (k === 'LEF') {
124 head--;
125 } else if (k === 'RIG') {
126 head++;
127 } else if (k === 'NEG') {
128 tape.put(head, -1);
129 } else if (k === 'BLA') {
130 tape.put(head, 0);
131 } else if (k === 'PLU') {
132 tape.put(head, 1);
133 } else if (k === 'OUT') {
134 this.outputElem.innerHTML += (tape.get(head) === 0 ? '0' : '1');
135 } else if (k === 'INP') {
136 var c = this.inputElem.value;
137 if (c === '') {
138 return 'block';
139 }
140 tape.put(head, c.charAt(0) === '1' ? 1 : 0);
141 this.inputElem.value = c.substr(1);
142 }
143 } else {
144 opp.advance();
145 }
146
147 ip.advance();
148 if (!pf.inBounds(ip.x, ip.y)) {
149 return 'stop';
150 }
151
152 this.draw();
153 };
154
155 this.load = function(text) {
156 pf.clear();
157 pf.load(0, 0, text);
158 ip.x = 0;
159 ip.y = 0;
160 ip.dx = 0;
161 ip.dy = 1;
162
163 opp.x = 0;
164 opp.y = 0;
165 opp.dx = 0;
166 opp.dy = 1;
167
168 tape = new yoob.Tape();
169 head = 0;
170
171 this.inputElem.value = "";
172 this.outputElem.innerHTML = "";
173
174 this.draw();
175 };
176
177 this.draw = function() {
178 this.programView.draw();
179 this.opTableView.draw();
180 tape.drawCanvas(this.tapeCanvas, 12, 12, []);
181 };
182 };
183 WunnelController.prototype = new yoob.Controller();
0 function launch(prefix, container, config) {
1 if (typeof container === 'string') {
2 container = document.getElementById(container);
3 }
4 config = config || {};
5 var deps = [
6 "yoob/element-factory.js",
7 "yoob/playfield.js",
8 "yoob/playfield-html-view.js",
9 "yoob/cursor.js",
10 "yoob/tape.js",
11 "yoob/tape-html-view.js",
12 "yoob/controller.js",
13 "yoob/source-manager.js",
14 "yoob/preset-manager.js",
15 "wunnel.js"
16 ];
17 var loaded = 0;
18 var onload = function() {
19 if (++loaded < deps.length) return;
20 /* ----- launch, phase 1: create the UI ----- */
21 var controlPanel = yoob.makeDiv(container);
22 controlPanel.id = "panel_container";
23
24 var subPanel = yoob.makeDiv(container);
25 var selectSource = yoob.makeSelect(subPanel, 'example source:', []);
26
27 var displayContainer = yoob.makeDiv(container);
28 displayContainer.id = 'display_container';
29
30 var programDisplay = yoob.makePre(displayContainer);
31 programDisplay.id = 'program_display';
32
33 var editor = yoob.makeTextArea(displayContainer, 40, 25);
34
35 var stateDisplay = yoob.makeDiv(displayContainer);
36 stateDisplay.id = "state_display";
37
38 var opTableDisplay = yoob.makePre(stateDisplay);
39 opTableDisplay.id = 'op_table_display';
40
41 var tapeSubDisplay = yoob.makeDiv(stateDisplay);
42 yoob.makeSpan(tapeSubDisplay, "Tape:");
43 var tapeDisplay = yoob.makeSpan(tapeSubDisplay);
44
45 var ioSubDisplay = yoob.makeDiv(stateDisplay);
46 ioSubDisplay.innerHTML = 'Input: <input id="input"></input><br />' +
47 'Output: <div id="output">';
48
49 var programView = new yoob.PlayfieldHTMLView().init({
50 element: programDisplay
51 });
52 var opTableView = new yoob.PlayfieldHTMLView().init({
53 element: opTableDisplay
54 });
55 opTableView.render = function(value) {
56 return ' ' + value + ' ';
57 };
58 var tapeView = new yoob.TapeHTMLView().init({
59 element: tapeDisplay
60 });
61
62 /* ----- launch, phase 2: connect the controller ----- */
63 var WunnelController = getWunnelControllerClass();
64 var controller = (new WunnelController()).init({
65 programView: programView,
66 opTableView: opTableView,
67 tapeView: tapeView,
68 inputElem: document.getElementById("input"),
69 outputElem: document.getElementById("output"),
70 panelContainer: controlPanel
71 });
72
73 var sourceManager = (new yoob.SourceManager()).init({
74 panelContainer: controlPanel,
75 editor: editor,
76 hideDuringEdit: [programDisplay],
77 disableDuringEdit: [controller.panel],
78 storageKey: 'wunnel.js',
79 onDone: function() {
80 controller.performReset(this.getEditorText());
81 }
82 });
83 var p = (new yoob.PresetManager()).init({
84 selectElem: selectSource,
85 controller: controller
86 });
87 function makeCallback(sourceText) {
88 return function(id) {
89 sourceManager.loadSource(sourceText);
90 }
91 }
92 for (var i = 0; i < examplePrograms.length; i++) {
93 p.add(examplePrograms[i][0], makeCallback(examplePrograms[i][1]));
94 }
95 p.select(examplePrograms[0][0]);
96 };
97 for (var i = 0; i < deps.length; i++) {
98 var elem = document.createElement('script');
99 elem.src = prefix + deps[i];
100 elem.onload = onload;
101 document.body.appendChild(elem);
102 }
103 }
0 /*
1 * requires yoob.Controller
2 * requires yoob.Playfield
3 * requires yoob.Cursor
4 * requires yoob.Tape
5 */
6
7 function getWunnelControllerClass() {
8
9 function WunnelPlayfield() {
10 this.setDefault(' ');
11
12 this.inBounds = function(x, y) {
13 return (x >= this.minX && x <= this.maxX &&
14 y >= this.minY && y <= this.maxY);
15 };
16 };
17 WunnelPlayfield.prototype = new yoob.Playfield();
18
19
20 function OperationTable() {
21 this.init = function() {
22 this.clear();
23 var table =
24 "RRS-+N\n" +
25 "<S>0N0\n" +
26 ">I<N+-\n" +
27 "NOSS<E\n" +
28 "SEN>SE\n" +
29 "RNRRRR\n";
30 var map = {
31 'R': 'ROT',
32 'S': 'SHU',
33 'N': 'NOP',
34 '+': 'PLU',
35 '-': 'NEG',
36 '0': 'BLA',
37 '<': 'LEF',
38 '>': 'RIG',
39 'E': 'END',
40 'O': 'OUT',
41 'I': 'INP'
42 };
43 this.load(0, 0, table, function(x) { return map[x] });
44 };
45 };
46 OperationTable.prototype = new yoob.Playfield();
47
48
49 function OpTableCursor() {
50 this.advance = function() {
51 this.x += this.dx;
52 if (this.x < 0) this.x = 5;
53 if (this.x > 5) this.x = 0;
54 this.y += this.dy;
55 if (this.y < 0) this.y = 5;
56 if (this.y > 5) this.y = 0;
57 };
58 };
59 OpTableCursor.prototype = new yoob.Cursor();
60
61
62 var proto = new yoob.Controller();
63 function WunnelController() {
64 var pf;
65 var ip;
66
67 var optab;
68 var opp;
69
70 var tape;
71 var head;
72
73 this.init = function(cfg) {
74 proto.init.apply(this, [cfg]);
75
76 this.programView = cfg.programView;
77 this.opTableView = cfg.opTableView;
78 this.tapeView = cfg.tapeView;
79 this.inputElem = cfg.inputElem;
80 this.outputElem = cfg.outputElem;
81
82 pf = new WunnelPlayfield();
83 ip = new yoob.Cursor(0, 0, 1, 1);
84 pf.setCursors([ip]);
85 this.programView.pf = pf;
86
87 optab = new OperationTable();
88 optab.init();
89 opp = new OpTableCursor(0, 0, 1, 1);
90 optab.setCursors([opp]);
91 this.opTableView.pf = optab;
92
93 return this;
94 };
95
96 this.positiveGenus = "0689@%&QROPADBqeopadb";
97
98 this.genusMoreThanZero = function(c) {
99 for (var i = 0; i < this.positiveGenus.length; i++) {
100 if (this.positiveGenus.charAt(i) === c)
101 return true;
102 }
103 return false;
104 };
105
106 this.step = function() {
107 var instruction = pf.get(ip.x, ip.y);
108 var k = optab.get(opp.x, opp.y);
109
110 if (this.genusMoreThanZero(instruction)) {
111 if (k === 'END') {
112 return 'stop';
113 } else if (k === 'NOP') {
114 } else if (k === 'SHU') {
115 if (ip.isHeaded(-1, 0)) {
116 ip.setY(ip.getY() - tape.read());
117 } else if (ip.isHeaded(1, 0)) {
118 ip.setY(ip.getY() + tape.read());
119 } else if (ip.isHeaded(0, -1)) {
120 ip.setX(ip.getX() + tape.read());
121 } else if (ip.isHeaded(0, 1)) {
122 ip.setX(ip.getX() - tape.read());
123 }
124 } else if (k === 'ROT') {
125 ip.rotateCounterclockwise();
126 ip.rotateCounterclockwise();
127 opp.rotateCounterclockwise();
128 opp.rotateCounterclockwise();
129 } else if (k === 'LEF') {
130 head.moveLeft();
131 } else if (k === 'RIG') {
132 head.moveRight();
133 } else if (k === 'NEG') {
134 tape.write(-1);
135 } else if (k === 'BLA') {
136 tape.write(0);
137 } else if (k === 'PLU') {
138 tape.write(1);
139 } else if (k === 'OUT') {
140 this.outputElem.innerHTML += (tape.read() === 0 ? '0' : '1');
141 } else if (k === 'INP') {
142 var c = this.inputElem.value;
143 if (c === '') {
144 return 'block';
145 }
146 tape.write(c.charAt(0) === '1' ? 1 : 0);
147 this.inputElem.value = c.substr(1);
148 }
149 } else {
150 opp.advance();
151 }
152
153 ip.advance();
154 if (!pf.inBounds(ip.x, ip.y)) {
155 return 'stop';
156 }
157
158 this.draw();
159 };
160
161 this.reset = function(text) {
162 pf.clear();
163 pf.load(0, 0, text);
164 ip.x = 0;
165 ip.y = 0;
166 ip.dx = 0;
167 ip.dy = 1;
168
169 opp.x = 0;
170 opp.y = 0;
171 opp.dx = 0;
172 opp.dy = 1;
173
174 head = (new yoob.Cursor()).init();
175 tape = (new yoob.Tape()).init({
176 cursors: [head]
177 });
178 this.tapeView.setTape(tape);
179
180 this.inputElem.value = "";
181 this.outputElem.innerHTML = "";
182
183 this.draw();
184 };
185
186 this.draw = function() {
187 this.programView.draw();
188 this.opTableView.draw();
189 this.tapeView.draw();
190 };
191 };
192 WunnelController.prototype = proto;
193
194 return WunnelController;
195 }
00 /*
1 * This file is part of yoob.js version 0.7-2015.0108
1 * This file is part of yoob.js version 0.12
22 * Available from https://github.com/catseye/yoob.js/
33 * This file is in the public domain. See http://unlicense.org/ for details.
44 */
1010 * convenience, we will refer to this as the _program state_, even though
1111 * it is of course highly adaptable and might not represent a "program".
1212 *
13 * The controller can be connected to a UI in the DOM, consisting of:
13 * Like most yoob objects, it is initialized after creation by calling the
14 * method `init` with a configuration object. If a DOM element is passed
15 * for `panelContainer` in the configuration, a panel containing a number
16 * of UI controls will be created and appended to that container. These
17 * are:
1418 *
1519 * - a set of buttons which control the evolution of the state:
1620 * - start
1721 * - stop
1822 * - step
1923 * - load
20 * - edit
2124 * - reset
2225 *
2326 * - a slider control which adjusts the speed of program state evolution.
24 *
25 * - a `source` element from which an program state can be loaded,
26 * and which is generally assumed to support user-editing of the source.
27 * The `edit` button will cause the `source` to be shown and the `display`
28 * to be hidden, while the `load` button will load the program state from
29 * the `source`, hide the `source`, and show the `display`.
30 *
31 * - a `display` element on which the current program state will be
32 * depicted. Note that the controller is not directly responsible for
33 * rendering the program state; use something like yoob.PlayfieldCanvasView
34 * for that instead. The controller only knows about the `display` in order
35 * to hide it while the `source` is being edited and to show it after the
36 * `source` has been loaded.
37 *
38 * - an `input` element, which provides input to the running program.
39 *
40 * Each of these is optional, and if not configured, will not be used.
4127 *
4228 * To use a Controller, create a subclass of yoob.Controller and override
4329 * the following methods:
4430 * - make it evolve the state by one tick in the step() method
45 * - make it load the state from a multiline string in the load() method
31 * - make it load the initial state from a string in the reset(s) method
4632 *
4733 * In these methods, you will need to store the state (in whatever
4834 * representation you find convenient for processing and for depicting on
5440 *
5541 * You should *not* store it in the `.state` attribute, as a yoob.Controller
5642 * uses this to track its own state (yes, it has its own state independent of
57 * the program state. at least potentially.)
43 * the program state.)
44 *
45 * Some theory of operation:
46 *
47 * For every action 'foo', three methods are exposed on the yoob.Controller
48 * object:
49 *
50 * - clickFoo
51 *
52 * Called when the button associated button is clicked.
53 * Client code may call this method to simulate the button having been
54 * clicked, including respecting and changing the state of the buttons panel.
55 *
56 * - performFoo
57 *
58 * Called by clickFoo to request the 'foo' action be performed.
59 * Responsible also for any Controller-related housekeeping involved with
60 * the 'foo' action. Client code may call this method when it wants the
61 * controller to perform this action without respecting or changing the
62 * state of the button panel.
63 *
64 * - foo
65 *
66 * Overridden (if necessary) by a subclass, or supplied by an instantiator,
67 * of yoob.Controller to implement some action. In particular, 'step' needs
68 * to be implemented this way. Client code should not call these methods
69 * directly.
70 *
71 * The clickFoo methods take one argument, an event structure. None of the
72 * other functions take an argument, with the exception of performReset() and
73 * reset(), which take a single argument, the text-encoded state to reset to.
5874 */
5975 yoob.Controller = function() {
6076 var STOPPED = 0; // the program has terminated (itself)
6278 var RUNNING = 2; // the program is running
6379 var BLOCKED = 3; // the program is waiting for more input
6480
65 this.intervalId = undefined;
66 this.delay = 100;
67 this.state = STOPPED;
68
69 this.source = undefined;
70 this.input = undefined;
71 this.display = undefined;
72
73 this.speed = undefined;
74 this.controls = {};
75
7681 /*
77 * This is not a public method.
78 */
79 this._makeEventHandler = function(control, key) {
80 if (this['click_' + key] !== undefined) {
81 key = 'click_' + key;
82 }
82 * panelContainer: an element into which to add the created button panel
83 * (if you do not give this, no panel will be created. You're on your own.)
84 * step: if given, if a function, it becomes the step() method on this
85 * reset: if given, if a function, it becomes the reset() method on this
86 */
87 this.init = function(cfg) {
88 this.delay = 100;
89 this.state = STOPPED;
90 this.controls = {};
91 this.resetState = undefined;
92 if (cfg.panelContainer) {
93 this.panel = this.makePanel();
94 cfg.panelContainer.appendChild(this.panel);
95 }
96 if (cfg.step) {
97 this.step = cfg.step;
98 }
99 if (cfg.reset) {
100 this.reset = cfg.reset;
101 }
102 return this;
103 };
104
105 /******************
106 * UI
107 */
108 this.makePanel = function() {
109 var panel = document.createElement('div');
83110 var $this = this;
84 return function(e) {
85 $this[key](control);
111
112 var makeEventHandler = function(control, upperAction) {
113 return function(e) {
114 $this['click' + upperAction](control);
115 };
86116 };
117
118 var makeButton = function(action) {
119 var button = document.createElement('button');
120 var upperAction = action.charAt(0).toUpperCase() + action.slice(1);
121 button.innerHTML = upperAction;
122 button.style.width = "5em";
123 panel.appendChild(button);
124 button.onclick = makeEventHandler(button, upperAction);
125 $this.controls[action] = button;
126 return button;
127 };
128
129 var keys = ["start", "stop", "step", "reset"];
130 for (var i = 0; i < keys.length; i++) {
131 makeButton(keys[i]);
132 }
133
134 var slider = document.createElement('input');
135 slider.type = "range";
136 slider.min = 0;
137 slider.max = 200;
138 slider.value = 100;
139 slider.onchange = function(e) {
140 $this.setDelayFrom(slider);
141 if ($this.intervalId !== undefined) {
142 $this.stop();
143 $this.start();
144 }
145 };
146
147 panel.appendChild(slider);
148 $this.controls.speed = slider;
149
150 return panel;
151 };
152
153 this.connectInput = function(elem) {
154 this.input = elem;
155 this.input.onchange = function(e) {
156 if (this.value.length > 0) {
157 // weird, where is this from?
158 $this.unblock();
159 }
160 }
87161 };
88162
89163 /*
90 * Single argument is a dictionary (object) where the keys
91 * are the actions a controller can undertake, and the values
92 * are either DOM elements or strings; if strings, DOM elements
93 * with those ids will be obtained from the document and used.
94 *
95 * When the button associated with e.g. 'start' is clicked,
96 * the corresponding method (in this case, 'click_start()')
97 * on this Controller will be called. These functions are
98 * responsible for changing the state of the Controller (both
99 * the internal state, and the enabled status, etc. of the
100 * controls), and for calling other methods on the Controller
101 * to implement the particulars of the action.
102 *
103 * For example, 'click_step()' calls 'performStep()' which
104 * calls 'step()' (which a subclass or instantiator must
105 * provide an implementation for.)
106 *
107 * To simulate one of the buttons being clicked, you may
108 * call 'click_foo()' yourself in code. However, that will
109 * be subject to the current restrictions of the interface.
110 * You may be better off calling one of the "internal" methods
111 * like 'performStep()'.
112 */
113 this.connect = function(dict) {
114 var $this = this;
115
116 var keys = ["start", "stop", "step", "load", "edit", "reset"];
117 for (var i in keys) {
118 var key = keys[i];
119 var value = dict[key];
120 if (typeof value === 'string') {
121 value = document.getElementById(value);
122 }
123 if (value) {
124 value.onclick = this._makeEventHandler(value, key);
125 this.controls[key] = value;
126 }
127 }
128
129 var keys = ["speed", "source", "input", "display"];
130 for (var i in keys) {
131 var key = keys[i];
132 var value = dict[key];
133 if (typeof value === 'string') {
134 value = document.getElementById(value);
135 }
136 if (value) {
137 this[key] = value;
138 // special cases
139 if (key === 'speed') {
140 this.speed.value = this.delay;
141 this.speed.onchange = function(e) {
142 $this.setDelayFrom($this.speed);
143 if ($this.intervalId !== undefined) {
144 $this.stop();
145 $this.start();
146 }
147 }
148 } else if (key === 'input') {
149 this.input.onchange = function(e) {
150 if (this.value.length > 0) {
151 $this.unblock();
152 }
153 }
154 }
155 }
156 }
157
158 this.click_stop();
159 };
160
161 this.click_step = function(e) {
164 * Override this to change how the delay is acquired from the 'speed'
165 * control.
166 */
167 this.setDelayFrom = function(elem) {
168 this.delay = elem.max - elem.value; // parseInt(elem.value, 10)
169 };
170
171 /******************
172 * action: Step
173 */
174 this.clickStep = function(e) {
162175 if (this.state === STOPPED) return;
163 this.click_stop();
176 this.clickStop();
164177 this.state = PAUSED;
165178 this.performStep();
179 };
180
181 this.performStep = function() {
182 var code = this.step();
183 if (code === 'stop') {
184 this.clickStop();
185 this.state = STOPPED;
186 } else if (code === 'block') {
187 this.state = BLOCKED;
188 }
166189 };
167190
168191 /*
173196 * - `block` to indicate that the program is waiting for more input.
174197 */
175198 this.step = function() {
176 alert("step() NotImplementedError");
177 };
178
179 this.performStep = function() {
180 var code = this.step();
181 if (code === 'stop') {
182 this.terminate();
183 } else if (code === 'block') {
184 this.state = BLOCKED;
185 }
186 };
187
188 this.click_load = function(e) {
189 this.click_stop();
190 this.load(this.source.value);
191 this.state = PAUSED;
192 if (this.controls.edit) this.controls.edit.style.display = "inline";
193 if (this.controls.load) this.controls.load.style.display = "none";
194 if (this.controls.start) this.controls.start.disabled = false;
195 if (this.controls.step) this.controls.step.disabled = false;
196 if (this.controls.stop) this.controls.stop.disabled = true;
197 if (this.controls.reset) this.controls.reset.disabled = false;
198 if (this.display) this.display.style.display = "block";
199 if (this.source) this.source.style.display = "none";
200 };
201
202 this.load = function(text) {
203 alert("load() NotImplementedError");
204 };
205
206 /*
207 * Loads a source text into the source element.
208 */
209 this.loadSource = function(text) {
210 if (this.source) this.source.value = text;
211 this.load(text);
212 this.state = PAUSED;
213 };
214
215 /*
216 * Loads a source text into the source element.
217 * Assumes it comes from an element in the document, so it translates
218 * the basic HTML escapes (but no others) to plain text.
219 */
220 this.loadSourceFromHTML = function(html) {
221 var text = html;
222 text = text.replace(/\&lt;/g, '<');
223 text = text.replace(/\&gt;/g, '>');
224 text = text.replace(/\&amp;/g, '&');
225 this.loadSource(text);
226 };
227
228 /*
229 * This is the basic idea, but not fleshed out yet.
230 * - Should we cache the source somewhere?
231 * - While we're waiting, should we disable the UI / show a spinny?
232 */
233 this.loadSourceFromURL = function(url, errorCallback) {
234 var http = new XMLHttpRequest();
235 var $this = this;
236 if (!errorCallback) {
237 errorCallback = function(http) {
238 $this.loadSource(
239 "Error: could not load " + url + ": " + http.statusText
240 );
241 }
242 }
243 http.open("get", url, true);
244 http.onload = function(e) {
245 if (http.readyState === 4 && http.responseText) {
246 if (http.status === 200) {
247 $this.loadSource(http.responseText);
248 } else {
249 errorCallback(http);
250 }
251 }
252 };
253 http.send(null);
254 };
255
256 this.click_edit = function(e) {
257 this.click_stop();
258 if (this.controls.edit) this.controls.edit.style.display = "none";
259 if (this.controls.load) this.controls.load.style.display = "inline";
260 if (this.controls.start) this.controls.start.disabled = true;
261 if (this.controls.step) this.controls.step.disabled = true;
262 if (this.controls.stop) this.controls.stop.disabled = true;
263 if (this.controls.reset) this.controls.reset.disabled = true;
264 if (this.display) this.display.style.display = "none";
265 if (this.source) this.source.style.display = "block";
266 };
267
268 this.click_start = function(e) {
269 this.start();
199 throw new Error("step() NotImplementedError");
200 };
201
202 /******************
203 * action: Start
204 */
205 this.clickStart = function(e) {
206 this.performStart();
270207 if (this.controls.start) this.controls.start.disabled = true;
271208 if (this.controls.step) this.controls.step.disabled = false;
272209 if (this.controls.stop) this.controls.stop.disabled = false;
273210 };
274211
212 this.performStart = function() {
213 this.start();
214 };
215
275216 this.start = function() {
276217 if (this.intervalId !== undefined)
277218 return;
278 this.step();
219 this.performStep();
279220 var $this = this;
280221 this.intervalId = setInterval(function() {
281222 $this.performStep();
283224 this.state = RUNNING;
284225 };
285226
286 this.click_stop = function(e) {
287 this.stop();
288 this.state = PAUSED;
289 /* why is this check here? ... */
290 if (this.controls.stop && this.controls.stop.disabled) {
291 return;
292 }
227 /******************
228 * action: Stop
229 */
230 this.clickStop = function(e) {
231 this.performStop();
293232 if (this.controls.start) this.controls.start.disabled = false;
294233 if (this.controls.step) this.controls.step.disabled = false;
295234 if (this.controls.stop) this.controls.stop.disabled = true;
296235 };
297236
298 this.terminate = function(e) {
237 this.performStop = function() {
299238 this.stop();
300 this.state = STOPPED;
301 if (this.controls.start) this.controls.start.disabled = true;
302 if (this.controls.step) this.controls.step.disabled = true;
303 if (this.controls.stop) this.controls.stop.disabled = true;
239 this.state = PAUSED;
304240 };
305241
306242 this.stop = function() {
310246 this.intervalId = undefined;
311247 };
312248
313 this.click_reset = function(e) {
314 this.click_stop();
315 this.load(this.source.value);
249 /******************
250 * action: Reset
251 */
252 this.clickReset = function(e) {
253 this.clickStop();
254 this.performReset();
316255 if (this.controls.start) this.controls.start.disabled = false;
317256 if (this.controls.step) this.controls.step.disabled = false;
318257 if (this.controls.stop) this.controls.stop.disabled = true;
319258 };
320259
321 /*
322 * Override this to change how the delay is acquired from the 'speed'
323 * element.
324 */
325 this.setDelayFrom = function(elem) {
326 this.delay = elem.max - elem.value;
260 this.performReset = function(state) {
261 if (state !== undefined) {
262 this.setResetState(state);
263 }
264 this.reset(this.resetState);
265 };
266
267 this.reset = function(state) {
268 throw new Error("reset() NotImplementedError");
269 };
270
271 this.setResetState = function(state) {
272 this.resetState = state;
327273 };
328274 };
00 /*
1 * This file is part of yoob.js version 0.7
1 * This file is part of yoob.js version 0.12
22 * Available from https://github.com/catseye/yoob.js/
33 * This file is in the public domain. See http://unlicense.org/ for details.
44 */
1111 * A direction vector accompanies the position, so the cursor can "know which
1212 * way it's headed", but this facility need not be used.
1313 *
14 * A cursor contains a built-in simple view, i.e. it knows how to render
15 * itself on a canvas (drawContext method) or in the midst of HTML text
16 * (wrapText method). These methods are used by the view classes (playfield,
17 * tape, source, etc.) The default methods draw basic block cursors in the
18 * colour given by the fillStyle attribute, if present, or a light green if
19 * it is not defined.
14 * These methods are written for efficiency rather than inheritability, so
15 * if you e.g. override setX() in a subclass, you will want to override
16 * advance() et al too.
2017 */
2118 yoob.Cursor = function() {
22 this.init = function(x, y, dx, dy) {
19 this.init = function(cfg) {
20 cfg = cfg || {};
21 this.setX(cfg.x || 0);
22 this.setY(cfg.y || 0);
23 this.setDx(cfg.dx || 0);
24 this.setDy(cfg.dy || 0);
25 return this;
26 };
27
28 this.clone = function() {
29 return new yoob.Cursor().init({
30 x: this.x,
31 y: this.y,
32 dx: this.dx,
33 dy: this.dy
34 });
35 };
36
37 /*** Chainable setters ***/
38
39 this.setX = function(x) {
2340 this.x = x;
41 return this;
42 };
43
44 this.setY = function(y) {
2445 this.y = y;
46 return this;
47 };
48
49 this.setDx = function(dx) {
2550 this.dx = dx;
51 return this;
52 };
53
54 this.setDy = function(dy) {
2655 this.dy = dy;
2756 return this;
2857 };
2958
30 this.clone = function() {
31 return new yoob.Cursor().init(this.x, this.y, this.dx, this.dy);
32 };
59 /*** Accessors ***/
3360
3461 this.getX = function() {
3562 return this.x;
3966 return this.y;
4067 };
4168
42 this.setX = function(x) {
43 this.x = x;
69 this.getDx = function() {
70 return this.dx;
4471 };
4572
46 this.setY = function(y) {
47 this.y = y;
73 this.getDy = function() {
74 return this.dy;
4875 };
4976
5077 this.isHeaded = function(dx, dy) {
5178 return this.dx === dx && this.dy === dy;
5279 };
5380
81 /*** Motion ***/
82
83 this.moveTo = function(x, y) {
84 this.x = x;
85 this.y = y;
86 return this;
87 };
88
89 this.moveBy = function(dx, dy) {
90 this.x += dx;
91 this.y += dy;
92 return this;
93 };
94
95 this.moveLeft = function(amount) {
96 if (amount === undefined) amount = 1;
97 this.x -= amount;
98 return this;
99 };
100
101 this.moveRight = function(amount) {
102 if (amount === undefined) amount = 1;
103 this.x += amount;
104 return this;
105 };
106
54107 this.advance = function() {
55108 this.x += this.dx;
56109 this.y += this.dy;
110 return this;
57111 };
112
113 /*** Orientation ***/
58114
59115 this.rotateClockwise = function() {
60116 if (this.dx === 0 && this.dy === -1) {
74130 } else if (this.dx === -1 && this.dy === -1) {
75131 this.dx = 0; this.dy = -1;
76132 }
133 return this;
77134 };
78135
79136 this.rotateCounterclockwise = function() {
94151 } else if (this.dx === 1 && this.dy === -1) {
95152 this.dx = 0; this.dy = -1;
96153 }
154 return this;
97155 };
98156
99157 this.rotateDegrees = function(degrees) {
101159 this.rotateCounterclockwise();
102160 degrees -= 45;
103161 }
104 };
105
106 /* from yoob.TapeHead; may go away or change slightly */
107 this.move = function(delta) {
108 this.x += delta;
109 };
110
111 this.moveLeft = function(amount) {
112 if (amount === undefined) amount = 1;
113 this.x -= amount;
114 };
115
116 this.moveRight = function(amount) {
117 if (amount === undefined) amount = 1;
118 this.x += amount;
119 };
120
121 this.read = function() {
122 if (!this.tape) return undefined;
123 return this.tape.get(this.x);
124 };
125
126 this.write = function(value) {
127 if (!this.tape) return;
128 this.tape.put(this.x, value);
129 };
130
131 /*
132 * For HTML views. Override if you like.
133 */
134 this.wrapText = function(text) {
135 var fillStyle = this.fillStyle || "#50ff50";
136 return '<span style="background: ' + fillStyle + '">' +
137 text + '</span>';
138 };
139
140 /*
141 * For canvas views. Override if you like.
142 */
143 this.drawContext = function(ctx, x, y, cellWidth, cellHeight) {
144 ctx.fillStyle = this.fillStyle || "#50ff50";
145 ctx.fillRect(x, y, cellWidth, cellHeight);
162 return this;
146163 };
147164 }
0 /*
1 * This file is part of yoob.js version 0.12
2 * Available from https://github.com/catseye/yoob.js/
3 * This file is in the public domain. See http://unlicense.org/ for details.
4 */
5 if (window.yoob === undefined) yoob = {};
6
7 /*
8 * Functions for creating elements.
9 */
10
11 yoob.makeCanvas = function(container, width, height) {
12 var canvas = document.createElement('canvas');
13 if (width) {
14 canvas.width = width;
15 }
16 if (height) {
17 canvas.height = height;
18 }
19 container.appendChild(canvas);
20 return canvas;
21 };
22
23 yoob.makeButton = function(container, labelText, fun) {
24 var button = document.createElement('button');
25 button.innerHTML = labelText;
26 container.appendChild(button);
27 if (fun) {
28 button.onclick = fun;
29 }
30 return button;
31 };
32
33 yoob.checkBoxNumber = 0;
34 yoob.makeCheckbox = function(container, checked, labelText, fun) {
35 var checkbox = document.createElement('input');
36 checkbox.type = "checkbox";
37 checkbox.id = 'cfzzzb_' + yoob.checkBoxNumber;
38 checkbox.checked = checked;
39 var label = document.createElement('label');
40 label.htmlFor = 'cfzzzb_' + yoob.checkBoxNumber;
41 yoob.checkBoxNumber += 1;
42 label.appendChild(document.createTextNode(labelText));
43
44 container.appendChild(checkbox);
45 container.appendChild(label);
46
47 if (fun) {
48 checkbox.onchange = function(e) {
49 fun(checkbox.checked);
50 };
51 }
52 return checkbox;
53 };
54
55 yoob.makeTextInput = function(container, size, value) {
56 var input = document.createElement('input');
57 input.size = "" + (size || 12);
58 input.value = value || "";
59 container.appendChild(input);
60 return input;
61 };
62
63 yoob.makeSlider = function(container, min, max, value, fun) {
64 var slider = document.createElement('input');
65 slider.type = "range";
66 slider.min = min;
67 slider.max = max;
68 slider.value = value || 0;
69 if (fun) {
70 slider.onchange = function(e) {
71 fun(parseInt(slider.value, 10));
72 };
73 }
74 container.appendChild(slider);
75 return slider;
76 };
77
78 yoob.makeParagraph = function(container, innerHTML) {
79 var p = document.createElement('p');
80 p.innerHTML = innerHTML || '';
81 container.appendChild(p);
82 return p;
83 };
84
85 yoob.makeSpan = function(container, innerHTML) {
86 var span = document.createElement('span');
87 span.innerHTML = innerHTML || '';
88 container.appendChild(span);
89 return span;
90 };
91
92 yoob.makeDiv = function(container, innerHTML) {
93 var div = document.createElement('div');
94 div.innerHTML = innerHTML || '';
95 container.appendChild(div);
96 return div;
97 };
98
99 yoob.makePre = function(container, innerHTML) {
100 var elem = document.createElement('pre');
101 elem.innerHTML = innerHTML || '';
102 container.appendChild(elem);
103 return elem;
104 };
105
106 yoob.makePanel = function(container, title, isOpen) {
107 isOpen = !!isOpen;
108 var panelContainer = document.createElement('div');
109 var button = document.createElement('button');
110 var innerContainer = document.createElement('div');
111 innerContainer.style.display = isOpen ? "block" : "none";
112
113 button.innerHTML = (isOpen ? "∇" : "⊳") + " " + title;
114 button.onclick = function(e) {
115 isOpen = !isOpen;
116 button.innerHTML = (isOpen ? "∇" : "⊳") + " " + title;
117 innerContainer.style.display = isOpen ? "block" : "none";
118 };
119
120 panelContainer.appendChild(button);
121 panelContainer.appendChild(innerContainer);
122 container.appendChild(panelContainer);
123 return innerContainer;
124 };
125
126 yoob.makeTextArea = function(container, cols, rows, initial) {
127 var textarea = document.createElement('textarea');
128 textarea.rows = "" + rows;
129 textarea.cols = "" + cols;
130 if (initial) {
131 textarea.value = initial;
132 }
133 container.appendChild(textarea);
134 return textarea;
135 };
136
137 yoob.makeLineBreak = function(container) {
138 var br = document.createElement('br');
139 container.appendChild(br);
140 return br;
141 };
142
143 yoob.makeSelect = function(container, labelText, optionsArray, fun, def) {
144 var label = document.createElement('label');
145 label.innerHTML = labelText;
146 container.appendChild(label);
147
148 var select = document.createElement("select");
149
150 for (var i = 0; i < optionsArray.length; i++) {
151 var op = document.createElement("option");
152 op.value = optionsArray[i][0];
153 op.text = optionsArray[i][1];
154 if (optionsArray[i].length > 2) {
155 op.selected = optionsArray[i][2];
156 } else {
157 op.selected = false;
158 }
159 select.options.add(op);
160 }
161
162 if (fun) {
163 select.onchange = function(e) {
164 fun(optionsArray[select.selectedIndex][0]);
165 };
166 }
167
168 if (def) {
169 var i = 0;
170 var opt = select.options[i];
171 while (opt) {
172 if (opt.value === def) {
173 select.selectedIndex = i;
174 if (fun) fun(def);
175 break;
176 }
177 i++;
178 opt = select.options[i];
179 }
180 }
181
182 container.appendChild(select);
183 return select;
184 };
185
186 var SliderPlusTextInput = function() {
187 this.init = function(cfg) {
188 this.slider = cfg.slider;
189 this.textInput = cfg.textInput;
190 this.callback = cfg.callback;
191 return this;
192 };
193
194 this.set = function(value) {
195 this.slider.value = "" + value;
196 this.textInput.value = "" + value;
197 this.callback(value);
198 };
199 };
200
201 yoob.makeSliderPlusTextInput = function(container, label, min_, max_, size, value, fun) {
202 yoob.makeSpan(container, label);
203 var slider = yoob.makeSlider(container, min_, max_, value);
204 var s = "" + value;
205 var textInput = yoob.makeTextInput(container, size, s);
206 slider.onchange = function(e) {
207 textInput.value = slider.value;
208 fun(parseInt(slider.value, 10));
209 };
210 textInput.onchange = function(e) {
211 var v = parseInt(textInput.value, 10);
212 if (v !== NaN) {
213 slider.value = "" + v;
214 fun(v);
215 }
216 };
217 return new SliderPlusTextInput().init({
218 'slider': slider,
219 'textInput': textInput,
220 'callback': fun
221 });
222 };
223
224 var RangeControl = function() {
225 this.init = function(cfg) {
226 this.slider = cfg.slider;
227 this.textInput = cfg.textInput;
228 this.callback = cfg.callback;
229 this.incButton = cfg.incButton;
230 this.decButton = cfg.decButton;
231 return this;
232 };
233
234 this.set = function(value) {
235 this.slider.value = "" + value;
236 this.textInput.value = "" + value;
237 this.callback(value);
238 };
239 };
240
241 yoob.makeRangeControl = function(container, config) {
242 var label = config.label;
243 var min_ = config['min'];
244 var max_ = config['max'];
245 var value = config.value || min_;
246 var callback = config.callback || function(v) {};
247 var textInputSize = config.textInputSize || 5;
248 var withButtons = config.withButtons === false ? false : true;
249
250 yoob.makeSpan(container, label);
251 var slider = yoob.makeSlider(container, min_, max_, value);
252 var s = "" + value;
253 var textInput = yoob.makeTextInput(container, textInputSize, s);
254 slider.onchange = function(e) {
255 textInput.value = slider.value;
256 callback(parseInt(slider.value, 10));
257 };
258 textInput.onchange = function(e) {
259 var v = parseInt(textInput.value, 10);
260 if (v !== NaN) {
261 slider.value = "" + v;
262 callback(v);
263 }
264 };
265 var incButton;
266 var decButton;
267 if (withButtons) {
268 decButton = yoob.makeButton(container, "-", function() {
269 var v = parseInt(textInput.value, 10);
270 if (v !== NaN && v > min_) {
271 v--;
272 textInput.value = "" + v;
273 slider.value = "" + v;
274 callback(v);
275 }
276 });
277 incButton = yoob.makeButton(container, "+", function() {
278 var v = parseInt(textInput.value, 10);
279 if (v !== NaN && v < max_) {
280 v++;
281 textInput.value = "" + v;
282 slider.value = "" + v;
283 callback(v);
284 }
285 });
286 }
287 return new RangeControl().init({
288 'slider': slider,
289 'incButton': incButton,
290 'decButton': decButton,
291 'textInput': textInput,
292 'callback': callback
293 });
294 };
295
296 yoob.makeSVG = function(container) {
297 var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
298 /* <svg viewBox = "0 0 200 200" version = "1.1"> */
299 container.appendChild(svg);
300 return svg;
301 };
302
303 yoob.makeSVGElem = function(svg, tag, cfg) {
304 var elem = document.createElementNS(svg.namespaceURI, tag);
305 for (var key in cfg) {
306 if (cfg.hasOwnProperty(key)) {
307 elem.setAttribute(key, cfg[key]);
308 }
309 }
310 svg.appendChild(elem);
311 return elem;
312 };
00 /*
1 * This file is part of yoob.js version 0.6
1 * This file is part of yoob.js version 0.11
22 * Available from https://github.com/catseye/yoob.js/
33 * This file is in the public domain. See http://unlicense.org/ for details.
44 */
77 /*
88 * A view (in the MVC sense) for depicting a yoob.Playfield (-compatible)
99 * object onto any DOM element that supports innerHTML.
10 *
11 * TODO: this may be incomplete; use at your own risk
12 * TODO: have this and the canvas view inherit from a common ABC?
1310 */
1411 yoob.PlayfieldHTMLView = function() {
15 this.pf = undefined;
16 this.element = undefined;
17
18 this.init = function(pf, element) {
19 this.pf = pf;
20 this.element = element;
21 this.cursors = [];
12 this.init = function(cfg) {
13 this.pf = cfg.playfield;
14 this.element = cfg.element;
2215 return this;
2316 };
2417
2518 /*** Chainable setters ***/
2619
27 /*
28 * Set the list of cursors to the given list of yoob.Cursor (or compatible)
29 * objects.
30 */
31 this.setCursors = function(cursors) {
32 this.cursors = cursors;
20 this.setPlayfield = function(pf) {
21 this.pf = pf;
22 return this;
23 };
24
25 this.setElement = function(element) {
26 this.element = element;
3327 return this;
3428 };
3529
3630 /*
37 * Return the requested bounds of the occupied portion of the playfield.
38 * "Occupation" in this sense includes all cursors.
39 *
40 * These may return 'undefined' if there is nothing in the playfield.
41 *
42 * Override these if you want to draw some portion of the
43 * playfield which is not the whole playfield.
31 * For compatibility with PlayfieldCanvasView. Sets the font size.
4432 */
45 this.getLowerX = function() {
46 var minX = this.pf.getMinX();
47 for (var i = 0; i < this.cursors.length; i++) {
48 if (minX === undefined || this.cursors[i].x < minX) {
49 minX = this.cursors[i].x;
50 }
51 }
52 return minX;
53 };
54 this.getUpperX = function() {
55 var maxX = this.pf.getMaxX();
56 for (var i = 0; i < this.cursors.length; i++) {
57 if (maxX === undefined || this.cursors[i].x > maxX) {
58 maxX = this.cursors[i].x;
59 }
60 }
61 return maxX;
62 };
63 this.getLowerY = function() {
64 var minY = this.pf.getMinY();
65 for (var i = 0; i < this.cursors.length; i++) {
66 if (minY === undefined || this.cursors[i].y < minY) {
67 minY = this.cursors[i].y;
68 }
69 }
70 return minY;
71 };
72 this.getUpperY = function() {
73 var maxY = this.pf.getMaxY();
74 for (var i = 0; i < this.cursors.length; i++) {
75 if (maxY === undefined || this.cursors[i].y > maxY) {
76 maxY = this.cursors[i].y;
77 }
78 }
79 return maxY;
80 };
81
82 /*
83 * Returns the number of occupied cells in the x direction.
84 * "Occupation" in this sense includes all cursors.
85 */
86 this.getExtentX = function() {
87 if (this.getLowerX() === undefined || this.getUpperX() === undefined) {
88 return 0;
89 } else {
90 return this.getUpperX() - this.getLowerX() + 1;
91 }
92 };
93
94 /*
95 * Returns the number of occupied cells in the y direction.
96 * "Occupation" in this sense includes all cursors.
97 */
98 this.getExtentY = function() {
99 if (this.getLowerY() === undefined || this.getUpperY() === undefined) {
100 return 0;
101 } else {
102 return this.getUpperY() - this.getLowerY() + 1;
103 }
33 this.setCellDimensions = function(cellWidth, cellHeight) {
34 this.element.style.fontSize = cellHeight + "px";
35 return this;
10436 };
10537
10638 /*
10739 * Override to convert Playfield values to HTML.
10840 */
10941 this.render = function(value) {
42 if (value === undefined) return ' ';
11043 return value;
44 };
45
46 /*
47 * Override if you like.
48 */
49 this.wrapCursorText = function(cursor, text) {
50 var fillStyle = this.cursorFillStyle || "#50ff50";
51 return '<span style="background: ' + fillStyle + '">' +
52 text + '</span>';
11153 };
11254
11355 /*
11557 */
11658 this.draw = function() {
11759 var text = "";
118 for (var y = this.getLowerY(); y <= this.getUpperY(); y++) {
60 var cursors = this.pf.cursors;
61 var lowerY = this.pf.getLowerY();
62 var upperY = this.pf.getUpperY();
63 var lowerX = this.pf.getLowerX();
64 var upperX = this.pf.getUpperX();
65 for (var y = lowerY; y <= upperY; y++) {
11966 var row = "";
120 for (var x = this.getLowerX(); x <= this.getUpperX(); x++) {
67 for (var x = lowerX; x <= upperX; x++) {
12168 var rendered = this.render(this.pf.get(x, y));
122 for (var i = 0; i < this.cursors.length; i++) {
123 if (this.cursors[i].x === x && this.cursors[i].y === y) {
124 rendered = this.cursors[i].wrapText(rendered);
69 for (var i = 0; i < cursors.length; i++) {
70 if (cursors[i].x === x && cursors[i].y === y) {
71 rendered = this.wrapCursorText(cursors[i], rendered);
12572 }
12673 }
12774 row += rendered;
00 /*
1 * This file is part of yoob.js version 0.6
1 * This file is part of yoob.js version 0.12
22 * Available from https://github.com/catseye/yoob.js/
33 * This file is in the public domain. See http://unlicense.org/ for details.
44 */
88 * A two-dimensional Cartesian grid of values.
99 */
1010 yoob.Playfield = function() {
11 this._store = {};
12 this.minX = undefined;
13 this.minY = undefined;
14 this.maxX = undefined;
15 this.maxY = undefined;
16 this._default = undefined;
11 this.init = function(cfg) {
12 cfg = cfg || {};
13 this._default = cfg.defaultValue;
14 this.cursors = cfg.cursors || [];
15 this.clear();
16 return this;
17 };
18
19 /*** Chainable setters ***/
1720
1821 /*
1922 * Set the default value for this Playfield. This
2427 this._default = v;
2528 return this;
2629 };
30
31 /*
32 * Set the list of cursors to the given list of yoob.Cursor (or compatible)
33 * objects.
34 */
35 this.setCursors = function(cursors) {
36 this.cursors = cursors;
37 return this;
38 };
39
40 /*** Accessors, etc. ***/
2741
2842 /*
2943 * Obtain the value at (x, y). The default value will
4458 this.put = function(x, y, value) {
4559 var key = x+','+y;
4660 if (value === undefined || value === this._default) {
61 // NOTE: this does not recalculate the bounds, nor
62 // will it set the bounds back to 'undefined'
63 // if the playfield is now empty.
4764 delete this._store[key];
4865 return;
4966 }
101118 this.minY = undefined;
102119 this.maxX = undefined;
103120 this.maxY = undefined;
121 return this;
104122 };
105123
106124 /*
114132 this.put(x, y, this.get(x, y - dy));
115133 }
116134 }
117 } else { alert("scrollRectangleY(" + dy + ") notImplemented"); }
135 } else {
136 throw new Error("scrollRectangleY(" + dy + ") notImplemented");
137 }
118138 };
119139
120140 this.clearRectangle = function(minX, minY, maxX, maxY) {
192212 * fun is a callback which takes three parameters:
193213 * x, y, and value. If this callback returns a value,
194214 * it is written into the Playfield at that position.
195 * This function ensures a particular order.
215 * This function ensures a particular order. For efficiency,
216 * This function knows about the structure of the backing
217 * store, so if you override .get() or .put() in a subclass,
218 * you should also override this.
196219 */
197220 this.foreach = function(fun) {
198221 for (var y = this.minY; y <= this.maxY; y++) {
202225 if (value === undefined)
203226 continue;
204227 var result = fun(x, y, value);
228 // TODO: Playfield.UNDEFINED vs. undefined meaning "no change"?
205229 if (result !== undefined) {
206 if (result === ' ') {
207 result = undefined;
208 }
230 this.put(x, y, result);
231 }
232 }
233 }
234 };
235
236 this.foreachVonNeumannNeighbour = function(x, y, fun) {
237 for (var dx = -1; dx <= 1; dx++) {
238 for (var dy = -1; dy <= 1; dy++) {
239 if (dx === 0 && dy === 0)
240 continue;
241 var value = this.get(x + dx, y + dy);
242 if (value === undefined)
243 continue;
244 var result = fun(x, y, value);
245 // TODO: Playfield.UNDEFINED vs. undefined meaning "no change"?
246 if (result !== undefined) {
209247 this.put(x, y, result);
210248 }
211249 }
282320 return this.maxY - this.minY + 1;
283321 }
284322 };
323
324 /*
325 * Return the requested bounds of the occupied portion of the playfield.
326 * "Occupation" in this sense includes all cursors.
327 *
328 * These may return 'undefined' if there is nothing in the playfield.
329 *
330 * Override these if you want to draw some portion of the
331 * playfield which is not the whole playfield.
332 */
333 this.getLowerX = function() {
334 var minX = this.getMinX();
335 for (var i = 0; i < this.cursors.length; i++) {
336 if (minX === undefined || this.cursors[i].x < minX) {
337 minX = this.cursors[i].x;
338 }
339 }
340 return minX;
341 };
342 this.getUpperX = function() {
343 var maxX = this.getMaxX();
344 for (var i = 0; i < this.cursors.length; i++) {
345 if (maxX === undefined || this.cursors[i].x > maxX) {
346 maxX = this.cursors[i].x;
347 }
348 }
349 return maxX;
350 };
351 this.getLowerY = function() {
352 var minY = this.getMinY();
353 for (var i = 0; i < this.cursors.length; i++) {
354 if (minY === undefined || this.cursors[i].y < minY) {
355 minY = this.cursors[i].y;
356 }
357 }
358 return minY;
359 };
360 this.getUpperY = function() {
361 var maxY = this.getMaxY();
362 for (var i = 0; i < this.cursors.length; i++) {
363 if (maxY === undefined || this.cursors[i].y > maxY) {
364 maxY = this.cursors[i].y;
365 }
366 }
367 return maxY;
368 };
369
370 /*
371 * Returns the number of occupied cells in the x direction.
372 * "Occupation" in this sense includes all cursors.
373 */
374 this.getCursoredExtentX = function() {
375 if (this.getLowerX() === undefined || this.getUpperX() === undefined) {
376 return 0;
377 } else {
378 return this.getUpperX() - this.getLowerX() + 1;
379 }
380 };
381
382 /*
383 * Returns the number of occupied cells in the y direction.
384 * "Occupation" in this sense includes all cursors.
385 */
386 this.getCursoredExtentY = function() {
387 if (this.getLowerY() === undefined || this.getUpperY() === undefined) {
388 return 0;
389 } else {
390 return this.getUpperY() - this.getLowerY() + 1;
391 }
392 };
393
394 /*
395 * Cursored read/write interface
396 */
397 this.read = function(index) {
398 var cursor = this.cursors[index || 0];
399 return this.get(cursor.getX(), cursor.getY());
400 };
401
402 this.write = function(value, index) {
403 var cursor = this.cursors[index || 0];
404 this.put(cursor.getX(), cursor.getY(), value);
405 return this;
406 };
285407 };
00 /*
1 * This file is part of yoob.js version 0.6
1 * This file is part of yoob.js version 0.12
22 * Available from https://github.com/catseye/yoob.js/
33 * This file is in the public domain. See http://unlicense.org/ for details.
44 */
2121 * will cause the .select() method of this manager to be called.
2222 * it will also call .onselect if that method is present.
2323 *
24 * controller: a yoob.Controller (or compatible object) that will
25 * be informed of the selection, if no callback was supplied
26 * when the item was added.
24 * setPreset: (optional) a callback which will be called whenever
25 * a new preset is selected. If this is not given, an individual
26 * callback must be supplied with each preset as it is added.
2727 */
2828 this.init = function(cfg) {
2929 this.selectElem = cfg.selectElem;
30 this.controller = cfg.controller || null;
30 if (cfg.setPreset) {
31 this.setPreset = cfg.setPreset;
32 }
3133 this.clear();
3234 var $this = this;
3335 this.selectElem.onchange = function() {
5153 /*
5254 * Adds a preset to this PresetManager. When it is selected,
5355 * the given callback will be called, being passed the id as the
54 * first argument. If no callback is provided, a default callback,
55 * which loads the contents of the element with the specified id
56 * into the configured yoob.Controller, will be used.
56 * first argument. If no callback is provided, the default callback,
57 * configured with setPreset in the init() configuration, will be used.
5758 */
5859 this.add = function(id, callback) {
5960 var opt = document.createElement("option");
6162 opt.value = id;
6263 this.selectElem.options.add(opt);
6364 var $this = this;
64 this.reactTo[id] = callback || function(id) {
65 $this.controller.click_stop(); // in case it is currently running
66 $this.controller.loadSourceFromHTML(
67 document.getElementById(id).innerHTML
68 );
69 };
65 this.reactTo[id] = callback || this.setPreset;
7066 return this;
67 };
68
69 this.setPreset = function(id) {
70 throw new Error("No default setPreset callback configured");
7171 };
7272
7373 /*
0 /*
1 * This file is part of yoob.js version 0.8
2 * Available from https://github.com/catseye/yoob.js/
3 * This file is in the public domain. See http://unlicense.org/ for details.
4 */
5 if (window.yoob === undefined) yoob = {};
6
7 /*
8 * A SourceManager co-operates with a Controller and maybe a PresetManager.
9 * It is for editing a program/configuration in some editing interface
10 * which is mutually exclusive, UI-wise, with the run/animation interface.
11 */
12 yoob.SourceManager = function() {
13 /*
14 * editor: an element (usually a textarea) which stores the source code
15 * hideDuringEdit: a list of elements which will be hidden when editing
16 * (typically this will be the animation display of a yoob.Controller)
17 * disableDuringEdit: a list of elements which will be disabled when editing
18 * storageKey: key under which sources will be saved/loaded from localStorage
19 * panelContainer: an element into which to add the created button panel
20 * (if you do not give this, no panel will be created. You're on your own.)
21 * onDone: if given, if a function, it becomes the onDone method on this
22 */
23 this.init = function(cfg) {
24 this.supportsLocalStorage = (
25 window['localStorage'] !== undefined &&
26 window['localStorage'] !== null
27 );
28 this.editor = cfg.editor;
29 this.hideDuringEdit = cfg.hideDuringEdit;
30 this.prevDisplay = {};
31 for (var i = 0; i < this.hideDuringEdit.length; i++) {
32 this.prevDisplay[this.hideDuringEdit[i]] = this.hideDuringEdit[i].display;
33 }
34 this.disableDuringEdit = cfg.disableDuringEdit;
35 this.storageKey = cfg.storageKey || 'default';
36 this.controls = {};
37 if (cfg.panelContainer) {
38 this.panel = this.makePanel();
39 cfg.panelContainer.appendChild(this.panel);
40 }
41 if (cfg.onDone) {
42 this.onDone = cfg.onDone;
43 }
44 this.clickDone();
45 return this;
46 };
47
48 this.makePanel = function() {
49 var panel = document.createElement('div');
50 var $this = this;
51 var makeButton = function(action) {
52 var button = document.createElement('button');
53 var upperAction = action.charAt(0).toUpperCase() + action.slice(1);
54 button.innerHTML = upperAction;
55 button.style.width = "5em";
56 panel.appendChild(button);
57 button.onclick = function(e) {
58 if ($this['click' + upperAction]) {
59 $this['click' + upperAction]();
60 }
61 }
62 $this.controls[action] = button;
63 };
64 var keys = ["edit", "done", "load", "save"];
65 for (var i = 0; i < keys.length; i++) {
66 makeButton(keys[i]);
67 }
68 return panel;
69 };
70
71 this.clickEdit = function() {
72 var hde = this.hideDuringEdit;
73 for (var i = 0; i < hde.length; i++) {
74 this.prevDisplay[hde[i]] = hde[i].style.display;
75 hde[i].style.display = 'none';
76 }
77 for (var i = 0; i < this.disableDuringEdit.length; i++) {
78 this.disableDuringEdit[i].disabled = true;
79 /* But if it's not a form control, disabled is meaningless. */
80 this.disableDuringEdit[i].style.pointerEvents = 'none';
81 this.disableDuringEdit[i].style.opacity = '0.5';
82 }
83 this.editor.style.display = 'block';
84 this.controls.edit.disabled = true;
85 var keys = ["done", "load", "save"];
86 for (var i = 0; i < keys.length; i++) {
87 this.controls[keys[i]].disabled = false;
88 }
89 this.onEdit();
90 };
91
92 this.clickDone = function() {
93 var hde = this.hideDuringEdit;
94 for (var i = 0; i < hde.length; i++) {
95 hde[i].style.display = this.prevDisplay[hde[i]];
96 }
97 for (var i = 0; i < this.disableDuringEdit.length; i++) {
98 this.disableDuringEdit[i].disabled = false;
99 /* But if it's not a form control, disabled is meaningless. */
100 this.disableDuringEdit[i].style.pointerEvents = 'auto';
101 this.disableDuringEdit[i].style.opacity = '1.0';
102 }
103 this.editor.style.display = 'none';
104 this.controls.edit.disabled = false;
105 var keys = ["done", "load", "save"];
106 for (var i = 0; i < keys.length; i++) {
107 this.controls[keys[i]].disabled = true;
108 }
109 this.onDone();
110 };
111
112 this.clickLoad = function() {
113 if (!this.supportsLocalStorage) {
114 var s = "Your browser does not support Local Storage.\n\n";
115 s += "You may instead open a local file in a text editor, ";
116 s += "select all, copy to clipboard, then paste into ";
117 s += "the textarea, to load a source you have saved locally.";
118 alert(s);
119 return;
120 }
121 this.loadSource(
122 localStorage.getItem('yoob:' + this.storageKey + ':default')
123 );
124 };
125
126 this.clickSave = function() {
127 if (!this.supportsLocalStorage) {
128 var s = "Your browser does not support Local Storage.\n\n";
129 s += "You may instead select all in the textarea, copy to ";
130 s += "clipboard, open a local text editor, paste in there, ";
131 s += "and save, to save a source locally.";
132 alert(s);
133 return;
134 }
135 localStorage.setItem(
136 'yoob:' + this.storageKey + ':default',
137 this.getEditorText()
138 );
139 };
140
141 this.onEdit = function() {
142 };
143
144 /*
145 * Override this to load it into the controller
146 */
147 this.onDone = function() {
148 };
149
150 /*
151 * Loads a source text into the editor element.
152 */
153 this.loadSource = function(text) {
154 this.setEditorText(text);
155 this.onDone();
156 };
157
158 /*
159 * You may need to override if your editor is not a textarea.
160 */
161 this.setEditorText = function(text) {
162 this.editor.value = text;
163 };
164
165 /*
166 * You may need to override if your editor is not a textarea.
167 */
168 this.getEditorText = function() {
169 return this.editor.value;
170 };
171
172 /*
173 * Loads a source text into the source element.
174 * Assumes it comes from an element in the document, so it translates
175 * the basic HTML escapes (but no others) to plain text.
176 */
177 this.loadSourceFromHTML = function(html) {
178 var text = html;
179 text = text.replace(/\&lt;/g, '<');
180 text = text.replace(/\&gt;/g, '>');
181 text = text.replace(/\&amp;/g, '&');
182 this.loadSource(text);
183 };
184
185 /*
186 * This is the basic idea, but not fleshed out yet.
187 * - Should we cache the source somewhere?
188 * - While we're waiting, should we disable the UI / show a spinny?
189 */
190 this.loadSourceFromURL = function(url, errorCallback) {
191 var http = new XMLHttpRequest();
192 var $this = this;
193 if (!errorCallback) {
194 errorCallback = function(http) {
195 $this.loadSource(
196 "Error: could not load " + url + ": " + http.statusText
197 );
198 }
199 }
200 http.open("get", url, true);
201 http.onload = function(e) {
202 if (http.readyState === 4 && http.responseText) {
203 if (http.status === 200) {
204 $this.loadSource(http.responseText);
205 } else {
206 errorCallback(http);
207 }
208 }
209 };
210 http.send(null);
211 };
212 };
0 /*
1 * This file is part of yoob.js version 0.11
2 * Available from https://github.com/catseye/yoob.js/
3 * This file is in the public domain. See http://unlicense.org/ for details.
4 */
5 if (window.yoob === undefined) yoob = {};
6
7 /*
8 * A view (in the MVC sense) for depicting a yoob.Tape (-compatible)
9 * object onto any DOM element that supports innerHTML.
10 */
11 yoob.TapeHTMLView = function() {
12 this.init = function(cfg) {
13 this.tape = cfg.tape;
14 this.element = cfg.element;
15 return this;
16 };
17
18 /*** Chainable setters ***/
19
20 this.setTape = function(tape) {
21 this.tape = tape;
22 return this;
23 };
24
25 /*
26 * For compatibility with PlayfieldCanvasView. Sets the font size.
27 */
28 this.setCellDimensions = function(cellWidth, cellHeight) {
29 this.element.style.fontSize = cellHeight + "px";
30 return this;
31 };
32
33 /*
34 * Convert Tape values to HTML. Override to customize appearance.
35 */
36 this.render = function(value) {
37 if (value === undefined) return ' ';
38 return value;
39 };
40
41 /*
42 * Override if you like.
43 */
44 this.wrapCursorText = function(cursor, text) {
45 var fillStyle = this.cursorFillStyle || "#50ff50";
46 return '<span style="background: ' + fillStyle + '">' +
47 text + '</span>';
48 };
49
50 /*
51 * Render the Tape, as HTML, on the DOM element.
52 * TODO: make this not awful.
53 */
54 this.draw = function() {
55 var cursors = this.tape.cursors;
56 var text = "";
57 var $this = this;
58 this.tape.foreach(function(pos, value) {
59 var rendered = $this.render(value);
60 for (var i = 0; i < cursors.length; i++) {
61 if (cursors[i].getX() === pos) {
62 rendered = $this.wrapCursorText(cursors[i], rendered);
63 }
64 }
65 text += rendered + "<br/>";
66 }, { dense: true });
67 this.element.innerHTML = text;
68 };
69 };
00 /*
1 * This file is part of yoob.js version 0.3
1 * This file is part of yoob.js version 0.11
22 * Available from https://github.com/catseye/yoob.js/
33 * This file is in the public domain. See http://unlicense.org/ for details.
44 */
66
77 /*
88 * A (theoretically) unbounded tape, like you'd find on a Turing machine.
9 *
10 * It can also be used as a stack -- in this case, give it a single cursor
11 * starting at x=0. Note that the result of trying to use it both as a stack
12 * and as a tape is currently undefined.
13 *
14 * TODO: recalculate bounds?
915 */
1016 yoob.Tape = function() {
11 this._store = {};
12 this.min = undefined;
13 this.max = undefined;
17 this.init = function(cfg) {
18 cfg = cfg || {};
19 this._default = cfg.defaultValue;
20 this.cursors = cfg.cursors || [];
21 this.clear();
22 return this;
23 };
24
25 /*
26 * Removes all values that have been written to the tape.
27 */
28 this.clear = function() {
29 this._store = {};
30 this.min = undefined;
31 this.max = undefined;
32 return this;
33 };
1434
1535 /*
1636 * Obtain the value at the given position.
17 * Cells are undefined if they were never written to.
37 * Returns the tape's default value, if the position was never written to.
1838 */
1939 this.get = function(pos) {
20 return this._store[pos];
40 var val = this._store[pos];
41 return val === undefined ? this._default : val;
2142 };
2243
2344 /*
2445 * Write a new value into the given position.
2546 */
2647 this.put = function(pos, value) {
48 if (value === this._default) {
49 delete this._store[pos];
50 // NOTE: this does not recalculate the bounds.
51 return;
52 }
2753 if (this.min === undefined || pos < this.min) this.min = pos;
2854 if (this.max === undefined || pos > this.max) this.max = pos;
29 if (value === undefined) {
30 delete this._store[pos];
31 }
3255 this._store[pos] = value;
3356 };
3457
58 this.size = function() {
59 return this._top;
60 };
61
3562 /*
36 * Iterate over every defined cell on the Tape
63 * Iterate over every cell on the Tape.
3764 * fun is a callback which takes two parameters:
3865 * position and value. If this callback returns a value,
3966 * it is written into the Tape at that position.
40 * This function ensures a particular order.
67 * This function iterates in a defined order: ascending.
68 * The callback is not called for cells containing the
69 * default value unless `dense: true` is given in the
70 * configuration object.
4171 */
42 this.foreach = function(fun) {
72 this.foreach = function(fun, cfg) {
73 cfg = cfg || {};
74 var dense = !!cfg.dense;
4375 for (var pos = this.min; pos <= this.max; pos++) {
4476 var value = this._store[pos];
45 if (value === undefined)
46 continue;
77 if (value === undefined) {
78 if (dense) {
79 value = this._default;
80 } else {
81 continue;
82 }
83 }
4784 var result = fun(pos, value);
4885 if (result !== undefined) {
49 if (result === ' ') {
50 result = undefined;
51 }
5286 this.put(pos, result);
5387 }
5488 }
5589 };
5690
57 /*
58 * Draws elements of the Tape in a drawing context.
59 * x and y are canvas coordinates, and width and height
60 * are canvas units of measure.
61 * The default implementation just renders them as text,
62 * in black.
63 * Override if you wish to draw them differently.
64 */
65 this.drawElement = function(ctx, x, y, cellWidth, cellHeight, elem) {
66 ctx.fillStyle = "black";
67 ctx.fillText(elem.toString(), x, y);
91 this.getExtent = function() {
92 return this.max - this.min + 1;
93 };
94
95 this.getCursoredExtent = function() {
96 var max_ = this.max;
97 var min_ = this.min;
98 var i;
99 for (i = 0; i < this.cursors.length; i++) {
100 var x = this.cursors[i].getX();
101 if (x > max_) max_ = x;
102 if (x < min_) min_ = x;
103 }
104 return max_ - min_ + 1;
68105 };
69106
70107 /*
71 * Draws the Tape in a drawing context.
72 * cellWidth and cellHeight are canvas units of measure for each cell.
108 * Cursored read/write interface
73109 */
74 this.drawContext = function(ctx, offsetX, offsetY, cellWidth, cellHeight) {
75 var me = this;
76 this.foreach(function (pos, elem) {
77 me.drawElement(ctx, offsetX + pos * cellWidth, offsetY,
78 cellWidth, cellHeight, elem);
79 });
110 this.read = function(index) {
111 var cursor = this.cursors[index || 0];
112 return this.get(cursor.getX());
113 };
114
115 this.write = function(value, index) {
116 var cursor = this.cursors[index || 0];
117 this.put(cursor.getX(), value);
118 return this;
80119 };
81120
82121 /*
83 * Draws the Tape, and a set of TapeHeads, on a canvas element.
84 * Resizes the canvas to the needed dimensions.
85 * cellWidth and cellHeight are canvas units of measure for each cell.
122 * Cursored stack interface.
86123 */
87 this.drawCanvas = function(canvas, cellWidth, cellHeight, heads) {
88 var ctx = canvas.getContext('2d');
124 this.push = function(value) {
125 var cursor = this.cursors[0];
126 this.put(cursor.getX(), value); // updates bounds
127 cursor.moveRight();
128 return this;
129 };
89130
90 var width = this.max - this.min + 1;
91 var height = 1;
131 this.pop = function() {
132 var cursor = this.cursors[0];
133 var x = cursor.getX();
134 if (x === 0) {
135 return undefined; // stack underflow
136 }
137 x--;
138 cursor.setX(x);
139 value = this.get(x);
140 this.put(x, this._default);
141 if (x === this.max) { // which it really should be. recalculate bounds
142 this.max--;
143 if (this.max < this.min) {
144 this.max = undefined;
145 this.min = undefined;
146 }
147 }
148 return value;
149 };
92150
93 if (cellWidth === undefined) {
94 ctx.textBaseline = "top";
95 ctx.font = cellHeight + "px monospace";
96 cellWidth = ctx.measureText("@").width;
151 this.peek = function() {
152 var cursor = this.cursors[0];
153 var x = cursor.getX();
154 if (x === 0) {
155 return undefined; // empty stack
97156 }
157 return this.get(x - 1);
158 };
98159
99 canvas.width = width * cellWidth;
100 canvas.height = height * cellHeight;
101
102 ctx.clearRect(0, 0, canvas.width, canvas.height);
103
104 ctx.textBaseline = "top";
105 ctx.font = cellHeight + "px monospace";
106
107 var offsetX = this.min * cellWidth * -1;
108 var offsetY = 0;
109
110 for (var i = 0; i < heads.length; i++) {
111 heads[i].drawContext(
112 ctx,
113 offsetX + heads[i].pos * cellWidth, offsetY,
114 cellWidth, cellHeight
115 );
116 }
117
118 this.drawContext(ctx, offsetX, offsetY, cellWidth, cellHeight);
160 this.getSize = function() {
161 return this.max === undefined ? 0 : this.max + 1;
119162 };
120163
121164 };