git @ Cat's Eye Technologies Jaccia / master
Merge pull request #1 from catseye/develop-1.1-2018.1 Develop 1.1 2018.1 Chris Pressey authored 5 years ago GitHub committed 5 years ago
12 changed file(s) with 1254 addition(s) and 755 deletion(s). Raw diff Collapse all Expand all
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/playfield-canvas-view.js",
10 "yoob/controller.js",
11 "yoob/source-manager.js",
12 "yoob/preset-manager.js",
13 "../script/jaccia.js"
14 ];
15 var loaded = 0;
16 var onload = function() {
17 if (++loaded < deps.length) return;
18 /* ----- launch, phase 1: create the UI ----- */
19 var controlPanel = yoob.makeDiv(container);
20 controlPanel.id = "panel_container";
21
22 var subPanel = yoob.makeDiv(container);
23 var selectConfiguration = yoob.makeSelect(subPanel, 'example configuration:', []);
24
25 yoob.makeLineBreak(subPanel);
26 var changeDepiction;
27 var selectDepiction = yoob.makeSelect(subPanel, 'depict using:', [
28 ['text', 'text'],
29 ['canvas', 'canvas']
30 ], function(value) {
31 changeDepiction(value);
32 });
33
34 var displayContainer = yoob.makeDiv(container);
35 displayContainer.id = 'display_container';
36
37 var displayText = yoob.makePre(displayContainer);
38
39 var displayCanvas = yoob.makeCanvas(displayContainer);
40
41 var editor = yoob.makeTextArea(displayContainer, 40, 25);
42
43 var htmlView = new yoob.PlayfieldHTMLView().init({
44 element: displayText
45 });
46 htmlView.draw = function() {
47 this.element.innerHTML = this.pf.dump(dumpMapper);
48 };
49
50 // CanvasView
51 var colourMap = {
52 'Space': '#ffffff',
53 'Wall': '#000000',
54 'Slime': '#00a000',
55 'Food2': '#ff0000',
56 'Food': '#0000ff'
57 };
58 var canvasView = new yoob.PlayfieldCanvasView().init({
59 canvas: displayCanvas
60 });
61 canvasView.drawCell = function(ctx, value, playfieldX, playfieldY,
62 canvasX, canvasY, cellWidth, cellHeight) {
63 ctx.fillStyle = colourMap[value] || '#ffffff';
64 ctx.fillRect(canvasX, canvasY, cellWidth, cellHeight);
65 };
66
67 // Playfield
68 var pf;
69
70 // "View Manager"
71 var currentView = 'text';
72 var views = {
73 'text': htmlView,
74 'canvas': canvasView
75 };
76 var draw = function(pf) {
77 views[currentView].setPlayfield(pf);
78 views[currentView].draw();
79 };
80 changeDepiction = function(value) {
81 if (value === 'text') {
82 displayText.style.display = 'block';
83 displayCanvas.style.display = 'none';
84 } else {
85 displayText.style.display = 'none';
86 displayCanvas.style.display = 'block';
87 }
88 currentView = value;
89 draw(pf);
90 }
91
92 /* ----- launch, phase 2: connect the controller ----- */
93 var controller = (new yoob.Controller()).init({
94 panelContainer: controlPanel,
95 step: function() {
96 var newPf = (new yoob.Playfield()).init({ defaultValue: 'Space' });
97 evolve_playfield(pf, newPf);
98 pf = newPf;
99 draw(pf);
100 },
101 reset: function(text) {
102 pf = (new yoob.Playfield()).init({ defaultValue: 'Space' });
103 pf.load(0, 0, text, loadMapper);
104 draw(pf);
105 }
106 });
107 controller.clickStop();
108
109 var sourceManager = (new yoob.SourceManager()).init({
110 panelContainer: controlPanel,
111 editor: editor,
112 hideDuringEdit: [displayText, displayCanvas],
113 disableDuringEdit: [controller.panel],
114 storageKey: 'jaccia.alp',
115 onDone: function() {
116 controller.performReset(this.getEditorText());
117 }
118 });
119 var p = (new yoob.PresetManager()).init({
120 selectElem: selectConfiguration,
121 controller: controller
122 }).populateFromPairs(sourceManager, exampleConfigurations);
123 };
124 for (var i = 0; i < deps.length; i++) {
125 var elem = document.createElement('script');
126 elem.src = prefix + deps[i];
127 elem.onload = onload;
128 document.body.appendChild(elem);
129 }
130 }
11 <head>
22 <meta charset="utf-8">
33 <title>Jaccia Demo</title>
4 <style>
5 .example {
6 display: none;
7 }
8 </style>
94 </head>
105 <body>
116
127 <h1>Jaccia Demo</h1>
138
14 <button id="load">Load</button>
15 <button id="edit">Edit</button>
16 <button id="start">Start</button>
17 <button id="stop">Stop</button>
18 <button id="step">Step</button>
19 <button id="reset">Reset</button>
20 Speed: <input id="speed" type="range" min="0" max="200" value="0" />
21
22 Starting configuration: <select id="select_source"></select>
23
24 <textarea id="input" rows="10"></textarea>
25
26 <div id="display_container">
27 Depict with:
28 <select id="select_depiction">
29 <option>text</option>
30 <option>canvas</option>
31 </select>
32
33 <pre id="display_text"></pre>
34 <canvas id="display_canvas" width="400" height="250">
35 There would be an HTML5 canvas here, but your browser isn't displaying it.
36 </canvas>
37 </div>
38
39
40 <div class="example" id="solveit"
41 >########S##
42 #:::::::::#
43 #:###:###:#
44 #:#:#:::#:#
45 #:#:#:#:###
46 #:::#:#:#:#
47 #####:#:#:#
48 #:#:::#:::#
49 #:#:###:###
50 #:::#:::::#
51 #########F#
52 </div>
53
54
55 <div class="example" id="nonunique"
56 >####S####
57 #:::::::#
58 #:#####:#
59 #:#:#:#:#
60 #:#:#:#:#
61 #:::::::#
62 ####F####
63 </div>
64
9 <div id="container"></div>
6510
6611 </body>
67 <script src="yoob/controller.js"></script>
68 <script src="yoob/playfield.js"></script>
69 <script src="yoob/playfield-html-view.js"></script>
70 <script src="yoob/playfield-canvas-view.js"></script>
71 <script src="yoob/preset-manager.js"></script>
72 <script src="../script/jaccia.js"></script>
12 <script src="../eg/index.js"></script>
13 <script src="jaccia-launcher.js"></script>
7314 <script>
74 var pf;
75
76 // Text view
77 var displayText = document.getElementById('display_text');
78 var htmlView = new yoob.PlayfieldHTMLView().init(pf, displayText);
79 htmlView.render = function(state) {
80 return dumpMapper(state);
81 };
82
83 // Canvas view
84 var displayCanvas = document.getElementById('display_canvas');
85 var colourMap = {
86 'Space': '#ffffff',
87 'Wall': '#000000',
88 'Slime': '#00a000',
89 'Food2': '#ff0000',
90 'Food': '#0000ff'
91 };
92 var canvasView = new yoob.PlayfieldCanvasView().init(pf, displayCanvas);
93 canvasView.drawCell = function(ctx, value, playfieldX, playfieldY,
94 canvasX, canvasY, cellWidth, cellHeight) {
95 ctx.fillStyle = colourMap[value] || '#ffffff';
96 ctx.fillRect(canvasX, canvasY, cellWidth, cellHeight);
97 };
98
99 // "View Manager"
100 var currentView = 'text';
101 var views = {
102 'text': htmlView,
103 'canvas': canvasView
104 };
105 var draw = function() {
106 views[currentView].pf = pf;
107 views[currentView].draw();
108 };
109
110 var selectDepiction = document.getElementById('select_depiction');
111 selectDepiction.onchange = function() {
112 var value = selectDepiction.options[selectDepiction.selectedIndex].value;
113 if (value === 'text') {
114 displayText.style.display = 'block';
115 displayCanvas.style.display = 'none';
116 } else {
117 displayText.style.display = 'none';
118 displayCanvas.style.display = 'block';
119 }
120 currentView = value;
121 draw();
122 };
123
124 // Controller. We don't subclass, we just monkeypatch.
125 var c = new yoob.Controller();
126
127 c.load = function(text) {
128 pf = new yoob.Playfield();
129 pf.setDefault('Space');
130 pf.load(0, 0, text, loadMapper);
131 draw();
132 };
133
134 c.step = function() {
135 var newPf = new yoob.Playfield();
136 newPf.setDefault('Space');
137 evolve_playfield(pf, newPf);
138 pf = newPf;
139 draw();
140 };
141
142 c.connect({
143 'start': 'start',
144 'stop': 'stop',
145 'step': 'step',
146 'load': 'load',
147 'edit': 'edit',
148 'reset': 'reset',
149 'speed': 'speed',
150 'source': 'input',
151 'display': 'display_container'
152 });
153
154 var p = (new yoob.PresetManager()).init({
155 selectElem: document.getElementById("select_source"),
156 controller: c
157 }).populateFromClass('example').select('test3');
158
159 c.click_load();
15 launch('', 'container');
16016 </script>
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/playfield-canvas-view.js",
10 "yoob/controller.js",
11 "yoob/source-manager.js",
12 "yoob/preset-manager.js",
13 "../script/jacciata.js"
14 ];
15 var loaded = 0;
16 var onload = function() {
17 if (++loaded < deps.length) return;
18 /* ----- launch, phase 1: create the UI ----- */
19 var controlPanel = yoob.makeDiv(container);
20 controlPanel.id = "panel_container";
21
22 var subPanel = yoob.makeDiv(container);
23 var selectConfiguration = yoob.makeSelect(subPanel, 'example configuration:', []);
24
25 yoob.makeLineBreak(subPanel);
26 var changeDepiction;
27 var selectDepiction = yoob.makeSelect(subPanel, 'depict using:', [
28 ['text', 'text'],
29 ['canvas', 'canvas']
30 ], function(value) {
31 changeDepiction(value);
32 });
33
34 var displayContainer = yoob.makeDiv(container);
35 displayContainer.id = 'display_container';
36
37 var displayText = yoob.makePre(displayContainer);
38
39 var displayCanvas = yoob.makeCanvas(displayContainer);
40
41 var editor = yoob.makeTextArea(displayContainer, 40, 25);
42
43 var htmlView = new yoob.PlayfieldHTMLView().init({
44 element: displayText
45 });
46 htmlView.draw = function() {
47 this.element.innerHTML = this.pf.dump(dumpMapper);
48 };
49
50 // CanvasView
51 var colourMap = {
52 'Space': '#ffffff',
53 'Wall': '#000000',
54 'Slime': '#00a000',
55 'Solved': '#00ff40',
56 'Finish': '#0000ff',
57 'Start': '#ff0000',
58 'Head': '#a0c0a0',
59 'Body': '#80a080'
60 };
61 var canvasView = new yoob.PlayfieldCanvasView().init({
62 canvas: displayCanvas
63 });
64 canvasView.drawCell = function(ctx, value, playfieldX, playfieldY,
65 canvasX, canvasY, cellWidth, cellHeight) {
66 ctx.fillStyle = colourMap[value] || '#ffffff';
67 ctx.fillRect(canvasX, canvasY, cellWidth, cellHeight);
68 };
69
70 // Playfield
71 var pf;
72
73 // "View Manager"
74 var currentView = 'text';
75 var views = {
76 'text': htmlView,
77 'canvas': canvasView
78 };
79 var draw = function(pf) {
80 views[currentView].setPlayfield(pf);
81 views[currentView].draw();
82 };
83 changeDepiction = function(value) {
84 if (value === 'text') {
85 displayText.style.display = 'block';
86 displayCanvas.style.display = 'none';
87 } else {
88 displayText.style.display = 'none';
89 displayCanvas.style.display = 'block';
90 }
91 currentView = value;
92 draw(pf);
93 }
94
95 /* ----- launch, phase 2: connect the controller ----- */
96 var controller = (new yoob.Controller()).init({
97 panelContainer: controlPanel,
98 step: function() {
99 var newPf = (new yoob.Playfield()).init({ defaultValue: 'Space' });
100 evolve_playfield(pf, newPf);
101 pf = newPf;
102 draw(pf);
103 },
104 reset: function(text) {
105 pf = (new yoob.Playfield()).init({ defaultValue: 'Space' });
106 pf.load(0, 0, text, loadMapper);
107 draw(pf);
108 }
109 });
110 controller.clickStop();
111
112 var sourceManager = (new yoob.SourceManager()).init({
113 panelContainer: controlPanel,
114 editor: editor,
115 hideDuringEdit: [displayText, displayCanvas],
116 disableDuringEdit: [controller.panel],
117 storageKey: 'jacciata.alp',
118 onDone: function() {
119 controller.performReset(this.getEditorText());
120 }
121 });
122 var p = (new yoob.PresetManager()).init({
123 selectElem: selectConfiguration,
124 controller: controller
125 }).populateFromPairs(sourceManager, exampleConfigurations);
126 };
127 for (var i = 0; i < deps.length; i++) {
128 var elem = document.createElement('script');
129 elem.src = prefix + deps[i];
130 elem.onload = onload;
131 document.body.appendChild(elem);
132 }
133 }
11 <head>
22 <meta charset="utf-8">
33 <title>Jacciata Demo</title>
4 <style>
5 .example {
6 display: none;
7 }
8 </style>
94 </head>
105 <body>
116
127 <h1>Jacciata Demo</h1>
138
14 <button id="load">Load</button>
15 <button id="edit">Edit</button>
16 <button id="start">Start</button>
17 <button id="stop">Stop</button>
18 <button id="step">Step</button>
19 <button id="reset">Reset</button>
20 Speed: <input id="speed" type="range" min="0" max="200" value="0" />
21
22 Starting configuration: <select id="select_source"></select>
23
24 <textarea id="input" rows="10"></textarea>
25
26 <div id="display_container">
27 Depict with:
28 <select id="select_depiction">
29 <option>text</option>
30 <option>canvas</option>
31 </select>
32
33 <pre id="display_text"></pre>
34 <canvas id="display_canvas" width="400" height="250">
35 There would be an HTML5 canvas here, but your browser isn't displaying it.
36 </canvas>
37 </div>
38
39
40 <div class="example" id="solveit"
41 >########S##
42 #:::::::::#
43 #:###:###:#
44 #:#:#:::#:#
45 #:#:#:#:###
46 #:::#:#:#:#
47 #####:#:#:#
48 #:#:::#:::#
49 #:#:###:###
50 #:::#:::::#
51 #########F#
52 </div>
53
54
55 <div class="example" id="nonunique"
56 >####S####
57 #:::::::#
58 #:#####:#
59 #:#:#:#:#
60 #:#:#:#:#
61 #:::::::#
62 ####F####
63 </div>
64
9 <div id="container"></div>
6510
6611 </body>
67 <script src="yoob/controller.js"></script>
68 <script src="yoob/playfield.js"></script>
69 <script src="yoob/playfield-html-view.js"></script>
70 <script src="yoob/playfield-canvas-view.js"></script>
71 <script src="yoob/preset-manager.js"></script>
72 <script src="../script/jacciata.js"></script>
12 <script src="../eg/index.js"></script>
13 <script src="jacciata-launcher.js"></script>
7314 <script>
74 var pf;
75
76 // Text view
77 var displayText = document.getElementById('display_text');
78 var htmlView = new yoob.PlayfieldHTMLView().init(pf, displayText);
79 htmlView.render = function(state) {
80 return dumpMapper(state);
81 };
82
83 // Canvas view
84 var displayCanvas = document.getElementById('display_canvas');
85 var colourMap = {
86 'Space': '#ffffff',
87 'Wall': '#000000',
88 'Slime': '#00a000',
89 'Solved': '#00ff40',
90 'Finish': '#0000ff',
91 'Start': '#ff0000',
92 'Head': '#a0c0a0',
93 'Body': '#80a080'
94 };
95 var canvasView = new yoob.PlayfieldCanvasView().init(pf, displayCanvas);
96 canvasView.drawCell = function(ctx, value, playfieldX, playfieldY,
97 canvasX, canvasY, cellWidth, cellHeight) {
98 ctx.fillStyle = colourMap[value] || '#ffffff';
99 ctx.fillRect(canvasX, canvasY, cellWidth, cellHeight);
100 };
101
102 // "View Manager"
103 var currentView = 'text';
104 var views = {
105 'text': htmlView,
106 'canvas': canvasView
107 };
108 var draw = function() {
109 views[currentView].pf = pf;
110 views[currentView].draw();
111 };
112
113 var selectDepiction = document.getElementById('select_depiction');
114 selectDepiction.onchange = function() {
115 var value = selectDepiction.options[selectDepiction.selectedIndex].value;
116 if (value === 'text') {
117 displayText.style.display = 'block';
118 displayCanvas.style.display = 'none';
119 } else {
120 displayText.style.display = 'none';
121 displayCanvas.style.display = 'block';
122 }
123 currentView = value;
124 draw();
125 };
126
127 // Controller. We don't subclass, we just monkeypatch.
128 var c = new yoob.Controller();
129
130 c.load = function(text) {
131 pf = new yoob.Playfield();
132 pf.setDefault('Space');
133 pf.load(0, 0, text, loadMapper);
134 draw();
135 };
136
137 c.step = function() {
138 var newPf = new yoob.Playfield();
139 newPf.setDefault('Space');
140 evolve_playfield(pf, newPf);
141 pf = newPf;
142 draw();
143 };
144
145 c.connect({
146 'start': 'start',
147 'stop': 'stop',
148 'step': 'step',
149 'load': 'load',
150 'edit': 'edit',
151 'reset': 'reset',
152 'speed': 'speed',
153 'source': 'input',
154 'display': 'display_container'
155 });
156
157 var p = (new yoob.PresetManager()).init({
158 selectElem: document.getElementById("select_source"),
159 controller: c
160 }).populateFromClass('example').select('test3');
161
162 c.click_load();
15 launch('', 'container');
16316 </script>
00 /*
1 * This file is part of yoob.js version 0.7-2015.0108
1 * This file is part of yoob.js version 0.12
22 * Available from https://github.com/catseye/yoob.js/
33 * This file is in the public domain. See http://unlicense.org/ for details.
44 */
1010 * convenience, we will refer to this as the _program state_, even though
1111 * it is of course highly adaptable and might not represent a "program".
1212 *
13 * The controller can be connected to a UI in the DOM, consisting of:
13 * Like most yoob objects, it is initialized after creation by calling the
14 * method `init` with a configuration object. If a DOM element is passed
15 * for `panelContainer` in the configuration, a panel containing a number
16 * of UI controls will be created and appended to that container. These
17 * are:
1418 *
1519 * - a set of buttons which control the evolution of the state:
1620 * - start
1721 * - stop
1822 * - step
1923 * - load
20 * - edit
2124 * - reset
2225 *
2326 * - a slider control which adjusts the speed of program state evolution.
24 *
25 * - a `source` element from which an program state can be loaded,
26 * and which is generally assumed to support user-editing of the source.
27 * The `edit` button will cause the `source` to be shown and the `display`
28 * to be hidden, while the `load` button will load the program state from
29 * the `source`, hide the `source`, and show the `display`.
30 *
31 * - a `display` element on which the current program state will be
32 * depicted. Note that the controller is not directly responsible for
33 * rendering the program state; use something like yoob.PlayfieldCanvasView
34 * for that instead. The controller only knows about the `display` in order
35 * to hide it while the `source` is being edited and to show it after the
36 * `source` has been loaded.
37 *
38 * - an `input` element, which provides input to the running program.
39 *
40 * Each of these is optional, and if not configured, will not be used.
4127 *
4228 * To use a Controller, create a subclass of yoob.Controller and override
4329 * the following methods:
4430 * - make it evolve the state by one tick in the step() method
45 * - make it load the state from a multiline string in the load() method
31 * - make it load the initial state from a string in the reset(s) method
4632 *
4733 * In these methods, you will need to store the state (in whatever
4834 * representation you find convenient for processing and for depicting on
5440 *
5541 * You should *not* store it in the `.state` attribute, as a yoob.Controller
5642 * uses this to track its own state (yes, it has its own state independent of
57 * the program state. at least potentially.)
43 * the program state.)
44 *
45 * Some theory of operation:
46 *
47 * For every action 'foo', three methods are exposed on the yoob.Controller
48 * object:
49 *
50 * - clickFoo
51 *
52 * Called when the button associated button is clicked.
53 * Client code may call this method to simulate the button having been
54 * clicked, including respecting and changing the state of the buttons panel.
55 *
56 * - performFoo
57 *
58 * Called by clickFoo to request the 'foo' action be performed.
59 * Responsible also for any Controller-related housekeeping involved with
60 * the 'foo' action. Client code may call this method when it wants the
61 * controller to perform this action without respecting or changing the
62 * state of the button panel.
63 *
64 * - foo
65 *
66 * Overridden (if necessary) by a subclass, or supplied by an instantiator,
67 * of yoob.Controller to implement some action. In particular, 'step' needs
68 * to be implemented this way. Client code should not call these methods
69 * directly.
70 *
71 * The clickFoo methods take one argument, an event structure. None of the
72 * other functions take an argument, with the exception of performReset() and
73 * reset(), which take a single argument, the text-encoded state to reset to.
5874 */
5975 yoob.Controller = function() {
6076 var STOPPED = 0; // the program has terminated (itself)
6278 var RUNNING = 2; // the program is running
6379 var BLOCKED = 3; // the program is waiting for more input
6480
65 this.intervalId = undefined;
66 this.delay = 100;
67 this.state = STOPPED;
68
69 this.source = undefined;
70 this.input = undefined;
71 this.display = undefined;
72
73 this.speed = undefined;
74 this.controls = {};
75
7681 /*
77 * This is not a public method.
78 */
79 this._makeEventHandler = function(control, key) {
80 if (this['click_' + key] !== undefined) {
81 key = 'click_' + key;
82 }
82 * panelContainer: an element into which to add the created button panel
83 * (if you do not give this, no panel will be created. You're on your own.)
84 * step: if given, if a function, it becomes the step() method on this
85 * reset: if given, if a function, it becomes the reset() method on this
86 */
87 this.init = function(cfg) {
88 this.delay = 100;
89 this.state = STOPPED;
90 this.controls = {};
91 this.resetState = undefined;
92 if (cfg.panelContainer) {
93 this.panel = this.makePanel();
94 cfg.panelContainer.appendChild(this.panel);
95 }
96 if (cfg.step) {
97 this.step = cfg.step;
98 }
99 if (cfg.reset) {
100 this.reset = cfg.reset;
101 }
102 return this;
103 };
104
105 /******************
106 * UI
107 */
108 this.makePanel = function() {
109 var panel = document.createElement('div');
83110 var $this = this;
84 return function(e) {
85 $this[key](control);
111
112 var makeEventHandler = function(control, upperAction) {
113 return function(e) {
114 $this['click' + upperAction](control);
115 };
86116 };
117
118 var makeButton = function(action) {
119 var button = document.createElement('button');
120 var upperAction = action.charAt(0).toUpperCase() + action.slice(1);
121 button.innerHTML = upperAction;
122 button.style.width = "5em";
123 panel.appendChild(button);
124 button.onclick = makeEventHandler(button, upperAction);
125 $this.controls[action] = button;
126 return button;
127 };
128
129 var keys = ["start", "stop", "step", "reset"];
130 for (var i = 0; i < keys.length; i++) {
131 makeButton(keys[i]);
132 }
133
134 var slider = document.createElement('input');
135 slider.type = "range";
136 slider.min = 0;
137 slider.max = 200;
138 slider.value = 100;
139 slider.onchange = function(e) {
140 $this.setDelayFrom(slider);
141 if ($this.intervalId !== undefined) {
142 $this.stop();
143 $this.start();
144 }
145 };
146
147 panel.appendChild(slider);
148 $this.controls.speed = slider;
149
150 return panel;
151 };
152
153 this.connectInput = function(elem) {
154 this.input = elem;
155 this.input.onchange = function(e) {
156 if (this.value.length > 0) {
157 // weird, where is this from?
158 $this.unblock();
159 }
160 }
87161 };
88162
89163 /*
90 * Single argument is a dictionary (object) where the keys
91 * are the actions a controller can undertake, and the values
92 * are either DOM elements or strings; if strings, DOM elements
93 * with those ids will be obtained from the document and used.
94 *
95 * When the button associated with e.g. 'start' is clicked,
96 * the corresponding method (in this case, 'click_start()')
97 * on this Controller will be called. These functions are
98 * responsible for changing the state of the Controller (both
99 * the internal state, and the enabled status, etc. of the
100 * controls), and for calling other methods on the Controller
101 * to implement the particulars of the action.
102 *
103 * For example, 'click_step()' calls 'performStep()' which
104 * calls 'step()' (which a subclass or instantiator must
105 * provide an implementation for.)
106 *
107 * To simulate one of the buttons being clicked, you may
108 * call 'click_foo()' yourself in code. However, that will
109 * be subject to the current restrictions of the interface.
110 * You may be better off calling one of the "internal" methods
111 * like 'performStep()'.
112 */
113 this.connect = function(dict) {
114 var $this = this;
115
116 var keys = ["start", "stop", "step", "load", "edit", "reset"];
117 for (var i in keys) {
118 var key = keys[i];
119 var value = dict[key];
120 if (typeof value === 'string') {
121 value = document.getElementById(value);
122 }
123 if (value) {
124 value.onclick = this._makeEventHandler(value, key);
125 this.controls[key] = value;
126 }
127 }
128
129 var keys = ["speed", "source", "input", "display"];
130 for (var i in keys) {
131 var key = keys[i];
132 var value = dict[key];
133 if (typeof value === 'string') {
134 value = document.getElementById(value);
135 }
136 if (value) {
137 this[key] = value;
138 // special cases
139 if (key === 'speed') {
140 this.speed.value = this.delay;
141 this.speed.onchange = function(e) {
142 $this.setDelayFrom($this.speed);
143 if ($this.intervalId !== undefined) {
144 $this.stop();
145 $this.start();
146 }
147 }
148 } else if (key === 'input') {
149 this.input.onchange = function(e) {
150 if (this.value.length > 0) {
151 $this.unblock();
152 }
153 }
154 }
155 }
156 }
157
158 this.click_stop();
159 };
160
161 this.click_step = function(e) {
164 * Override this to change how the delay is acquired from the 'speed'
165 * control.
166 */
167 this.setDelayFrom = function(elem) {
168 this.delay = elem.max - elem.value; // parseInt(elem.value, 10)
169 };
170
171 /******************
172 * action: Step
173 */
174 this.clickStep = function(e) {
162175 if (this.state === STOPPED) return;
163 this.click_stop();
176 this.clickStop();
164177 this.state = PAUSED;
165178 this.performStep();
179 };
180
181 this.performStep = function() {
182 var code = this.step();
183 if (code === 'stop') {
184 this.clickStop();
185 this.state = STOPPED;
186 } else if (code === 'block') {
187 this.state = BLOCKED;
188 }
166189 };
167190
168191 /*
173196 * - `block` to indicate that the program is waiting for more input.
174197 */
175198 this.step = function() {
176 alert("step() NotImplementedError");
177 };
178
179 this.performStep = function() {
180 var code = this.step();
181 if (code === 'stop') {
182 this.terminate();
183 } else if (code === 'block') {
184 this.state = BLOCKED;
185 }
186 };
187
188 this.click_load = function(e) {
189 this.click_stop();
190 this.load(this.source.value);
191 this.state = PAUSED;
192 if (this.controls.edit) this.controls.edit.style.display = "inline";
193 if (this.controls.load) this.controls.load.style.display = "none";
194 if (this.controls.start) this.controls.start.disabled = false;
195 if (this.controls.step) this.controls.step.disabled = false;
196 if (this.controls.stop) this.controls.stop.disabled = true;
197 if (this.controls.reset) this.controls.reset.disabled = false;
198 if (this.display) this.display.style.display = "block";
199 if (this.source) this.source.style.display = "none";
200 };
201
202 this.load = function(text) {
203 alert("load() NotImplementedError");
204 };
205
206 /*
207 * Loads a source text into the source element.
208 */
209 this.loadSource = function(text) {
210 if (this.source) this.source.value = text;
211 this.load(text);
212 this.state = PAUSED;
213 };
214
215 /*
216 * Loads a source text into the source element.
217 * Assumes it comes from an element in the document, so it translates
218 * the basic HTML escapes (but no others) to plain text.
219 */
220 this.loadSourceFromHTML = function(html) {
221 var text = html;
222 text = text.replace(/\&lt;/g, '<');
223 text = text.replace(/\&gt;/g, '>');
224 text = text.replace(/\&amp;/g, '&');
225 this.loadSource(text);
226 };
227
228 /*
229 * This is the basic idea, but not fleshed out yet.
230 * - Should we cache the source somewhere?
231 * - While we're waiting, should we disable the UI / show a spinny?
232 */
233 this.loadSourceFromURL = function(url, errorCallback) {
234 var http = new XMLHttpRequest();
235 var $this = this;
236 if (!errorCallback) {
237 errorCallback = function(http) {
238 $this.loadSource(
239 "Error: could not load " + url + ": " + http.statusText
240 );
241 }
242 }
243 http.open("get", url, true);
244 http.onload = function(e) {
245 if (http.readyState === 4 && http.responseText) {
246 if (http.status === 200) {
247 $this.loadSource(http.responseText);
248 } else {
249 errorCallback(http);
250 }
251 }
252 };
253 http.send(null);
254 };
255
256 this.click_edit = function(e) {
257 this.click_stop();
258 if (this.controls.edit) this.controls.edit.style.display = "none";
259 if (this.controls.load) this.controls.load.style.display = "inline";
260 if (this.controls.start) this.controls.start.disabled = true;
261 if (this.controls.step) this.controls.step.disabled = true;
262 if (this.controls.stop) this.controls.stop.disabled = true;
263 if (this.controls.reset) this.controls.reset.disabled = true;
264 if (this.display) this.display.style.display = "none";
265 if (this.source) this.source.style.display = "block";
266 };
267
268 this.click_start = function(e) {
269 this.start();
199 throw new Error("step() NotImplementedError");
200 };
201
202 /******************
203 * action: Start
204 */
205 this.clickStart = function(e) {
206 this.performStart();
270207 if (this.controls.start) this.controls.start.disabled = true;
271208 if (this.controls.step) this.controls.step.disabled = false;
272209 if (this.controls.stop) this.controls.stop.disabled = false;
273210 };
274211
212 this.performStart = function() {
213 this.start();
214 };
215
275216 this.start = function() {
276217 if (this.intervalId !== undefined)
277218 return;
278 this.step();
219 this.performStep();
279220 var $this = this;
280221 this.intervalId = setInterval(function() {
281222 $this.performStep();
283224 this.state = RUNNING;
284225 };
285226
286 this.click_stop = function(e) {
287 this.stop();
288 this.state = PAUSED;
289 /* why is this check here? ... */
290 if (this.controls.stop && this.controls.stop.disabled) {
291 return;
292 }
227 /******************
228 * action: Stop
229 */
230 this.clickStop = function(e) {
231 this.performStop();
293232 if (this.controls.start) this.controls.start.disabled = false;
294233 if (this.controls.step) this.controls.step.disabled = false;
295234 if (this.controls.stop) this.controls.stop.disabled = true;
296235 };
297236
298 this.terminate = function(e) {
237 this.performStop = function() {
299238 this.stop();
300 this.state = STOPPED;
301 if (this.controls.start) this.controls.start.disabled = true;
302 if (this.controls.step) this.controls.step.disabled = true;
303 if (this.controls.stop) this.controls.stop.disabled = true;
239 this.state = PAUSED;
304240 };
305241
306242 this.stop = function() {
310246 this.intervalId = undefined;
311247 };
312248
313 this.click_reset = function(e) {
314 this.click_stop();
315 this.load(this.source.value);
249 /******************
250 * action: Reset
251 */
252 this.clickReset = function(e) {
253 this.clickStop();
254 this.performReset();
316255 if (this.controls.start) this.controls.start.disabled = false;
317256 if (this.controls.step) this.controls.step.disabled = false;
318257 if (this.controls.stop) this.controls.stop.disabled = true;
319258 };
320259
321 /*
322 * Override this to change how the delay is acquired from the 'speed'
323 * element.
324 */
325 this.setDelayFrom = function(elem) {
326 this.delay = elem.max - elem.value;
260 this.performReset = function(state) {
261 if (state !== undefined) {
262 this.setResetState(state);
263 }
264 this.reset(this.resetState);
265 };
266
267 this.reset = function(state) {
268 throw new Error("reset() NotImplementedError");
269 };
270
271 this.setResetState = function(state) {
272 this.resetState = state;
327273 };
328274 };
0 /*
1 * This file is part of yoob.js version 0.13
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 (!isNaN(v)) {
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 (!isNaN(v)) {
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 ((!isNaN(v)) && 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 ((!isNaN(v)) && 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 Object.keys(cfg).forEach(function(key) {
306 elem.setAttribute(key, cfg[key]);
307 });
308 svg.appendChild(elem);
309 return elem;
310 };
00 /*
1 * This file is part of yoob.js version 0.6
1 * This file is part of yoob.js version 0.12
22 * Available from https://github.com/catseye/yoob.js/
33 * This file is in the public domain. See http://unlicense.org/ for details.
44 */
88 * A view (in the MVC sense) for depicting a yoob.Playfield (-compatible)
99 * object on an HTML5 <canvas> element (or compatible object).
1010 *
11 * TODO: don't necesarily resize canvas each time?
12 * TODO: option to stretch content rendering to fill a fixed-size canvas
11 * drawCursorsFirst defaults to true. This produces the pleasing visual
12 * effect of the cursor being behind the cell values, but only if the cell values
13 * themselves have transparent areas (e.g. if they're glyphs in some font.)
14 * If the cell values are solid and fill the entire cell, drawCursorsFirst: false
15 * may be in order.
16 *
17 * resizeCanvas defaults to true. If set to false, the canvas element will
18 * not be resized before each draw. You may wish to do this yourself in your
19 * code which calls playfieldCanvasView.draw().
20 *
1321 */
1422 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);
23 this.init = function(cfg) {
24 this.pf = cfg.playfield;
25 this.canvas = cfg.canvas;
26 this.ctx = this.canvas.getContext('2d');
27 this.fixedPosition = !!cfg.fixedPosition;
28 this.fixedSizeCanvas = !!cfg.fixedSizeCanvas;
29 this.drawCursorsFirst = (cfg.drawCursorsFirst === undefined) ? true : !!cfg.drawCursorsFirst;
30 this.setCellDimensions(cfg.cellWidth || 8, cfg.cellHeight || 8);
31 this.resizeCanvas = cfg.resizeCanvas === false ? false : true;
2432 return this;
2533 };
2634
2735 /*** Chainable setters ***/
2836
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;
37 this.setPlayfield = function(pf) {
38 this.pf = pf;
39 return this;
40 };
41
42 this.setCanvas = function(element) {
43 this.canvas = element;
44 this.ctx = this.canvas.getContext('2d');
3545 return this;
3646 };
3747
5262 this.cellWidth = cellWidth;
5363 this.cellHeight = cellHeight;
5464 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 }
12565 };
12666
12767 /*
15191 * of the entire playfield.
15292 */
15393 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);
94 var $this = this;
95 this.pf.foreach(function(x, y, value) {
96 $this.drawCell(ctx, value, x, y,
97 offsetX + x * cellWidth, offsetY + y * cellHeight,
98 cellWidth, cellHeight);
15999 });
160100 };
161101
102 /*
103 * Override if you like.
104 */
105 this.drawCursor = function(ctx, cursor, canvasX, canvasY, cellWidth, cellHeight) {
106 ctx.fillStyle = this.cursorFillStyle || "#50ff50";
107 ctx.fillRect(canvasX, canvasY, cellWidth, cellHeight);
108 };
109
162110 this.drawCursors = function(ctx, offsetX, offsetY, cellWidth, cellHeight) {
163 var cursors = this.cursors;
111 var cursors = this.pf.cursors;
164112 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 );
113 var cursor = cursors[i];
114 var x = offsetX + cursor.getX() * cellWidth;
115 var y = offsetY + cursor.getY() * cellHeight;
116 this.drawCursor(ctx, cursor, x, y, cellWidth, cellHeight);
171117 }
172118 };
173119
174120 /*
175121 * Draw the Playfield, and its set of Cursors, on the canvas element.
176 * Resizes the canvas to the needed dimensions first.
122 * Optionally resizes the canvas to the needed dimensions first.
177123 */
178124 this.draw = function() {
179125 var canvas = this.canvas;
180 var ctx = canvas.getContext('2d');
181126 var cellWidth = this.cellWidth;
182127 var cellHeight = this.cellHeight;
183 var cursors = this.cursors;
184128
185 var width = this.getExtentX();
186 var height = this.getExtentY();
129 var width = this.pf.getCursoredExtentX();
130 var height = this.pf.getCursoredExtentY();
187131
188 canvas.width = width * cellWidth;
189 canvas.height = height * cellHeight;
132 if (this.resizeCanvas) {
133 canvas.width = width * cellWidth;
134 canvas.height = height * cellHeight;
135 }
136 var ctx = this.ctx;
190137
191 this.ctx.textBaseline = "top";
192 this.ctx.font = cellHeight + "px monospace";
138 ctx.textBaseline = "top";
139 ctx.font = cellHeight + "px monospace";
193140
194141 var offsetX = 0;
195142 var offsetY = 0;
196143
197144 if (!this.fixedPosition) {
198 offsetX = (this.getLowerX() || 0) * cellWidth * -1;
199 offsetY = (this.getLowerY() || 0) * cellHeight * -1;
145 offsetX = (this.pf.getLowerX() || 0) * cellWidth * -1;
146 offsetY = (this.pf.getLowerY() || 0) * cellHeight * -1;
200147 }
201148
202149 if (this.drawCursorsFirst) {
00 /*
1 * This file is part of yoob.js version 0.6
1 * This file is part of yoob.js version 0.11
22 * Available from https://github.com/catseye/yoob.js/
33 * This file is in the public domain. See http://unlicense.org/ for details.
44 */
77 /*
88 * A view (in the MVC sense) for depicting a yoob.Playfield (-compatible)
99 * object onto any DOM element that supports innerHTML.
10 *
11 * TODO: this may be incomplete; use at your own risk
12 * TODO: have this and the canvas view inherit from a common ABC?
1310 */
1411 yoob.PlayfieldHTMLView = function() {
15 this.pf = undefined;
16 this.element = undefined;
17
18 this.init = function(pf, element) {
19 this.pf = pf;
20 this.element = element;
21 this.cursors = [];
12 this.init = function(cfg) {
13 this.pf = cfg.playfield;
14 this.element = cfg.element;
2215 return this;
2316 };
2417
2518 /*** Chainable setters ***/
2619
27 /*
28 * Set the list of cursors to the given list of yoob.Cursor (or compatible)
29 * objects.
30 */
31 this.setCursors = function(cursors) {
32 this.cursors = cursors;
20 this.setPlayfield = function(pf) {
21 this.pf = pf;
22 return this;
23 };
24
25 this.setElement = function(element) {
26 this.element = element;
3327 return this;
3428 };
3529
3630 /*
37 * Return the requested bounds of the occupied portion of the playfield.
38 * "Occupation" in this sense includes all cursors.
39 *
40 * These may return 'undefined' if there is nothing in the playfield.
41 *
42 * Override these if you want to draw some portion of the
43 * playfield which is not the whole playfield.
31 * For compatibility with PlayfieldCanvasView. Sets the font size.
4432 */
45 this.getLowerX = function() {
46 var minX = this.pf.getMinX();
47 for (var i = 0; i < this.cursors.length; i++) {
48 if (minX === undefined || this.cursors[i].x < minX) {
49 minX = this.cursors[i].x;
50 }
51 }
52 return minX;
53 };
54 this.getUpperX = function() {
55 var maxX = this.pf.getMaxX();
56 for (var i = 0; i < this.cursors.length; i++) {
57 if (maxX === undefined || this.cursors[i].x > maxX) {
58 maxX = this.cursors[i].x;
59 }
60 }
61 return maxX;
62 };
63 this.getLowerY = function() {
64 var minY = this.pf.getMinY();
65 for (var i = 0; i < this.cursors.length; i++) {
66 if (minY === undefined || this.cursors[i].y < minY) {
67 minY = this.cursors[i].y;
68 }
69 }
70 return minY;
71 };
72 this.getUpperY = function() {
73 var maxY = this.pf.getMaxY();
74 for (var i = 0; i < this.cursors.length; i++) {
75 if (maxY === undefined || this.cursors[i].y > maxY) {
76 maxY = this.cursors[i].y;
77 }
78 }
79 return maxY;
80 };
81
82 /*
83 * Returns the number of occupied cells in the x direction.
84 * "Occupation" in this sense includes all cursors.
85 */
86 this.getExtentX = function() {
87 if (this.getLowerX() === undefined || this.getUpperX() === undefined) {
88 return 0;
89 } else {
90 return this.getUpperX() - this.getLowerX() + 1;
91 }
92 };
93
94 /*
95 * Returns the number of occupied cells in the y direction.
96 * "Occupation" in this sense includes all cursors.
97 */
98 this.getExtentY = function() {
99 if (this.getLowerY() === undefined || this.getUpperY() === undefined) {
100 return 0;
101 } else {
102 return this.getUpperY() - this.getLowerY() + 1;
103 }
33 this.setCellDimensions = function(cellWidth, cellHeight) {
34 this.element.style.fontSize = cellHeight + "px";
35 return this;
10436 };
10537
10638 /*
10739 * Override to convert Playfield values to HTML.
10840 */
10941 this.render = function(value) {
42 if (value === undefined) return ' ';
11043 return value;
44 };
45
46 /*
47 * Override if you like.
48 */
49 this.wrapCursorText = function(cursor, text) {
50 var fillStyle = this.cursorFillStyle || "#50ff50";
51 return '<span style="background: ' + fillStyle + '">' +
52 text + '</span>';
11153 };
11254
11355 /*
11557 */
11658 this.draw = function() {
11759 var text = "";
118 for (var y = this.getLowerY(); y <= this.getUpperY(); y++) {
60 var cursors = this.pf.cursors;
61 var lowerY = this.pf.getLowerY();
62 var upperY = this.pf.getUpperY();
63 var lowerX = this.pf.getLowerX();
64 var upperX = this.pf.getUpperX();
65 for (var y = lowerY; y <= upperY; y++) {
11966 var row = "";
120 for (var x = this.getLowerX(); x <= this.getUpperX(); x++) {
67 for (var x = lowerX; x <= upperX; x++) {
12168 var rendered = this.render(this.pf.get(x, y));
122 for (var i = 0; i < this.cursors.length; i++) {
123 if (this.cursors[i].x === x && this.cursors[i].y === y) {
124 rendered = this.cursors[i].wrapText(rendered);
69 for (var i = 0; i < cursors.length; i++) {
70 if (cursors[i].x === x && cursors[i].y === y) {
71 rendered = this.wrapCursorText(cursors[i], rendered);
12572 }
12673 }
12774 row += rendered;
00 /*
1 * This file is part of yoob.js version 0.6
1 * This file is part of yoob.js version 0.13
22 * Available from https://github.com/catseye/yoob.js/
33 * This file is in the public domain. See http://unlicense.org/ for details.
44 */
88 * A two-dimensional Cartesian grid of values.
99 */
1010 yoob.Playfield = function() {
11 this._store = {};
12 this.minX = undefined;
13 this.minY = undefined;
14 this.maxX = undefined;
15 this.maxY = undefined;
16 this._default = undefined;
11 this.init = function(cfg) {
12 cfg = cfg || {};
13 this._default = cfg.defaultValue;
14 this.cursors = cfg.cursors || [];
15 this.clear();
16 return this;
17 };
18
19 /*** Chainable setters ***/
1720
1821 /*
1922 * Set the default value for this Playfield. This
2427 this._default = v;
2528 return this;
2629 };
30
31 /*
32 * Set the list of cursors to the given list of yoob.Cursor (or compatible)
33 * objects.
34 */
35 this.setCursors = function(cursors) {
36 this.cursors = cursors;
37 return this;
38 };
39
40 /*** Accessors, etc. ***/
2741
2842 /*
2943 * Obtain the value at (x, y). The default value will
4458 this.put = function(x, y, value) {
4559 var key = x+','+y;
4660 if (value === undefined || value === this._default) {
61 // NOTE: this does not recalculate the bounds, nor
62 // will it set the bounds back to 'undefined'
63 // if the playfield is now empty.
4764 delete this._store[key];
4865 return;
4966 }
101118 this.minY = undefined;
102119 this.maxX = undefined;
103120 this.maxY = undefined;
121 return this;
104122 };
105123
106124 /*
114132 this.put(x, y, this.get(x, y - dy));
115133 }
116134 }
117 } else { alert("scrollRectangleY(" + dy + ") notImplemented"); }
135 } else {
136 throw new Error("scrollRectangleY(" + dy + ") notImplemented");
137 }
118138 };
119139
120140 this.clearRectangle = function(minX, minY, maxX, maxY) {
192212 * fun is a callback which takes three parameters:
193213 * x, y, and value. If this callback returns a value,
194214 * it is written into the Playfield at that position.
195 * This function ensures a particular order.
215 * This function ensures a particular order. For efficiency,
216 * This function knows about the structure of the backing
217 * store, so if you override .get() or .put() in a subclass,
218 * you should also override this.
196219 */
197220 this.foreach = function(fun) {
198221 for (var y = this.minY; y <= this.maxY; y++) {
202225 if (value === undefined)
203226 continue;
204227 var result = fun(x, y, value);
228 // TODO: Playfield.UNDEFINED vs. undefined meaning "no change"?
205229 if (result !== undefined) {
206 if (result === ' ') {
207 result = undefined;
208 }
230 this.put(x, y, result);
231 }
232 }
233 }
234 };
235
236 this.foreachVonNeumannNeighbour = function(x, y, fun) {
237 for (var dx = -1; dx <= 1; dx++) {
238 for (var dy = -1; dy <= 1; dy++) {
239 if (dx === 0 && dy === 0)
240 continue;
241 var value = this.get(x + dx, y + dy);
242 if (value === undefined)
243 continue;
244 var result = fun(x, y, value);
245 // TODO: Playfield.UNDEFINED vs. undefined meaning "no change"?
246 if (result !== undefined) {
209247 this.put(x, y, result);
210248 }
211249 }
235273 if (maxDy === undefined) maxDy = 0;
236274 for (var y = this.minY + minDy; y <= this.maxY + maxDy; y++) {
237275 for (var x = this.minX + minDx; x <= this.maxX + maxDx; x++) {
238 destPf.putDirty(x, y, fun(pf, x, y));
276 destPf.putDirty(x, y, fun(this, x, y));
239277 }
240278 }
241279 destPf.recalculateBounds();
282320 return this.maxY - this.minY + 1;
283321 }
284322 };
323
324 /*
325 * Return the requested bounds of the occupied portion of the playfield.
326 * "Occupation" in this sense includes all cursors.
327 *
328 * These may return 'undefined' if there is nothing in the playfield.
329 *
330 * Override these if you want to draw some portion of the
331 * playfield which is not the whole playfield.
332 */
333 this.getLowerX = function() {
334 var minX = this.getMinX();
335 for (var i = 0; i < this.cursors.length; i++) {
336 if (minX === undefined || this.cursors[i].x < minX) {
337 minX = this.cursors[i].x;
338 }
339 }
340 return minX;
341 };
342 this.getUpperX = function() {
343 var maxX = this.getMaxX();
344 for (var i = 0; i < this.cursors.length; i++) {
345 if (maxX === undefined || this.cursors[i].x > maxX) {
346 maxX = this.cursors[i].x;
347 }
348 }
349 return maxX;
350 };
351 this.getLowerY = function() {
352 var minY = this.getMinY();
353 for (var i = 0; i < this.cursors.length; i++) {
354 if (minY === undefined || this.cursors[i].y < minY) {
355 minY = this.cursors[i].y;
356 }
357 }
358 return minY;
359 };
360 this.getUpperY = function() {
361 var maxY = this.getMaxY();
362 for (var i = 0; i < this.cursors.length; i++) {
363 if (maxY === undefined || this.cursors[i].y > maxY) {
364 maxY = this.cursors[i].y;
365 }
366 }
367 return maxY;
368 };
369
370 /*
371 * Returns the number of occupied cells in the x direction.
372 * "Occupation" in this sense includes all cursors.
373 */
374 this.getCursoredExtentX = function() {
375 if (this.getLowerX() === undefined || this.getUpperX() === undefined) {
376 return 0;
377 } else {
378 return this.getUpperX() - this.getLowerX() + 1;
379 }
380 };
381
382 /*
383 * Returns the number of occupied cells in the y direction.
384 * "Occupation" in this sense includes all cursors.
385 */
386 this.getCursoredExtentY = function() {
387 if (this.getLowerY() === undefined || this.getUpperY() === undefined) {
388 return 0;
389 } else {
390 return this.getUpperY() - this.getLowerY() + 1;
391 }
392 };
393
394 /*
395 * Cursored read/write interface
396 */
397 this.read = function(index) {
398 var cursor = this.cursors[index || 0];
399 return this.get(cursor.getX(), cursor.getY());
400 };
401
402 this.write = function(value, index) {
403 var cursor = this.cursors[index || 0];
404 this.put(cursor.getX(), cursor.getY(), value);
405 return this;
406 };
285407 };
00 /*
1 * This file is part of yoob.js version 0.6
1 * This file is part of yoob.js version 0.13
22 * Available from https://github.com/catseye/yoob.js/
33 * This file is in the public domain. See http://unlicense.org/ for details.
44 */
2121 * will cause the .select() method of this manager to be called.
2222 * it will also call .onselect if that method is present.
2323 *
24 * controller: a yoob.Controller (or compatible object) that will
25 * be informed of the selection, if no callback was supplied
26 * when the item was added.
24 * setPreset: (optional) a callback which will be called whenever
25 * a new preset is selected. If this is not given, an individual
26 * callback must be supplied with each preset as it is added.
2727 */
2828 this.init = function(cfg) {
2929 this.selectElem = cfg.selectElem;
30 this.controller = cfg.controller || null;
30 if (cfg.setPreset) {
31 this.setPreset = cfg.setPreset;
32 }
3133 this.clear();
3234 var $this = this;
3335 this.selectElem.onchange = function() {
5153 /*
5254 * Adds a preset to this PresetManager. When it is selected,
5355 * the given callback will be called, being passed the id as the
54 * first argument. If no callback is provided, a default callback,
55 * which loads the contents of the element with the specified id
56 * into the configured yoob.Controller, will be used.
56 * first argument. If no callback is provided, the default callback,
57 * configured with setPreset in the init() configuration, will be used.
5758 */
5859 this.add = function(id, callback) {
5960 var opt = document.createElement("option");
6162 opt.value = id;
6263 this.selectElem.options.add(opt);
6364 var $this = this;
64 this.reactTo[id] = callback || function(id) {
65 $this.controller.click_stop(); // in case it is currently running
66 $this.controller.loadSourceFromHTML(
67 document.getElementById(id).innerHTML
68 );
69 };
65 this.reactTo[id] = callback || this.setPreset;
7066 return this;
67 };
68
69 this.setPreset = function(id) {
70 throw new Error("No default setPreset callback configured");
7171 };
7272
7373 /*
119119 }
120120 return this;
121121 };
122
123 /*
124 * When called with a yoob.SourceManager and an array of
125 * 2-element arrays of a name and a source text, this preset
126 * manager will be populated with each source text as a
127 * named preset. A callback to load the source text with
128 * the SourceManager will be automatically supplied.
129 */
130 this.populateFromPairs = function(sourceManager, pairs) {
131 function makeCallback(sourceText) {
132 return function(id) {
133 sourceManager.loadSource(sourceText);
134 }
135 }
136
137 for (var i = 0; i < pairs.length; i++) {
138 this.add(pairs[i][0], makeCallback(pairs[i][1]));
139 }
140 this.select(pairs[0][0]);
141 return this;
142 };
122143 };
0 /*
1 * This file is part of yoob.js version 0.8
2 * Available from https://github.com/catseye/yoob.js/
3 * This file is in the public domain. See http://unlicense.org/ for details.
4 */
5 if (window.yoob === undefined) yoob = {};
6
7 /*
8 * A SourceManager co-operates with a Controller and maybe a PresetManager.
9 * It is for editing a program/configuration in some editing interface
10 * which is mutually exclusive, UI-wise, with the run/animation interface.
11 */
12 yoob.SourceManager = function() {
13 /*
14 * editor: an element (usually a textarea) which stores the source code
15 * hideDuringEdit: a list of elements which will be hidden when editing
16 * (typically this will be the animation display of a yoob.Controller)
17 * disableDuringEdit: a list of elements which will be disabled when editing
18 * storageKey: key under which sources will be saved/loaded from localStorage
19 * panelContainer: an element into which to add the created button panel
20 * (if you do not give this, no panel will be created. You're on your own.)
21 * onDone: if given, if a function, it becomes the onDone method on this
22 */
23 this.init = function(cfg) {
24 this.supportsLocalStorage = (
25 window['localStorage'] !== undefined &&
26 window['localStorage'] !== null
27 );
28 this.editor = cfg.editor;
29 this.hideDuringEdit = cfg.hideDuringEdit;
30 this.prevDisplay = {};
31 for (var i = 0; i < this.hideDuringEdit.length; i++) {
32 this.prevDisplay[this.hideDuringEdit[i]] = this.hideDuringEdit[i].display;
33 }
34 this.disableDuringEdit = cfg.disableDuringEdit;
35 this.storageKey = cfg.storageKey || 'default';
36 this.controls = {};
37 if (cfg.panelContainer) {
38 this.panel = this.makePanel();
39 cfg.panelContainer.appendChild(this.panel);
40 }
41 if (cfg.onDone) {
42 this.onDone = cfg.onDone;
43 }
44 this.clickDone();
45 return this;
46 };
47
48 this.makePanel = function() {
49 var panel = document.createElement('div');
50 var $this = this;
51 var makeButton = function(action) {
52 var button = document.createElement('button');
53 var upperAction = action.charAt(0).toUpperCase() + action.slice(1);
54 button.innerHTML = upperAction;
55 button.style.width = "5em";
56 panel.appendChild(button);
57 button.onclick = function(e) {
58 if ($this['click' + upperAction]) {
59 $this['click' + upperAction]();
60 }
61 }
62 $this.controls[action] = button;
63 };
64 var keys = ["edit", "done", "load", "save"];
65 for (var i = 0; i < keys.length; i++) {
66 makeButton(keys[i]);
67 }
68 return panel;
69 };
70
71 this.clickEdit = function() {
72 var hde = this.hideDuringEdit;
73 for (var i = 0; i < hde.length; i++) {
74 this.prevDisplay[hde[i]] = hde[i].style.display;
75 hde[i].style.display = 'none';
76 }
77 for (var i = 0; i < this.disableDuringEdit.length; i++) {
78 this.disableDuringEdit[i].disabled = true;
79 /* But if it's not a form control, disabled is meaningless. */
80 this.disableDuringEdit[i].style.pointerEvents = 'none';
81 this.disableDuringEdit[i].style.opacity = '0.5';
82 }
83 this.editor.style.display = 'block';
84 this.controls.edit.disabled = true;
85 var keys = ["done", "load", "save"];
86 for (var i = 0; i < keys.length; i++) {
87 this.controls[keys[i]].disabled = false;
88 }
89 this.onEdit();
90 };
91
92 this.clickDone = function() {
93 var hde = this.hideDuringEdit;
94 for (var i = 0; i < hde.length; i++) {
95 hde[i].style.display = this.prevDisplay[hde[i]];
96 }
97 for (var i = 0; i < this.disableDuringEdit.length; i++) {
98 this.disableDuringEdit[i].disabled = false;
99 /* But if it's not a form control, disabled is meaningless. */
100 this.disableDuringEdit[i].style.pointerEvents = 'auto';
101 this.disableDuringEdit[i].style.opacity = '1.0';
102 }
103 this.editor.style.display = 'none';
104 this.controls.edit.disabled = false;
105 var keys = ["done", "load", "save"];
106 for (var i = 0; i < keys.length; i++) {
107 this.controls[keys[i]].disabled = true;
108 }
109 this.onDone();
110 };
111
112 this.clickLoad = function() {
113 if (!this.supportsLocalStorage) {
114 var s = "Your browser does not support Local Storage.\n\n";
115 s += "You may instead open a local file in a text editor, ";
116 s += "select all, copy to clipboard, then paste into ";
117 s += "the textarea, to load a source you have saved locally.";
118 alert(s);
119 return;
120 }
121 this.loadSource(
122 localStorage.getItem('yoob:' + this.storageKey + ':default')
123 );
124 };
125
126 this.clickSave = function() {
127 if (!this.supportsLocalStorage) {
128 var s = "Your browser does not support Local Storage.\n\n";
129 s += "You may instead select all in the textarea, copy to ";
130 s += "clipboard, open a local text editor, paste in there, ";
131 s += "and save, to save a source locally.";
132 alert(s);
133 return;
134 }
135 localStorage.setItem(
136 'yoob:' + this.storageKey + ':default',
137 this.getEditorText()
138 );
139 };
140
141 this.onEdit = function() {
142 };
143
144 /*
145 * Override this to load it into the controller
146 */
147 this.onDone = function() {
148 };
149
150 /*
151 * Loads a source text into the editor element.
152 */
153 this.loadSource = function(text) {
154 this.setEditorText(text);
155 this.onDone();
156 };
157
158 /*
159 * You may need to override if your editor is not a textarea.
160 */
161 this.setEditorText = function(text) {
162 this.editor.value = text;
163 };
164
165 /*
166 * You may need to override if your editor is not a textarea.
167 */
168 this.getEditorText = function() {
169 return this.editor.value;
170 };
171
172 /*
173 * Loads a source text into the source element.
174 * Assumes it comes from an element in the document, so it translates
175 * the basic HTML escapes (but no others) to plain text.
176 */
177 this.loadSourceFromHTML = function(html) {
178 var text = html;
179 text = text.replace(/\&lt;/g, '<');
180 text = text.replace(/\&gt;/g, '>');
181 text = text.replace(/\&amp;/g, '&');
182 this.loadSource(text);
183 };
184
185 /*
186 * This is the basic idea, but not fleshed out yet.
187 * - Should we cache the source somewhere?
188 * - While we're waiting, should we disable the UI / show a spinny?
189 */
190 this.loadSourceFromURL = function(url, errorCallback) {
191 var http = new XMLHttpRequest();
192 var $this = this;
193 if (!errorCallback) {
194 errorCallback = function(http) {
195 $this.loadSource(
196 "Error: could not load " + url + ": " + http.statusText
197 );
198 }
199 }
200 http.open("get", url, true);
201 http.onload = function(e) {
202 if (http.readyState === 4 && http.responseText) {
203 if (http.status === 200) {
204 $this.loadSource(http.responseText);
205 } else {
206 errorCallback(http);
207 }
208 }
209 };
210 http.send(null);
211 };
212 };
0 exampleConfigurations = [
1 [
2 "solveit.jac",
3 "########S##\n#:::::::::#\n#:###:###:#\n#:#:#:::#:#\n#:#:#:#:###\n#:::#:#:#:#\n#####:#:#:#\n#:#:::#:::#\n#:#:###:###\n#:::#:::::#\n#########F#\n"
4 ],
5 [
6 "unique.jac",
7 "####S####\n#:::::::#\n#:#####:#\n#:#:::#:#\n#:#:#:#:#\n#:::#:::#\n##F######\n"
8 ],
9 [
10 "nonunique.jac",
11 "####S####\n#:::::::#\n#:#####:#\n#:#:#:#:#\n#:#:#:#:#\n#:::::::#\n####F####\n"
12 ],
13 [
14 "nowalls.jac",
15 " F \n ::::::::: \n : : : \n : : ::: : \n : : : : \n ::: : : : \n : : : \n : ::: ::: \n : : : \n ::: ::::: \n F \n"
16 ]
17 ];