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
2 | 2 | <meta charset="utf-8"> |
3 | 3 | <title>Wunnel</title> |
4 | 4 | <style> |
5 | #program { display: none; } | |
6 | #load { display: none; } | |
5 | #display_container { | |
6 | display: flex; | |
7 | } | |
7 | 8 | #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; | |
13 | 19 | } |
14 | 20 | #state_display { |
15 | float: left; | |
16 | margin-left: 1em; | |
21 | margin-left: 1em; | |
22 | display: inline-block; | |
23 | flex: 0 1 auto; | |
17 | 24 | } |
18 | 25 | #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; | |
24 | 31 | } |
25 | 32 | #tape_display { |
26 | border: 1px solid black; | |
33 | border: 1px solid black; | |
27 | 34 | } |
28 | 35 | #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; | |
33 | 40 | } |
34 | #canvas { border: 1px solid blue; display: none; } | |
41 | #canvas { | |
42 | border: 1px solid blue; display: none; | |
43 | } | |
35 | 44 | </style> |
36 | 45 | </head> |
37 | 46 | <body> |
38 | 47 | |
39 | 48 | <h1>Wunnel</h1> |
40 | 49 | |
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> | |
64 | 51 | |
65 | 52 | </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> | |
73 | 54 | <script src="../../../eg/index.js"></script> |
74 | 55 | <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'); | |
116 | 57 | </script> |
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 | } |
0 | 0 | /* |
1 | * This file is part of yoob.js version 0.7-2015.0108 | |
1 | * This file is part of yoob.js version 0.12 | |
2 | 2 | * Available from https://github.com/catseye/yoob.js/ |
3 | 3 | * This file is in the public domain. See http://unlicense.org/ for details. |
4 | 4 | */ |
10 | 10 | * convenience, we will refer to this as the _program state_, even though |
11 | 11 | * it is of course highly adaptable and might not represent a "program". |
12 | 12 | * |
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: | |
14 | 18 | * |
15 | 19 | * - a set of buttons which control the evolution of the state: |
16 | 20 | * - start |
17 | 21 | * - stop |
18 | 22 | * - step |
19 | 23 | * - load |
20 | * - edit | |
21 | 24 | * - reset |
22 | 25 | * |
23 | 26 | * - 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. | |
41 | 27 | * |
42 | 28 | * To use a Controller, create a subclass of yoob.Controller and override |
43 | 29 | * the following methods: |
44 | 30 | * - 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 | |
46 | 32 | * |
47 | 33 | * In these methods, you will need to store the state (in whatever |
48 | 34 | * representation you find convenient for processing and for depicting on |
54 | 40 | * |
55 | 41 | * You should *not* store it in the `.state` attribute, as a yoob.Controller |
56 | 42 | * 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. | |
58 | 74 | */ |
59 | 75 | yoob.Controller = function() { |
60 | 76 | var STOPPED = 0; // the program has terminated (itself) |
62 | 78 | var RUNNING = 2; // the program is running |
63 | 79 | var BLOCKED = 3; // the program is waiting for more input |
64 | 80 | |
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 | ||
76 | 81 | /* |
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'); | |
83 | 110 | 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 | }; | |
86 | 116 | }; |
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 | } | |
87 | 161 | }; |
88 | 162 | |
89 | 163 | /* |
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) { | |
162 | 175 | if (this.state === STOPPED) return; |
163 | this.click_stop(); | |
176 | this.clickStop(); | |
164 | 177 | this.state = PAUSED; |
165 | 178 | 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 | } | |
166 | 189 | }; |
167 | 190 | |
168 | 191 | /* |
173 | 196 | * - `block` to indicate that the program is waiting for more input. |
174 | 197 | */ |
175 | 198 | 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(/\</g, '<'); | |
223 | text = text.replace(/\>/g, '>'); | |
224 | text = text.replace(/\&/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(); | |
270 | 207 | if (this.controls.start) this.controls.start.disabled = true; |
271 | 208 | if (this.controls.step) this.controls.step.disabled = false; |
272 | 209 | if (this.controls.stop) this.controls.stop.disabled = false; |
273 | 210 | }; |
274 | 211 | |
212 | this.performStart = function() { | |
213 | this.start(); | |
214 | }; | |
215 | ||
275 | 216 | this.start = function() { |
276 | 217 | if (this.intervalId !== undefined) |
277 | 218 | return; |
278 | this.step(); | |
219 | this.performStep(); | |
279 | 220 | var $this = this; |
280 | 221 | this.intervalId = setInterval(function() { |
281 | 222 | $this.performStep(); |
283 | 224 | this.state = RUNNING; |
284 | 225 | }; |
285 | 226 | |
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(); | |
293 | 232 | if (this.controls.start) this.controls.start.disabled = false; |
294 | 233 | if (this.controls.step) this.controls.step.disabled = false; |
295 | 234 | if (this.controls.stop) this.controls.stop.disabled = true; |
296 | 235 | }; |
297 | 236 | |
298 | this.terminate = function(e) { | |
237 | this.performStop = function() { | |
299 | 238 | 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; | |
304 | 240 | }; |
305 | 241 | |
306 | 242 | this.stop = function() { |
310 | 246 | this.intervalId = undefined; |
311 | 247 | }; |
312 | 248 | |
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(); | |
316 | 255 | if (this.controls.start) this.controls.start.disabled = false; |
317 | 256 | if (this.controls.step) this.controls.step.disabled = false; |
318 | 257 | if (this.controls.stop) this.controls.stop.disabled = true; |
319 | 258 | }; |
320 | 259 | |
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; | |
327 | 273 | }; |
328 | 274 | }; |
0 | 0 | /* |
1 | * This file is part of yoob.js version 0.7 | |
1 | * This file is part of yoob.js version 0.12 | |
2 | 2 | * Available from https://github.com/catseye/yoob.js/ |
3 | 3 | * This file is in the public domain. See http://unlicense.org/ for details. |
4 | 4 | */ |
11 | 11 | * A direction vector accompanies the position, so the cursor can "know which |
12 | 12 | * way it's headed", but this facility need not be used. |
13 | 13 | * |
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. | |
20 | 17 | */ |
21 | 18 | 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) { | |
23 | 40 | this.x = x; |
41 | return this; | |
42 | }; | |
43 | ||
44 | this.setY = function(y) { | |
24 | 45 | this.y = y; |
46 | return this; | |
47 | }; | |
48 | ||
49 | this.setDx = function(dx) { | |
25 | 50 | this.dx = dx; |
51 | return this; | |
52 | }; | |
53 | ||
54 | this.setDy = function(dy) { | |
26 | 55 | this.dy = dy; |
27 | 56 | return this; |
28 | 57 | }; |
29 | 58 | |
30 | this.clone = function() { | |
31 | return new yoob.Cursor().init(this.x, this.y, this.dx, this.dy); | |
32 | }; | |
59 | /*** Accessors ***/ | |
33 | 60 | |
34 | 61 | this.getX = function() { |
35 | 62 | return this.x; |
39 | 66 | return this.y; |
40 | 67 | }; |
41 | 68 | |
42 | this.setX = function(x) { | |
43 | this.x = x; | |
69 | this.getDx = function() { | |
70 | return this.dx; | |
44 | 71 | }; |
45 | 72 | |
46 | this.setY = function(y) { | |
47 | this.y = y; | |
73 | this.getDy = function() { | |
74 | return this.dy; | |
48 | 75 | }; |
49 | 76 | |
50 | 77 | this.isHeaded = function(dx, dy) { |
51 | 78 | return this.dx === dx && this.dy === dy; |
52 | 79 | }; |
53 | 80 | |
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 | ||
54 | 107 | this.advance = function() { |
55 | 108 | this.x += this.dx; |
56 | 109 | this.y += this.dy; |
110 | return this; | |
57 | 111 | }; |
112 | ||
113 | /*** Orientation ***/ | |
58 | 114 | |
59 | 115 | this.rotateClockwise = function() { |
60 | 116 | if (this.dx === 0 && this.dy === -1) { |
74 | 130 | } else if (this.dx === -1 && this.dy === -1) { |
75 | 131 | this.dx = 0; this.dy = -1; |
76 | 132 | } |
133 | return this; | |
77 | 134 | }; |
78 | 135 | |
79 | 136 | this.rotateCounterclockwise = function() { |
94 | 151 | } else if (this.dx === 1 && this.dy === -1) { |
95 | 152 | this.dx = 0; this.dy = -1; |
96 | 153 | } |
154 | return this; | |
97 | 155 | }; |
98 | 156 | |
99 | 157 | this.rotateDegrees = function(degrees) { |
101 | 159 | this.rotateCounterclockwise(); |
102 | 160 | degrees -= 45; |
103 | 161 | } |
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; | |
146 | 163 | }; |
147 | 164 | } |
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 | }; |
0 | 0 | /* |
1 | * This file is part of yoob.js version 0.6 | |
1 | * This file is part of yoob.js version 0.11 | |
2 | 2 | * Available from https://github.com/catseye/yoob.js/ |
3 | 3 | * This file is in the public domain. See http://unlicense.org/ for details. |
4 | 4 | */ |
7 | 7 | /* |
8 | 8 | * A view (in the MVC sense) for depicting a yoob.Playfield (-compatible) |
9 | 9 | * 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? | |
13 | 10 | */ |
14 | 11 | 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; | |
22 | 15 | return this; |
23 | 16 | }; |
24 | 17 | |
25 | 18 | /*** Chainable setters ***/ |
26 | 19 | |
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; | |
33 | 27 | return this; |
34 | 28 | }; |
35 | 29 | |
36 | 30 | /* |
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. | |
44 | 32 | */ |
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; | |
104 | 36 | }; |
105 | 37 | |
106 | 38 | /* |
107 | 39 | * Override to convert Playfield values to HTML. |
108 | 40 | */ |
109 | 41 | this.render = function(value) { |
42 | if (value === undefined) return ' '; | |
110 | 43 | 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>'; | |
111 | 53 | }; |
112 | 54 | |
113 | 55 | /* |
115 | 57 | */ |
116 | 58 | this.draw = function() { |
117 | 59 | 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++) { | |
119 | 66 | var row = ""; |
120 | for (var x = this.getLowerX(); x <= this.getUpperX(); x++) { | |
67 | for (var x = lowerX; x <= upperX; x++) { | |
121 | 68 | 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); | |
125 | 72 | } |
126 | 73 | } |
127 | 74 | row += rendered; |
0 | 0 | /* |
1 | * This file is part of yoob.js version 0.6 | |
1 | * This file is part of yoob.js version 0.12 | |
2 | 2 | * Available from https://github.com/catseye/yoob.js/ |
3 | 3 | * This file is in the public domain. See http://unlicense.org/ for details. |
4 | 4 | */ |
8 | 8 | * A two-dimensional Cartesian grid of values. |
9 | 9 | */ |
10 | 10 | 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 ***/ | |
17 | 20 | |
18 | 21 | /* |
19 | 22 | * Set the default value for this Playfield. This |
24 | 27 | this._default = v; |
25 | 28 | return this; |
26 | 29 | }; |
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. ***/ | |
27 | 41 | |
28 | 42 | /* |
29 | 43 | * Obtain the value at (x, y). The default value will |
44 | 58 | this.put = function(x, y, value) { |
45 | 59 | var key = x+','+y; |
46 | 60 | 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. | |
47 | 64 | delete this._store[key]; |
48 | 65 | return; |
49 | 66 | } |
101 | 118 | this.minY = undefined; |
102 | 119 | this.maxX = undefined; |
103 | 120 | this.maxY = undefined; |
121 | return this; | |
104 | 122 | }; |
105 | 123 | |
106 | 124 | /* |
114 | 132 | this.put(x, y, this.get(x, y - dy)); |
115 | 133 | } |
116 | 134 | } |
117 | } else { alert("scrollRectangleY(" + dy + ") notImplemented"); } | |
135 | } else { | |
136 | throw new Error("scrollRectangleY(" + dy + ") notImplemented"); | |
137 | } | |
118 | 138 | }; |
119 | 139 | |
120 | 140 | this.clearRectangle = function(minX, minY, maxX, maxY) { |
192 | 212 | * fun is a callback which takes three parameters: |
193 | 213 | * x, y, and value. If this callback returns a value, |
194 | 214 | * 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. | |
196 | 219 | */ |
197 | 220 | this.foreach = function(fun) { |
198 | 221 | for (var y = this.minY; y <= this.maxY; y++) { |
202 | 225 | if (value === undefined) |
203 | 226 | continue; |
204 | 227 | var result = fun(x, y, value); |
228 | // TODO: Playfield.UNDEFINED vs. undefined meaning "no change"? | |
205 | 229 | 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) { | |
209 | 247 | this.put(x, y, result); |
210 | 248 | } |
211 | 249 | } |
282 | 320 | return this.maxY - this.minY + 1; |
283 | 321 | } |
284 | 322 | }; |
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 | }; | |
285 | 407 | }; |
0 | 0 | /* |
1 | * This file is part of yoob.js version 0.6 | |
1 | * This file is part of yoob.js version 0.12 | |
2 | 2 | * Available from https://github.com/catseye/yoob.js/ |
3 | 3 | * This file is in the public domain. See http://unlicense.org/ for details. |
4 | 4 | */ |
21 | 21 | * will cause the .select() method of this manager to be called. |
22 | 22 | * it will also call .onselect if that method is present. |
23 | 23 | * |
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. | |
27 | 27 | */ |
28 | 28 | this.init = function(cfg) { |
29 | 29 | this.selectElem = cfg.selectElem; |
30 | this.controller = cfg.controller || null; | |
30 | if (cfg.setPreset) { | |
31 | this.setPreset = cfg.setPreset; | |
32 | } | |
31 | 33 | this.clear(); |
32 | 34 | var $this = this; |
33 | 35 | this.selectElem.onchange = function() { |
51 | 53 | /* |
52 | 54 | * Adds a preset to this PresetManager. When it is selected, |
53 | 55 | * 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. | |
57 | 58 | */ |
58 | 59 | this.add = function(id, callback) { |
59 | 60 | var opt = document.createElement("option"); |
61 | 62 | opt.value = id; |
62 | 63 | this.selectElem.options.add(opt); |
63 | 64 | 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; | |
70 | 66 | return this; |
67 | }; | |
68 | ||
69 | this.setPreset = function(id) { | |
70 | throw new Error("No default setPreset callback configured"); | |
71 | 71 | }; |
72 | 72 | |
73 | 73 | /* |
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(/\</g, '<'); | |
180 | text = text.replace(/\>/g, '>'); | |
181 | text = text.replace(/\&/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 | }; |
0 | 0 | /* |
1 | * This file is part of yoob.js version 0.3 | |
1 | * This file is part of yoob.js version 0.11 | |
2 | 2 | * Available from https://github.com/catseye/yoob.js/ |
3 | 3 | * This file is in the public domain. See http://unlicense.org/ for details. |
4 | 4 | */ |
6 | 6 | |
7 | 7 | /* |
8 | 8 | * 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? | |
9 | 15 | */ |
10 | 16 | 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 | }; | |
14 | 34 | |
15 | 35 | /* |
16 | 36 | * 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. | |
18 | 38 | */ |
19 | 39 | this.get = function(pos) { |
20 | return this._store[pos]; | |
40 | var val = this._store[pos]; | |
41 | return val === undefined ? this._default : val; | |
21 | 42 | }; |
22 | 43 | |
23 | 44 | /* |
24 | 45 | * Write a new value into the given position. |
25 | 46 | */ |
26 | 47 | 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 | } | |
27 | 53 | if (this.min === undefined || pos < this.min) this.min = pos; |
28 | 54 | if (this.max === undefined || pos > this.max) this.max = pos; |
29 | if (value === undefined) { | |
30 | delete this._store[pos]; | |
31 | } | |
32 | 55 | this._store[pos] = value; |
33 | 56 | }; |
34 | 57 | |
58 | this.size = function() { | |
59 | return this._top; | |
60 | }; | |
61 | ||
35 | 62 | /* |
36 | * Iterate over every defined cell on the Tape | |
63 | * Iterate over every cell on the Tape. | |
37 | 64 | * fun is a callback which takes two parameters: |
38 | 65 | * position and value. If this callback returns a value, |
39 | 66 | * 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. | |
41 | 71 | */ |
42 | this.foreach = function(fun) { | |
72 | this.foreach = function(fun, cfg) { | |
73 | cfg = cfg || {}; | |
74 | var dense = !!cfg.dense; | |
43 | 75 | for (var pos = this.min; pos <= this.max; pos++) { |
44 | 76 | 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 | } | |
47 | 84 | var result = fun(pos, value); |
48 | 85 | if (result !== undefined) { |
49 | if (result === ' ') { | |
50 | result = undefined; | |
51 | } | |
52 | 86 | this.put(pos, result); |
53 | 87 | } |
54 | 88 | } |
55 | 89 | }; |
56 | 90 | |
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; | |
68 | 105 | }; |
69 | 106 | |
70 | 107 | /* |
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 | |
73 | 109 | */ |
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; | |
80 | 119 | }; |
81 | 120 | |
82 | 121 | /* |
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. | |
86 | 123 | */ |
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 | }; | |
89 | 130 | |
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 | }; | |
92 | 150 | |
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 | |
97 | 156 | } |
157 | return this.get(x - 1); | |
158 | }; | |
98 | 159 | |
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; | |
119 | 162 | }; |
120 | 163 | |
121 | 164 | }; |