Initial import of crude yoob.js-based impl of John's Wierd.
Chris Pressey
10 years ago
0 | <!DOCTYPE html> | |
1 | <head> | |
2 | <meta charset="utf-8"> | |
3 | <title>wierd.js</title> | |
4 | <style> | |
5 | #canvas { border: 1px solid blue; } | |
6 | #canvas_viewport { | |
7 | width: 800px; height: 600px; overflow: scroll; border: 1px solid black; | |
8 | } | |
9 | #info { float: right; } | |
10 | #program { | |
11 | display: none; | |
12 | } | |
13 | #load { | |
14 | display: none; | |
15 | } | |
16 | #output { | |
17 | border: 1px solid red; | |
18 | } | |
19 | </style> | |
20 | </head> | |
21 | <body> | |
22 | ||
23 | <h1>Wierd</h1> | |
24 | ||
25 | <button id="load">Load</button> | |
26 | <button id="edit">Edit</button> | |
27 | <button id="start">Start</button> | |
28 | <button id="stop">Stop</button> | |
29 | <button id="step">Step</button> | |
30 | Speed: <input id="speed" type="range" min="0" max="200" value="0" /> | |
31 | ||
32 | <div id="canvas_viewport"> | |
33 | <canvas id="canvas" width="400" height="400"> | |
34 | Your browser doesn't support displaying an HTML5 canvas. | |
35 | </canvas> | |
36 | </div> | |
37 | ||
38 | <pre id="output"></pre> | |
39 | ||
40 | <textarea id="program" rows="25" cols="40"> | |
41 | </textarea> | |
42 | ||
43 | </body> | |
44 | <script src="../src/yoob/controller.js"></script> | |
45 | <script src="../src/yoob/playfield.js"></script> | |
46 | <script src="../src/yoob/playfield-canvas-view.js"></script> | |
47 | <script src="../src/yoob/cursor.js"></script> | |
48 | <script src="../src/yoob/stack.js"></script> | |
49 | <script src="../src/wierd.js"></script> | |
50 | <script> | |
51 | var c = new WierdController(); | |
52 | var v = new yoob.PlayfieldCanvasView; | |
53 | v.init(null, document.getElementById('canvas')); | |
54 | v.setCellDimensions(undefined, 6); | |
55 | c.init(v); | |
56 | c.connect({ | |
57 | 'start': 'start', | |
58 | 'stop': 'stop', | |
59 | 'step': 'step', | |
60 | 'load': 'load', | |
61 | 'edit': 'edit', | |
62 | 'speed': 'speed', | |
63 | 'source': 'program', | |
64 | 'display': 'canvas_viewport' | |
65 | }); | |
66 | c.click_load(); | |
67 | </script> |
0 | "use strict"; | |
1 | ||
2 | /* | |
3 | * requires yoob.Controller | |
4 | * requires yoob.Playfield | |
5 | * requires yoob.Cursor | |
6 | * requires yoob.Stack | |
7 | */ | |
8 | ||
9 | function WierdController() { | |
10 | var intervalId; | |
11 | ||
12 | var pf; | |
13 | var ip; | |
14 | var stack; | |
15 | var output; | |
16 | ||
17 | this.init = function(view) { | |
18 | pf = new yoob.Playfield(); | |
19 | ip = new yoob.Cursor().init(0, 0, 1, 1); | |
20 | stack = new yoob.Stack(); | |
21 | view.pf = pf; | |
22 | this.view = view.setCursors([ip]); | |
23 | output = document.getElementById('output'); | |
24 | }; | |
25 | ||
26 | this.step = function() { | |
27 | if (this.tryAhead(0, true)) { // NOP | |
28 | } else if (this.tryAhead(45, true)) { | |
29 | stack.push(1); | |
30 | //console.log("[PUSH1]"); | |
31 | } else if (this.tryAhead(315, true)) { | |
32 | /* hello.w relies on the fact that | |
33 | if there are <2 elements on the stack, it's a NOP */ | |
34 | if (stack.size() >= 2) { | |
35 | var a = stack.pop(); | |
36 | var b = stack.pop(); | |
37 | stack.push(b - a); | |
38 | //console.log("[SUBT]"); | |
39 | } | |
40 | } else if (this.tryAhead(90, false)) { | |
41 | var a = 0; | |
42 | if (stack.size() > 0) a = stack.pop(); | |
43 | if (a === 0) { | |
44 | ip.rotateDegrees(90); | |
45 | //console.log("[THEN]"); | |
46 | } else { | |
47 | ip.rotateDegrees(180); | |
48 | //console.log("[ELSE]"); | |
49 | } | |
50 | ip.advance(); | |
51 | } else if (this.tryAhead(270, false)) { | |
52 | var a = 0; | |
53 | if (stack.size() > 0) a = stack.pop(); | |
54 | if (a === 0) { | |
55 | ip.rotateDegrees(270); | |
56 | //console.log("[THEN]"); | |
57 | } else { | |
58 | ip.rotateDegrees(180); | |
59 | // console.log("[ELSE]"); | |
60 | } | |
61 | ip.advance(); | |
62 | } else if (this.tryAhead(135, true)) { | |
63 | var a = stack.pop(); | |
64 | if (a !== 0) { /* spec says 0 is GET. in JC's interp, 0 is PUT. */ | |
65 | var x = stack.pop(); | |
66 | var y = stack.pop(); | |
67 | var e = (pf.get(x, y) || ' ').charCodeAt(0); | |
68 | stack.push(e); | |
69 | //console.log("[GET]"); | |
70 | } else { | |
71 | var x = stack.pop(); | |
72 | var y = stack.pop(); | |
73 | var c = String.fromCharCode(stack.pop()); | |
74 | pf.put(x, y, c); | |
75 | //console.log("[PUT]"); | |
76 | } | |
77 | } else if (this.tryAhead(225, true)) { | |
78 | var a = stack.pop(); | |
79 | if (a === 0) { | |
80 | var c = 'x'; | |
81 | if (c == null) { | |
82 | //needsInput = true; | |
83 | //return errors; | |
84 | } | |
85 | stack.push(c.charCodeAt(0)); | |
86 | //console.log("[IN]"); | |
87 | } else { | |
88 | var a = stack.pop(); | |
89 | output.innerHTML += String.fromCharCode(a); | |
90 | //console.log("[OUT]"); | |
91 | } | |
92 | } else { | |
93 | var lookahead = ip.clone(); | |
94 | lookahead.advance(); | |
95 | lookahead.advance(); | |
96 | var there = pf.get(lookahead.x, lookahead.y); | |
97 | if (there != ' ' && there != undefined) { | |
98 | ip.advance(); | |
99 | ip.advance(); | |
100 | //console.log("[SPRK]"); | |
101 | } else { | |
102 | //halted = true; | |
103 | //return errors; | |
104 | } | |
105 | } | |
106 | ||
107 | this.view.draw(); | |
108 | }; | |
109 | ||
110 | this.load = function(text) { | |
111 | pf.clear(); | |
112 | pf.load(1, 1, text); | |
113 | ip.dx = 1; | |
114 | ip.dy = 1; | |
115 | this.view.draw(); | |
116 | }; | |
117 | ||
118 | this.tryAhead = function(degrees, advance) { | |
119 | var lookahead = ip.clone(); | |
120 | lookahead.rotateDegrees(degrees); | |
121 | lookahead.advance(); | |
122 | var there = pf.get(lookahead.x, lookahead.y); | |
123 | //console.log(lookahead.x, lookahead.y, there); | |
124 | if (there != ' ' && there != undefined) { | |
125 | if (advance) { | |
126 | ip.rotateDegrees(degrees); | |
127 | ip.advance(); | |
128 | } | |
129 | return true; | |
130 | } | |
131 | return false; | |
132 | }; | |
133 | }; | |
134 | WierdController.prototype = new yoob.Controller(); |
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 | * 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: | |
43 | * - make it evolve the state by one tick in the step() method | |
44 | * - 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.) | |
57 | */ | |
58 | 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 | ||
64 | this.intervalId = undefined; | |
65 | this.delay = 100; | |
66 | this.state = STOPPED; | |
67 | ||
68 | this.source = undefined; | |
69 | this.input = undefined; | |
70 | this.display = undefined; | |
71 | ||
72 | this.speed = undefined; | |
73 | this.controls = {}; | |
74 | ||
75 | /* | |
76 | * This is not a public method. | |
77 | */ | |
78 | this._makeEventHandler = function(control, key) { | |
79 | if (this['click_' + key] !== undefined) { | |
80 | key = 'click_' + key; | |
81 | } | |
82 | var $this = this; | |
83 | return function(e) { | |
84 | $this[key](control); | |
85 | }; | |
86 | }; | |
87 | ||
88 | /* | |
89 | * Single argument is a dictionary (object) where the keys | |
90 | * are the actions a controller can undertake, and the values | |
91 | * are either DOM elements or strings; if strings, DOM elements | |
92 | * with those ids will be obtained from the document and used. | |
93 | */ | |
94 | this.connect = function(dict) { | |
95 | var $this = this; | |
96 | ||
97 | var keys = ["start", "stop", "step", "load", "edit"]; | |
98 | for (var i in keys) { | |
99 | var key = keys[i]; | |
100 | var value = dict[key]; | |
101 | if (typeof value === 'string') { | |
102 | value = document.getElementById(value); | |
103 | } | |
104 | if (value) { | |
105 | value.onclick = this._makeEventHandler(value, key); | |
106 | this.controls[key] = value; | |
107 | } | |
108 | } | |
109 | ||
110 | var keys = ["speed", "source", "input", "display"]; | |
111 | for (var i in keys) { | |
112 | var key = keys[i]; | |
113 | var value = dict[key]; | |
114 | if (typeof value === 'string') { | |
115 | value = document.getElementById(value); | |
116 | } | |
117 | if (value) { | |
118 | this[key] = value; | |
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 | } | |
135 | } | |
136 | } | |
137 | } | |
138 | ||
139 | this.click_stop(); | |
140 | }; | |
141 | ||
142 | this.click_step = function(e) { | |
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 | */ | |
156 | this.step = function() { | |
157 | alert("step() NotImplementedError"); | |
158 | }; | |
159 | ||
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 | ||
169 | this.click_load = function(e) { | |
170 | this.click_stop(); | |
171 | this.load(this.source.value); | |
172 | this.state = PAUSED; | |
173 | if (this.controls.edit) this.controls.edit.style.display = "inline"; | |
174 | if (this.controls.load) this.controls.load.style.display = "none"; | |
175 | if (this.controls.start) this.controls.start.disabled = false; | |
176 | if (this.controls.step) this.controls.step.disabled = false; | |
177 | if (this.controls.stop) this.controls.stop.disabled = true; | |
178 | if (this.display) this.display.style.display = "block"; | |
179 | if (this.source) this.source.style.display = "none"; | |
180 | }; | |
181 | ||
182 | this.load = function(text) { | |
183 | alert("load() NotImplementedError"); | |
184 | }; | |
185 | ||
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(/\</g, '<'); | |
203 | text = text.replace(/\>/g, '>'); | |
204 | text = text.replace(/\&/g, '&'); | |
205 | this.loadSource(text); | |
206 | }; | |
207 | ||
208 | this.click_edit = function(e) { | |
209 | this.click_stop(); | |
210 | if (this.controls.edit) this.controls.edit.style.display = "none"; | |
211 | if (this.controls.load) this.controls.load.style.display = "inline"; | |
212 | if (this.controls.start) this.controls.start.disabled = true; | |
213 | if (this.controls.step) this.controls.step.disabled = true; | |
214 | if (this.controls.stop) this.controls.stop.disabled = true; | |
215 | if (this.display) this.display.style.display = "none"; | |
216 | if (this.source) this.source.style.display = "block"; | |
217 | }; | |
218 | ||
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; | |
224 | }; | |
225 | ||
226 | this.start = function() { | |
227 | if (this.intervalId !== undefined) | |
228 | return; | |
229 | this.step(); | |
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; | |
254 | }; | |
255 | ||
256 | this.stop = function() { | |
257 | if (this.intervalId === undefined) | |
258 | return; | |
259 | clearInterval(this.intervalId); | |
260 | this.intervalId = undefined; | |
261 | }; | |
262 | }; |
0 | /* | |
1 | * This file is part of yoob.js version 0.7-PRE | |
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 representing a position and direction in some space. The space | |
9 | * may be one-dimensional (a yoob.Tape, or a string representing a program | |
10 | * source) or a two-dimensional Cartesian space (such as a yoob.Playfield.) | |
11 | * A direction vector accompanies the position, so the cursor can "know which | |
12 | * way it's headed", but this facility need not be used. | |
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. | |
20 | */ | |
21 | yoob.Cursor = function() { | |
22 | this.init = function(x, y, dx, dy) { | |
23 | this.x = x; | |
24 | this.y = y; | |
25 | this.dx = dx; | |
26 | this.dy = dy; | |
27 | return this; | |
28 | }; | |
29 | ||
30 | this.clone = function() { | |
31 | return new yoob.Cursor().init(this.x, this.y, this.dx, this.dy); | |
32 | }; | |
33 | ||
34 | this.getX = function() { | |
35 | return this.x; | |
36 | }; | |
37 | ||
38 | this.getY = function() { | |
39 | return this.y; | |
40 | }; | |
41 | ||
42 | this.setX = function(x) { | |
43 | this.x = x; | |
44 | }; | |
45 | ||
46 | this.setY = function(y) { | |
47 | this.y = y; | |
48 | }; | |
49 | ||
50 | this.isHeaded = function(dx, dy) { | |
51 | return this.dx === dx && this.dy === dy; | |
52 | }; | |
53 | ||
54 | this.advance = function() { | |
55 | this.x += this.dx; | |
56 | this.y += this.dy; | |
57 | }; | |
58 | ||
59 | this.rotateClockwise = function() { | |
60 | if (this.dx === 0 && this.dy === -1) { | |
61 | this.dx = 1; this.dy = -1; | |
62 | } else if (this.dx === 1 && this.dy === -1) { | |
63 | this.dx = 1; this.dy = 0; | |
64 | } else if (this.dx === 1 && this.dy === 0) { | |
65 | this.dx = 1; this.dy = 1; | |
66 | } else if (this.dx === 1 && this.dy === 1) { | |
67 | this.dx = 0; this.dy = 1; | |
68 | } else if (this.dx === 0 && this.dy === 1) { | |
69 | this.dx = -1; this.dy = 1; | |
70 | } else if (this.dx === -1 && this.dy === 1) { | |
71 | this.dx = -1; this.dy = 0; | |
72 | } else if (this.dx === -1 && this.dy === 0) { | |
73 | this.dx = -1; this.dy = -1; | |
74 | } else if (this.dx === -1 && this.dy === -1) { | |
75 | this.dx = 0; this.dy = -1; | |
76 | } | |
77 | }; | |
78 | ||
79 | this.rotateCounterclockwise = function() { | |
80 | if (this.dx === 0 && this.dy === -1) { | |
81 | this.dx = -1; this.dy = -1; | |
82 | } else if (this.dx === -1 && this.dy === -1) { | |
83 | this.dx = -1; this.dy = 0; | |
84 | } else if (this.dx === -1 && this.dy === 0) { | |
85 | this.dx = -1; this.dy = 1; | |
86 | } else if (this.dx === -1 && this.dy === 1) { | |
87 | this.dx = 0; this.dy = 1; | |
88 | } else if (this.dx === 0 && this.dy === 1) { | |
89 | this.dx = 1; this.dy = 1; | |
90 | } else if (this.dx === 1 && this.dy === 1) { | |
91 | this.dx = 1; this.dy = 0; | |
92 | } else if (this.dx === 1 && this.dy === 0) { | |
93 | this.dx = 1; this.dy = -1; | |
94 | } else if (this.dx === 1 && this.dy === -1) { | |
95 | this.dx = 0; this.dy = -1; | |
96 | } | |
97 | }; | |
98 | ||
99 | this.rotateDegrees = function(degrees) { | |
100 | while (degrees > 0) { | |
101 | this.rotateCounterclockwise(); | |
102 | degrees -= 45; | |
103 | } | |
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); | |
146 | }; | |
147 | } |
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 | * A view (in the MVC sense) for depicting a yoob.Playfield (-compatible) | |
9 | * object on an HTML5 <canvas> element (or compatible object). | |
10 | * | |
11 | * TODO: don't necesarily resize canvas each time? | |
12 | * TODO: option to stretch content rendering to fill a fixed-size canvas | |
13 | */ | |
14 | yoob.PlayfieldCanvasView = function() { | |
15 | this.init = function(pf, canvas) { | |
16 | this.pf = pf; | |
17 | this.canvas = canvas; | |
18 | this.ctx = canvas.getContext('2d'); | |
19 | this.cursors = []; | |
20 | this.fixedPosition = false; | |
21 | this.fixedSizeCanvas = false; | |
22 | this.drawCursorsFirst = true; | |
23 | this.setCellDimensions(8, 8); | |
24 | return this; | |
25 | }; | |
26 | ||
27 | /*** Chainable setters ***/ | |
28 | ||
29 | /* | |
30 | * Set the list of cursors to the given list of yoob.Cursor (or compatible) | |
31 | * objects. | |
32 | */ | |
33 | this.setCursors = function(cursors) { | |
34 | this.cursors = cursors; | |
35 | return this; | |
36 | }; | |
37 | ||
38 | /* | |
39 | * Set the displayed dimensions of every cell. | |
40 | * cellWidth and cellHeight are canvas units of measure for each cell. | |
41 | * If cellWidth is undefined, the width of a character in the monospace | |
42 | * font of cellHeight pixels is used. | |
43 | */ | |
44 | this.setCellDimensions = function(cellWidth, cellHeight) { | |
45 | this.ctx.textBaseline = "top"; | |
46 | this.ctx.font = cellHeight + "px monospace"; | |
47 | ||
48 | if (cellWidth === undefined) { | |
49 | cellWidth = this.ctx.measureText("@").width; | |
50 | } | |
51 | ||
52 | this.cellWidth = cellWidth; | |
53 | this.cellHeight = cellHeight; | |
54 | return this; | |
55 | }; | |
56 | ||
57 | /* | |
58 | * Return the requested bounds of the occupied portion of the playfield. | |
59 | * "Occupation" in this sense includes all cursors. | |
60 | * | |
61 | * These may return 'undefined' if there is nothing in the playfield. | |
62 | * | |
63 | * Override these if you want to draw some portion of the | |
64 | * playfield which is not the whole playfield. | |
65 | */ | |
66 | this.getLowerX = function() { | |
67 | var minX = this.pf.getMinX(); | |
68 | for (var i = 0; i < this.cursors.length; i++) { | |
69 | if (minX === undefined || this.cursors[i].x < minX) { | |
70 | minX = this.cursors[i].x; | |
71 | } | |
72 | } | |
73 | return minX; | |
74 | }; | |
75 | this.getUpperX = function() { | |
76 | var maxX = this.pf.getMaxX(); | |
77 | for (var i = 0; i < this.cursors.length; i++) { | |
78 | if (maxX === undefined || this.cursors[i].x > maxX) { | |
79 | maxX = this.cursors[i].x; | |
80 | } | |
81 | } | |
82 | return maxX; | |
83 | }; | |
84 | this.getLowerY = function() { | |
85 | var minY = this.pf.getMinY(); | |
86 | for (var i = 0; i < this.cursors.length; i++) { | |
87 | if (minY === undefined || this.cursors[i].y < minY) { | |
88 | minY = this.cursors[i].y; | |
89 | } | |
90 | } | |
91 | return minY; | |
92 | }; | |
93 | this.getUpperY = function() { | |
94 | var maxY = this.pf.getMaxY(); | |
95 | for (var i = 0; i < this.cursors.length; i++) { | |
96 | if (maxY === undefined || this.cursors[i].y > maxY) { | |
97 | maxY = this.cursors[i].y; | |
98 | } | |
99 | } | |
100 | return maxY; | |
101 | }; | |
102 | ||
103 | /* | |
104 | * Returns the number of occupied cells in the x direction. | |
105 | * "Occupation" in this sense includes all cursors. | |
106 | */ | |
107 | this.getExtentX = function() { | |
108 | if (this.getLowerX() === undefined || this.getUpperX() === undefined) { | |
109 | return 0; | |
110 | } else { | |
111 | return this.getUpperX() - this.getLowerX() + 1; | |
112 | } | |
113 | }; | |
114 | ||
115 | /* | |
116 | * Returns the number of occupied cells in the y direction. | |
117 | * "Occupation" in this sense includes all cursors. | |
118 | */ | |
119 | this.getExtentY = function() { | |
120 | if (this.getLowerY() === undefined || this.getUpperY() === undefined) { | |
121 | return 0; | |
122 | } else { | |
123 | return this.getUpperY() - this.getLowerY() + 1; | |
124 | } | |
125 | }; | |
126 | ||
127 | /* | |
128 | * Draws cells of the Playfield in a drawing context. | |
129 | * cellWidth and cellHeight are canvas units of measure. | |
130 | * | |
131 | * The default implementation tries to call a .draw() method on the cell's | |
132 | * value, if one exists, and just renders it as text, in black, if not. | |
133 | * | |
134 | * Override if you wish to draw elements in some other way. | |
135 | */ | |
136 | this.drawCell = function(ctx, value, playfieldX, playfieldY, | |
137 | canvasX, canvasY, cellWidth, cellHeight) { | |
138 | if (value.draw !== undefined) { | |
139 | value.draw(ctx, playfieldX, playfieldY, canvasX, canvasY, | |
140 | cellWidth, cellHeight); | |
141 | } else { | |
142 | ctx.fillStyle = "black"; | |
143 | ctx.fillText(value.toString(), canvasX, canvasY); | |
144 | } | |
145 | }; | |
146 | ||
147 | /* | |
148 | * Draws the Playfield in a drawing context. | |
149 | * cellWidth and cellHeight are canvas units of measure for each cell. | |
150 | * offsetX and offsetY are canvas units of measure for the top-left | |
151 | * of the entire playfield. | |
152 | */ | |
153 | this.drawContext = function(ctx, offsetX, offsetY, cellWidth, cellHeight) { | |
154 | var self = this; | |
155 | this.pf.foreach(function (x, y, value) { | |
156 | self.drawCell(ctx, value, x, y, | |
157 | offsetX + x * cellWidth, offsetY + y * cellHeight, | |
158 | cellWidth, cellHeight); | |
159 | }); | |
160 | }; | |
161 | ||
162 | this.drawCursors = function(ctx, offsetX, offsetY, cellWidth, cellHeight) { | |
163 | var cursors = this.cursors; | |
164 | for (var i = 0; i < cursors.length; i++) { | |
165 | cursors[i].drawContext( | |
166 | ctx, | |
167 | offsetX + cursors[i].x * cellWidth, | |
168 | offsetY + cursors[i].y * cellHeight, | |
169 | cellWidth, cellHeight | |
170 | ); | |
171 | } | |
172 | }; | |
173 | ||
174 | /* | |
175 | * Draw the Playfield, and its set of Cursors, on the canvas element. | |
176 | * Resizes the canvas to the needed dimensions first. | |
177 | */ | |
178 | this.draw = function() { | |
179 | var canvas = this.canvas; | |
180 | var ctx = canvas.getContext('2d'); | |
181 | var cellWidth = this.cellWidth; | |
182 | var cellHeight = this.cellHeight; | |
183 | var cursors = this.cursors; | |
184 | ||
185 | var width = this.getExtentX(); | |
186 | var height = this.getExtentY(); | |
187 | ||
188 | canvas.width = width * cellWidth; | |
189 | canvas.height = height * cellHeight; | |
190 | ||
191 | this.ctx.textBaseline = "top"; | |
192 | this.ctx.font = cellHeight + "px monospace"; | |
193 | ||
194 | var offsetX = 0; | |
195 | var offsetY = 0; | |
196 | ||
197 | if (!this.fixedPosition) { | |
198 | offsetX = (this.getLowerX() || 0) * cellWidth * -1; | |
199 | offsetY = (this.getLowerY() || 0) * cellHeight * -1; | |
200 | } | |
201 | ||
202 | if (this.drawCursorsFirst) { | |
203 | this.drawCursors(ctx, offsetX, offsetY, cellWidth, cellHeight); | |
204 | } | |
205 | ||
206 | this.drawContext(ctx, offsetX, offsetY, cellWidth, cellHeight); | |
207 | ||
208 | if (!this.drawCursorsFirst) { | |
209 | this.drawCursors(ctx, offsetX, offsetY, cellWidth, cellHeight); | |
210 | } | |
211 | }; | |
212 | ||
213 | }; |
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 | * A two-dimensional Cartesian grid of values. | |
9 | */ | |
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; | |
17 | ||
18 | /* | |
19 | * Set the default value for this Playfield. This | |
20 | * value is returned by get() for any cell that was | |
21 | * never written to, or had `undefined` put() into it. | |
22 | */ | |
23 | this.setDefault = function(v) { | |
24 | this._default = v; | |
25 | return this; | |
26 | }; | |
27 | ||
28 | /* | |
29 | * Obtain the value at (x, y). The default value will | |
30 | * be returned if the cell was never written to. | |
31 | */ | |
32 | this.get = function(x, y) { | |
33 | var v = this._store[x+','+y]; | |
34 | if (v === undefined) return this._default; | |
35 | return v; | |
36 | }; | |
37 | ||
38 | /* | |
39 | * Write a new value into (x, y). Note that writing | |
40 | * `undefined` into a cell has the semantics of deleting | |
41 | * the value at that cell; a subsequent get() for that | |
42 | * location will return this Playfield's default value. | |
43 | */ | |
44 | this.put = function(x, y, value) { | |
45 | var key = x+','+y; | |
46 | if (value === undefined || value === this._default) { | |
47 | delete this._store[key]; | |
48 | return; | |
49 | } | |
50 | if (this.minX === undefined || x < this.minX) this.minX = x; | |
51 | if (this.maxX === undefined || x > this.maxX) this.maxX = x; | |
52 | if (this.minY === undefined || y < this.minY) this.minY = y; | |
53 | if (this.maxY === undefined || y > this.maxY) this.maxY = y; | |
54 | this._store[key] = value; | |
55 | }; | |
56 | ||
57 | /* | |
58 | * Like put(), but does not update the playfield bounds. Do | |
59 | * this if you must do a batch of put()s in a more efficient | |
60 | * manner; after doing so, call recalculateBounds(). | |
61 | */ | |
62 | this.putDirty = function(x, y, value) { | |
63 | var key = x+','+y; | |
64 | if (value === undefined || value === this._default) { | |
65 | delete this._store[key]; | |
66 | return; | |
67 | } | |
68 | this._store[key] = value; | |
69 | }; | |
70 | ||
71 | /* | |
72 | * Recalculate the bounds (min/max X/Y) which are tracked | |
73 | * internally to support methods like foreach(). This is | |
74 | * not needed *unless* you've used putDirty() at some point. | |
75 | * (In which case, call this immediately after your batch | |
76 | * of putDirty()s.) | |
77 | */ | |
78 | this.recalculateBounds = function() { | |
79 | this.minX = undefined; | |
80 | this.minY = undefined; | |
81 | this.maxX = undefined; | |
82 | this.maxY = undefined; | |
83 | ||
84 | for (var cell in this._store) { | |
85 | var pos = cell.split(','); | |
86 | var x = parseInt(pos[0], 10); | |
87 | var y = parseInt(pos[1], 10); | |
88 | if (this.minX === undefined || x < this.minX) this.minX = x; | |
89 | if (this.maxX === undefined || x > this.maxX) this.maxX = x; | |
90 | if (this.minY === undefined || y < this.minY) this.minY = y; | |
91 | if (this.maxY === undefined || y > this.maxY) this.maxY = y; | |
92 | } | |
93 | }; | |
94 | ||
95 | /* | |
96 | * Clear the contents of this Playfield. | |
97 | */ | |
98 | this.clear = function() { | |
99 | this._store = {}; | |
100 | this.minX = undefined; | |
101 | this.minY = undefined; | |
102 | 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 | } | |
128 | }; | |
129 | ||
130 | /* | |
131 | * Load a string into this Playfield. | |
132 | * The string may be multiline, with newline (ASCII 10) | |
133 | * characters delimiting lines. ASCII 13 is ignored. | |
134 | * | |
135 | * If transformer is given, it should be a one-argument | |
136 | * function which accepts a character and returns the | |
137 | * object you wish to write into the playfield upon reading | |
138 | * that character. | |
139 | */ | |
140 | this.load = function(x, y, string, transformer) { | |
141 | var lx = x; | |
142 | var ly = y; | |
143 | if (transformer === undefined) { | |
144 | transformer = function(c) { | |
145 | if (c === ' ') { | |
146 | return undefined; | |
147 | } else { | |
148 | return c; | |
149 | } | |
150 | } | |
151 | } | |
152 | for (var i = 0; i < string.length; i++) { | |
153 | var c = string.charAt(i); | |
154 | if (c === '\n') { | |
155 | lx = x; | |
156 | ly++; | |
157 | } else if (c === '\r') { | |
158 | } else { | |
159 | this.putDirty(lx, ly, transformer(c)); | |
160 | lx++; | |
161 | } | |
162 | } | |
163 | this.recalculateBounds(); | |
164 | }; | |
165 | ||
166 | /* | |
167 | * Convert this Playfield to a multi-line string. Each row | |
168 | * is a line, delimited with a newline (ASCII 10). | |
169 | * | |
170 | * If transformer is given, it should be a one-argument | |
171 | * function which accepts a playfield element and returns a | |
172 | * character (or string) you wish to place in the resulting | |
173 | * string for that element. | |
174 | */ | |
175 | this.dump = function(transformer) { | |
176 | var text = ""; | |
177 | if (transformer === undefined) { | |
178 | transformer = function(c) { return c; } | |
179 | } | |
180 | for (var y = this.minY; y <= this.maxY; y++) { | |
181 | var row = ""; | |
182 | for (var x = this.minX; x <= this.maxX; x++) { | |
183 | row += transformer(this.get(x, y)); | |
184 | } | |
185 | text += row + "\n"; | |
186 | } | |
187 | return text; | |
188 | }; | |
189 | ||
190 | /* | |
191 | * Iterate over every defined cell in the Playfield. | |
192 | * fun is a callback which takes three parameters: | |
193 | * x, y, and value. If this callback returns a value, | |
194 | * it is written into the Playfield at that position. | |
195 | * This function ensures a particular order. | |
196 | */ | |
197 | this.foreach = function(fun) { | |
198 | for (var y = this.minY; y <= this.maxY; y++) { | |
199 | for (var x = this.minX; x <= this.maxX; x++) { | |
200 | var key = x+','+y; | |
201 | var value = this._store[key]; | |
202 | if (value === undefined) | |
203 | continue; | |
204 | var result = fun(x, y, value); | |
205 | if (result !== undefined) { | |
206 | if (result === ' ') { | |
207 | result = undefined; | |
208 | } | |
209 | this.put(x, y, result); | |
210 | } | |
211 | } | |
212 | } | |
213 | }; | |
214 | ||
215 | /* | |
216 | * Analogous to (monoid) map in functional languages, | |
217 | * iterate over this Playfield, transform each value using | |
218 | * a supplied function, and write the transformed value into | |
219 | * a destination Playfield. | |
220 | * | |
221 | * Supplied function should take a Playfield (this Playfield), | |
222 | * x, and y, and return a value. | |
223 | * | |
224 | * The map source may extend beyond the internal bounds of | |
225 | * the Playfield, by giving the min/max Dx/Dy arguments | |
226 | * (which work like margin offsets.) | |
227 | * | |
228 | * Useful for evolving a cellular automaton playfield. In this | |
229 | * case, min/max Dx/Dy should be computed from the neighbourhood. | |
230 | */ | |
231 | this.map = function(destPf, fun, minDx, minDy, maxDx, maxDy) { | |
232 | if (minDx === undefined) minDx = 0; | |
233 | if (minDy === undefined) minDy = 0; | |
234 | if (maxDx === undefined) maxDx = 0; | |
235 | if (maxDy === undefined) maxDy = 0; | |
236 | for (var y = this.minY + minDy; y <= this.maxY + maxDy; y++) { | |
237 | for (var x = this.minX + minDx; x <= this.maxX + maxDx; x++) { | |
238 | destPf.putDirty(x, y, fun(pf, x, y)); | |
239 | } | |
240 | } | |
241 | destPf.recalculateBounds(); | |
242 | }; | |
243 | ||
244 | /* | |
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 | */ | |
267 | this.getExtentX = function() { | |
268 | if (this.maxX === undefined || this.minX === undefined) { | |
269 | return 0; | |
270 | } else { | |
271 | return this.maxX - this.minX + 1; | |
272 | } | |
273 | }; | |
274 | ||
275 | /* | |
276 | * Returns the number of occupied cells in the y direction. | |
277 | */ | |
278 | this.getExtentY = function() { | |
279 | if (this.maxY === undefined || this.minY === undefined) { | |
280 | return 0; | |
281 | } else { | |
282 | return this.maxY - this.minY + 1; | |
283 | } | |
284 | }; | |
285 | }; |
0 | /* | |
1 | * This file is part of yoob.js version 0.7-PRE | |
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 (theoretically) unbounded first-in first-out stack. | |
9 | */ | |
10 | yoob.Stack = function() { | |
11 | this._store = {}; | |
12 | this._top = 0; | |
13 | ||
14 | this.pop = function() { | |
15 | if (this._top === 0) { | |
16 | return undefined; | |
17 | } | |
18 | var result = this._store[this._top]; | |
19 | this._top -= 1; | |
20 | return result; | |
21 | }; | |
22 | ||
23 | this.push = function(value) { | |
24 | this._top += 1; | |
25 | this._store[this._top] = value; | |
26 | }; | |
27 | ||
28 | this.size = function() { | |
29 | return this._top; | |
30 | }; | |
31 | ||
32 | /* | |
33 | * Iterate over every value on the stack, top to bottom. | |
34 | * fun is a callback which takes two parameters: | |
35 | * position (0 === top of stack) and value. | |
36 | */ | |
37 | this.foreach = function(fun) { | |
38 | for (var pos = this._top; pos > 0; pos--) { | |
39 | fun(this._top - pos, this._store[pos]); | |
40 | } | |
41 | }; | |
42 | ||
43 | /* | |
44 | * Draws elements of the Stack in a drawing context. | |
45 | * x and y are canvas coordinates, and width and height | |
46 | * are canvas units of measure. | |
47 | * The default implementation just renders them as text, | |
48 | * in black. | |
49 | * Override if you wish to draw them differently. | |
50 | */ | |
51 | this.drawElement = function(ctx, x, y, cellWidth, cellHeight, elem) { | |
52 | ctx.fillStyle = "black"; | |
53 | ctx.fillText(elem.toString(), x, y); | |
54 | }; | |
55 | ||
56 | /* | |
57 | * Draws the Stack in a drawing context. | |
58 | * cellWidth and cellHeight are canvas units of measure for each cell. | |
59 | */ | |
60 | this.drawContext = function(ctx, cellWidth, cellHeight) { | |
61 | var $this = this; | |
62 | this.foreach(function (pos, elem) { | |
63 | $this.drawElement(ctx, 0, pos * cellHeight, | |
64 | cellWidth, cellHeight, elem); | |
65 | }); | |
66 | }; | |
67 | ||
68 | /* | |
69 | * Draws the Stack on a canvas element. | |
70 | * Resizes the canvas to the needed dimensions. | |
71 | * cellWidth and cellHeight are canvas units of measure for each cell. | |
72 | */ | |
73 | this.drawCanvas = function(canvas, cellWidth, cellHeight) { | |
74 | var ctx = canvas.getContext('2d'); | |
75 | ||
76 | var width = 1; | |
77 | var height = this._top; | |
78 | ||
79 | if (cellWidth === undefined) { | |
80 | ctx.textBaseline = "top"; | |
81 | ctx.font = cellHeight + "px monospace"; | |
82 | cellWidth = ctx.measureText("@").width; | |
83 | } | |
84 | ||
85 | canvas.width = width * cellWidth; | |
86 | canvas.height = height * cellHeight; | |
87 | ||
88 | ctx.clearRect(0, 0, canvas.width, canvas.height); | |
89 | ||
90 | ctx.textBaseline = "top"; | |
91 | ctx.font = cellHeight + "px monospace"; | |
92 | ||
93 | this.drawContext(ctx, cellWidth, cellHeight); | |
94 | }; | |
95 | }; |