git @ Cat's Eye Technologies Braktif / e09ee19
Update yoob.js dependencies to 0.6; use PresetManager. Chris Pressey 6 years ago
4 changed file(s) with 363 addition(s) and 142 deletion(s). Raw diff Collapse all Expand all
66 border: 1px solid blue;
77 }
88 #load {
9 display: none;
10 }
11 .example {
912 display: none;
1013 }
1114 </style>
2124 <button id="step">Step</button>
2225 Speed: <input id="speed" type="range" min="0" max="200" value="0" />
2326
24 <span id="select_source_container">
25 <select id="select_source">
26 <option>(select one...)</option>
27 <option>test</option>
28 <option>test2</option>
29 <option>test3</option>
30 </select>
31 </span>
27 <select id="select_source">
28 <option>(select one...)</option>
29 <option>test</option>
30 <option>test2</option>
31 <option>test3</option>
32 </select>
3233
33 <textarea id="input" rows="10" cols="50">
34 *
35 &lt;&lt;*[--]*
36 000000000000000000 *[----- --]
37 -----------------d-i-- --------
38 </textarea>
34 <textarea id="input" rows="10" cols="50"></textarea>
3935
4036 <pre id="output" style="border: 1px solid blue">
4137 </pre>
4238
43 <div id="test" style="display: none;"
39 <div class="example" id="test"
4440 >000000000000000000 &lt;*&lt;*&gt;*&gt;*&lt;*&lt;*&gt;*&gt;*
4541 -----------------d-i---------------
4642 </div>
4743
4844
49 <div id="test2" style="display: none;"
45 <div class="example" id="test2"
5046 > &lt;*
5147 000000000000000000000000000000000000000000000000000000000 *[---]
5248 --------------------------------------------------------d-i- --
5349 </div>
5450
5551
56 <div id="test3" style="display: none;"
52 <div class="example" id="test3"
5753 > *
5854 &lt;&lt;*[--]*
5955 000000000000000000 *[----- --]
6460 </body>
6561 <script src="yoob/controller.js"></script>
6662 <script src="yoob/playfield.js"></script>
63 <script src="yoob/preset-manager.js"></script>
6764 <script src="../script/braktif.js"></script>
6865 <script>
6966 var output = document.getElementById('output');
9289 'load': 'load',
9390 'edit': 'edit',
9491 'speed': 'speed',
95 'select': 'select_source',
9692 'source': 'input',
9793 'display': 'output'
9894 });
95 var p = (new yoob.PresetManager()).init({
96 selectElem: document.getElementById("select_source"),
97 controller: c
98 }).populateFromClass('example').select('test3');
9999 c.click_load();
100100 </script>
00 /*
1 * This file is part of yoob.js version 0.3-PRE
1 * This file is part of yoob.js version 0.6
2 * Available from https://github.com/catseye/yoob.js/
23 * This file is in the public domain. See http://unlicense.org/ for details.
34 */
45 if (window.yoob === undefined) yoob = {};
56
67 /*
7 * A controller for executing(/animating/evolving) states
8 * (such as esolang program states or cellular automaton
9 * configurations.)
10 *
11 * Can be connected to a UI in the DOM.
12 *
13 * Subclass this and override the following methods:
8 * A controller for executing(/animating/evolving) states such as esolang
9 * program states or cellular automaton configurations. For the sake of
10 * convenience, we will refer to this as the _program state_, even though
11 * it is of course highly adaptable and might not represent a "program".
12 *
13 * The controller can be connected to a UI in the DOM, consisting of:
14 *
15 * - a set of buttons which control the evolution of the state:
16 * - start
17 * - stop
18 * - step
19 * - load
20 * - edit
21 *
22 * - a slider control which adjusts the speed of program state evolution.
23 *
24 * - a `source` element from which an program state can be loaded,
25 * and which is generally assumed to support user-editing of the source.
26 * The `edit` button will cause the `source` to be shown and the `display`
27 * to be hidden, while the `load` button will load the program state from
28 * the `source`, hide the `source`, and show the `display`.
29 *
30 * - a `display` element on which the current program state will be
31 * depicted. Note that the controller is not directly responsible for
32 * rendering the program state; use something like yoob.PlayfieldCanvasView
33 * for that instead. The controller only knows about the `display` in order
34 * to hide it while the `source` is being edited and to show it after the
35 * `source` has been loaded.
36 *
37 * - an `input` element, which provides input to the running program.
38 *
39 * Each of these is optional, and if not configured, will not be used.
40 *
41 * To use a Controller, create a subclass of yoob.Controller and override
42 * the following methods:
1443 * - make it evolve the state by one tick in the step() method
1544 * - make it load the state from a multiline string in the load() method
45 *
46 * In these methods, you will need to store the state (in whatever
47 * representation you find convenient for processing and for depicting on
48 * the `display` in some fashion) somehow. You may store it in a closed-over
49 * private variable, or in an attribute on your controller object.
50 *
51 * If you store in in an attribute on your controller object, you should use
52 * the `.programState` attribute; it is reserved for this purpose.
53 *
54 * You should *not* store it in the `.state` attribute, as a yoob.Controller
55 * uses this to track its own state (yes, it has its own state independent of
56 * the program state. at least potentially.)
1657 */
1758 yoob.Controller = function() {
59 var STOPPED = 0; // the program has terminated (itself)
60 var PAUSED = 1; // the program is ready to step/run (stopped by user)
61 var RUNNING = 2; // the program is running
62 var BLOCKED = 3; // the program is waiting for more input
63
1864 this.intervalId = undefined;
1965 this.delay = 100;
66 this.state = STOPPED;
67
2068 this.source = undefined;
69 this.input = undefined;
70 this.display = undefined;
71
2172 this.speed = undefined;
2273 this.controls = {};
2374
24 this.makeEventHandler = function(control, key) {
75 /*
76 * This is not a public method.
77 */
78 this._makeEventHandler = function(control, key) {
2579 if (this['click_' + key] !== undefined) {
2680 key = 'click_' + key;
2781 }
28 var self = this;
82 var $this = this;
2983 return function(e) {
30 self[key](control);
84 $this[key](control);
3185 };
3286 };
3387
3892 * with those ids will be obtained from the document and used.
3993 */
4094 this.connect = function(dict) {
41 var self = this;
42 var keys = ["start", "stop", "step", "load", "edit", "select"];
95 var $this = this;
96
97 var keys = ["start", "stop", "step", "load", "edit"];
4398 for (var i in keys) {
4499 var key = keys[i];
45100 var value = dict[key];
46101 if (typeof value === 'string') {
47102 value = document.getElementById(value);
48103 }
49 if (value !== undefined) {
50 if (key === 'select') {
51 value.onchange = this.makeEventHandler(value, key);
52 } else {
53 value.onclick = this.makeEventHandler(value, key);
54 }
104 if (value) {
105 value.onclick = this._makeEventHandler(value, key);
55106 this.controls[key] = value;
56107 }
57108 }
58109
59 var keys = ["source", "display"];
110 var keys = ["speed", "source", "input", "display"];
60111 for (var i in keys) {
61112 var key = keys[i];
62113 var value = dict[key];
63114 if (typeof value === 'string') {
64115 value = document.getElementById(value);
65116 }
66 if (value !== undefined) {
117 if (value) {
67118 this[key] = value;
68 }
69 }
70
71 var speed = dict.speed;
72 if (typeof speed === 'string') {
73 speed = document.getElementById(speed);
74 }
75 if (speed !== undefined) {
76 this.speed = speed;
77 speed.value = self.delay;
78 speed.onchange = function(e) {
79 self.delay = speed.value;
80 if (self.intervalId !== undefined) {
81 self.stop();
82 self.start();
119 // special cases
120 if (key === 'speed') {
121 this.speed.value = this.delay;
122 this.speed.onchange = function(e) {
123 $this.delay = speed.value;
124 if ($this.intervalId !== undefined) {
125 $this.stop();
126 $this.start();
127 }
128 }
129 } else if (key === 'input') {
130 this.input.onchange = function(e) {
131 if (this.value.length > 0) {
132 $this.unblock();
133 }
134 }
83135 }
84136 }
85 }
137 }
138
139 this.click_stop();
86140 };
87141
88142 this.click_step = function(e) {
89 this.stop();
90 this.step();
91 };
92
143 if (this.state === STOPPED) return;
144 this.click_stop();
145 this.state = PAUSED;
146 this.performStep();
147 };
148
149 /*
150 * Override this and make it evolve the program state by one tick.
151 * The method may also return a control code string:
152 *
153 * - `stop` to indicate that the program has terminated.
154 * - `block` to indicate that the program is waiting for more input.
155 */
93156 this.step = function() {
94157 alert("step() NotImplementedError");
95158 };
96159
160 this.performStep = function() {
161 var code = this.step();
162 if (code === 'stop') {
163 this.terminate();
164 } else if (code === 'block') {
165 this.state = BLOCKED;
166 }
167 };
168
97169 this.click_load = function(e) {
98 this.stop();
170 this.click_stop();
99171 this.load(this.source.value);
172 this.state = PAUSED;
100173 if (this.controls.edit) this.controls.edit.style.display = "inline";
101174 if (this.controls.load) this.controls.load.style.display = "none";
102175 if (this.controls.start) this.controls.start.disabled = false;
103176 if (this.controls.step) this.controls.step.disabled = false;
104 if (this.controls.stop) this.controls.stop.disabled = false;
177 if (this.controls.stop) this.controls.stop.disabled = true;
105178 if (this.display) this.display.style.display = "block";
106179 if (this.source) this.source.style.display = "none";
107180 };
110183 alert("load() NotImplementedError");
111184 };
112185
186 /*
187 * Loads a source text into the source element.
188 */
189 this.loadSource = function(text) {
190 if (this.source) this.source.value = text;
191 this.load(text);
192 this.state = PAUSED;
193 };
194
195 /*
196 * Loads a source text into the source element.
197 * Assumes it comes from an element in the document, so it translates
198 * the basic HTML escapes (but no others) to plain text.
199 */
200 this.loadSourceFromHTML = function(html) {
201 var text = html;
202 text = text.replace(/\&lt;/g, '<');
203 text = text.replace(/\&gt;/g, '>');
204 text = text.replace(/\&amp;/g, '&');
205 this.loadSource(text);
206 };
207
113208 this.click_edit = function(e) {
114 this.stop();
209 this.click_stop();
115210 if (this.controls.edit) this.controls.edit.style.display = "none";
116211 if (this.controls.load) this.controls.load.style.display = "inline";
117212 if (this.controls.start) this.controls.start.disabled = true;
121216 if (this.source) this.source.style.display = "block";
122217 };
123218
124 this.click_select = function(control) {
125 this.stop();
126 var source = document.getElementById(
127 control.options[control.selectedIndex].value
128 );
129 var text = source.innerHTML;
130 text = text.replace(/\&lt;/g, '<');
131 text = text.replace(/\&gt;/g, '>');
132 text = text.replace(/\&amp;/g, '&');
133 if (this.source) this.source.value = text;
134 this.load(text);
219 this.click_start = function(e) {
220 this.start();
221 if (this.controls.start) this.controls.start.disabled = true;
222 if (this.controls.step) this.controls.step.disabled = false;
223 if (this.controls.stop) this.controls.stop.disabled = false;
135224 };
136225
137226 this.start = function() {
138227 if (this.intervalId !== undefined)
139228 return;
140229 this.step();
141 var self = this;
142 this.intervalId = setInterval(function() { self.step(); }, this.delay);
230 var $this = this;
231 this.intervalId = setInterval(function() {
232 $this.performStep();
233 }, this.delay);
234 this.state = RUNNING;
235 };
236
237 this.click_stop = function(e) {
238 this.stop();
239 this.state = PAUSED;
240 if (this.controls.stop && this.controls.stop.disabled) {
241 return;
242 }
243 if (this.controls.start) this.controls.start.disabled = false;
244 if (this.controls.step) this.controls.step.disabled = false;
245 if (this.controls.stop) this.controls.stop.disabled = true;
246 };
247
248 this.terminate = function(e) {
249 this.stop();
250 this.state = STOPPED;
251 if (this.controls.start) this.controls.start.disabled = true;
252 if (this.controls.step) this.controls.step.disabled = true;
253 if (this.controls.stop) this.controls.stop.disabled = true;
143254 };
144255
145256 this.stop = function() {
00 /*
1 * This file is part of yoob.js version 0.2
1 * This file is part of yoob.js version 0.6
2 * Available from https://github.com/catseye/yoob.js/
23 * This file is in the public domain. See http://unlicense.org/ for details.
34 */
45 if (window.yoob === undefined) yoob = {};
2122 */
2223 this.setDefault = function(v) {
2324 this._default = v;
25 return this;
2426 };
2527
2628 /*
7779 this.minX = undefined;
7880 this.minY = undefined;
7981 this.maxX = undefined;
80 this.maxX = undefined;
82 this.maxY = undefined;
8183
8284 for (var cell in this._store) {
8385 var pos = cell.split(',');
98100 this.minX = undefined;
99101 this.minY = undefined;
100102 this.maxX = undefined;
101 this.maxX = undefined;
103 this.maxY = undefined;
104 };
105
106 /*
107 * Scroll a rectangular subrectangle of this Playfield, up.
108 * TODO: support other directions.
109 */
110 this.scrollRectangleY = function(dy, minX, minY, maxX, maxY) {
111 if (dy < 1) {
112 for (var y = minY; y <= (maxY + dy); y++) {
113 for (var x = minX; x <= maxX; x++) {
114 this.put(x, y, this.get(x, y - dy));
115 }
116 }
117 } else { alert("scrollRectangleY(" + dy + ") notImplemented"); }
118 };
119
120 this.clearRectangle = function(minX, minY, maxX, maxY) {
121 // Could also do this with a foreach that checks
122 // each position. Would be faster on sparser playfields.
123 for (var y = minY; y <= maxY; y++) {
124 for (var x = minX; x <= maxX; x++) {
125 this.put(x, y, undefined);
126 }
127 }
102128 };
103129
104130 /*
216242 };
217243
218244 /*
219 * Draws elements of the Playfield in a drawing context.
220 * x and y are canvas coordinates, and width and height
221 * are canvas units of measure.
222 * The default implementation just renders them as text,
223 * in black.
224 * Override if you wish to draw them differently.
225 */
226 this.drawElement = function(ctx, x, y, cellWidth, cellHeight, elem) {
227 ctx.fillStyle = "black";
228 ctx.fillText(elem.toString(), x, y);
229 };
230
231 /*
232 * Draws the Playfield in a drawing context.
233 * cellWidth and cellHeight are canvas units of measure for each cell.
234 */
235 this.drawContext = function(ctx, offsetX, offsetY, cellWidth, cellHeight) {
236 var me = this;
237 this.foreach(function (x, y, elem) {
238 me.drawElement(ctx, offsetX + x * cellWidth, offsetY + y * cellHeight,
239 cellWidth, cellHeight, elem);
240 });
241 };
242
245 * Accessors for the minimum (resp. maximum) x (resp. y) values of
246 * occupied (non-default-valued) cells in this Playfield. If there are
247 * no cells in this Playfield, these will refturn undefined. Note that
248 * these are not guaranteed to be tight bounds; if values in cells
249 * are deleted, these bounds may still be considered to be outside them.
250 */
251 this.getMinX = function() {
252 return this.minX;
253 };
254 this.getMaxX = function() {
255 return this.maxX;
256 };
257 this.getMinY = function() {
258 return this.minY;
259 };
260 this.getMaxY = function() {
261 return this.maxY;
262 };
263
264 /*
265 * Returns the number of occupied cells in the x direction.
266 */
243267 this.getExtentX = function() {
244268 if (this.maxX === undefined || this.minX === undefined) {
245269 return 0;
248272 }
249273 };
250274
275 /*
276 * Returns the number of occupied cells in the y direction.
277 */
251278 this.getExtentY = function() {
252279 if (this.maxY === undefined || this.minY === undefined) {
253280 return 0;
255282 return this.maxY - this.minY + 1;
256283 }
257284 };
258
259 /*
260 * Draws the Playfield, and a set of Cursors, on a canvas element.
261 * Resizes the canvas to the needed dimensions.
262 * cellWidth and cellHeight are canvas units of measure for each cell.
263 */
264 this.drawCanvas = function(canvas, cellWidth, cellHeight, cursors) {
265 var ctx = canvas.getContext('2d');
266
267 var width = this.getExtentX();
268 var height = this.getExtentY();
269
270 if (cellWidth === undefined) {
271 ctx.textBaseline = "top";
272 ctx.font = cellHeight + "px monospace";
273 cellWidth = ctx.measureText("@").width;
274 }
275
276 canvas.width = width * cellWidth;
277 canvas.height = height * cellHeight;
278
279 ctx.clearRect(0, 0, canvas.width, canvas.height);
280
281 ctx.textBaseline = "top";
282 ctx.font = cellHeight + "px monospace";
283
284 var offsetX = this.minX * cellWidth * -1;
285 var offsetY = this.minY * cellHeight * -1;
286
287 for (var i = 0; i < cursors.length; i++) {
288 cursors[i].drawContext(
289 ctx,
290 cursors[i].x * cellWidth, cursors[i].y * cellHeight,
291 cellWidth, cellHeight
292 );
293 }
294
295 this.drawContext(ctx, offsetX, offsetY, cellWidth, cellHeight);
296 };
297
298285 };
0 /*
1 * This file is part of yoob.js version 0.6
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 * An object for managing a set of "presets" -- which, for an esolang,
9 * might be example programs; for an emulator, might be ROM images;
10 * for a control panel, may be pre-selected combinations of settings;
11 * and so forth.
12 *
13 * Mostly intended to be connected to a yoob.Controller -- but need not be.
14 */
15 yoob.PresetManager = function() {
16 /*
17 * The single argument is a dictionary (object) where the keys are:
18 *
19 * selectElem: (required) the <select> DOM element that will be
20 * populated with the available example programs. Selecting one
21 * will cause the .select() method of this manager to be called.
22 * it will also call .onselect if that method is present.
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.
27 */
28 this.init = function(cfg) {
29 this.selectElem = cfg.selectElem;
30 this.controller = cfg.controller || null;
31 this.clear();
32 var $this = this;
33 this.selectElem.onchange = function() {
34 $this._select(this.options[this.selectedIndex].value);
35 }
36 return this;
37 };
38
39 /*
40 * Removes all options from the selectElem, and their associated data.
41 */
42 this.clear = function() {
43 this.reactTo = {};
44 while (this.selectElem.firstChild) {
45 this.selectElem.removeChild(this.selectElem.firstChild);
46 }
47 this.add('(select one...)', function() {});
48 return this;
49 };
50
51 /*
52 * Adds a preset to this PresetManager. When it is selected,
53 * 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.
57 */
58 this.add = function(id, callback) {
59 var opt = document.createElement("option");
60 opt.text = id;
61 opt.value = id;
62 this.selectElem.options.add(opt);
63 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 };
70 return this;
71 };
72
73 /*
74 * Called by the selectElem's onchange event. For sanity, you should
75 * probably not call this yourself.
76 */
77 this._select = function(id) {
78 this.reactTo[id](id);
79 if (this.onselect) {
80 this.onselect(id);
81 }
82 };
83
84 /*
85 * Call this to programmatically select an item. This will change
86 * the selected option in the selectElem and trigger the appropriate
87 * callback in this PresetManager.
88 */
89 this.select = function(id) {
90 var i = 0;
91 var opt = this.selectElem.options[i];
92 while (opt) {
93 if (opt.value === id) {
94 this.selectElem.selectedIndex = i;
95 this._select(id);
96 return this;
97 }
98 i++;
99 opt = this.selectElem.options[i];
100 }
101 // if not found, select the "(select one...)" option
102 this.selectElem.selectedIndex = 0;
103 return this;
104 };
105
106 /*
107 * When called, every DOM element in the document with the given
108 * class will be considered a preset, and the manager
109 * will be populated with these. Generally the CSS for the class
110 * will have `display: none` and the elements will be <div>s.
111 *
112 * callback is as described for the .add() method.
113 */
114 this.populateFromClass = function(className, callback) {
115 var elements = document.getElementsByClassName(className);
116 for (var i = 0; i < elements.length; i++) {
117 var e = elements[i];
118 this.add(e.id, callback);
119 }
120 return this;
121 };
122 };