chiark / gitweb /
Fix completion checking in Killer Solo.
[sgt-puzzles.git] / emccpre.js
1 /*
2  * emccpre.js: one of the Javascript components of an Emscripten-based
3  * web/Javascript front end for Puzzles.
4  *
5  * The other parts of this system live in emcc.c and emcclib.js. It
6  * also depends on being run in the context of a web page containing
7  * an appropriate collection of bits and pieces (a canvas, some
8  * buttons and links etc), which is generated for each puzzle by the
9  * script html/jspage.pl.
10  *
11  * This file contains the Javascript code which is prefixed unmodified
12  * to Emscripten's output via the --pre-js option. It declares all our
13  * global variables, and provides the puzzle init function and a
14  * couple of other helper functions.
15  */
16
17 // To avoid flicker while doing complicated drawing, we use two
18 // canvases, the same size. One is actually on the web page, and the
19 // other is off-screen. We do all our drawing on the off-screen one
20 // first, and then copy rectangles of it to the on-screen canvas in
21 // response to draw_update() calls by the game backend.
22 var onscreen_canvas, offscreen_canvas;
23
24 // A persistent drawing context for the offscreen canvas, to save
25 // constructing one per individual graphics operation.
26 var ctx;
27
28 // Bounding rectangle for the copy to the onscreen canvas that will be
29 // done at drawing end time. Updated by js_canvas_draw_update and used
30 // by js_canvas_end_draw.
31 var update_xmin, update_xmax, update_ymin, update_ymax;
32
33 // Module object for Emscripten. We fill in these parameters to ensure
34 // that Module.run() won't be called until we're ready (we want to do
35 // our own init stuff first), and that when main() returns nothing
36 // will get cleaned up so we remain able to call the puzzle's various
37 // callbacks.
38 var Module = {
39     'noInitialRun': true,
40     'noExitRuntime': true
41 };
42
43 // Variables used by js_canvas_find_font_midpoint().
44 var midpoint_test_str = "ABCDEFGHIKLMNOPRSTUVWXYZ0123456789";
45 var midpoint_cache = [];
46
47 // Variables used by js_activate_timer() and js_deactivate_timer().
48 var timer = null;
49 var timer_reference_date;
50
51 // void timer_callback(double tplus);
52 //
53 // Called every 20ms while timing is active.
54 var timer_callback;
55
56 // The status bar object, if we create one.
57 var statusbar = null;
58
59 // Currently live blitters. We keep an integer id for each one on the
60 // JS side; the C side, which expects a blitter to look like a struct,
61 // simply defines the struct to contain that integer id.
62 var blittercount = 0;
63 var blitters = [];
64
65 // State for the dialog-box mechanism. dlg_dimmer and dlg_form are the
66 // page-darkening overlay and the actual dialog box respectively;
67 // dlg_next_id is used to allocate each checkbox a unique id to use
68 // for linking its label to it (see js_dialog_boolean);
69 // dlg_return_funcs is a list of JS functions to be called when the OK
70 // button is pressed, to pass the results back to C.
71 var dlg_dimmer = null, dlg_form = null;
72 var dlg_next_id = 0;
73 var dlg_return_funcs = null;
74
75 // void dlg_return_sval(int index, const char *val);
76 // void dlg_return_ival(int index, int val);
77 //
78 // C-side entry points called by functions in dlg_return_funcs, to
79 // pass back the final value in each dialog control.
80 var dlg_return_sval, dlg_return_ival;
81
82 // The <select> object implementing the game-type drop-down, and a
83 // list of the <option> objects inside it. Used by js_add_preset(),
84 // js_get_selected_preset() and js_select_preset().
85 //
86 // gametypethiscustom is an option which indicates some custom game
87 // params you've already set up, and which will be auto-selected on
88 // return from the customisation dialog; gametypenewcustom is an
89 // option which you select to indicate that you want to bring up the
90 // customisation dialog and select a new configuration. Ideally I'd do
91 // this with just one option serving both purposes, but instead we
92 // have to do this a bit oddly because browsers don't send 'onchange'
93 // events for a select element if you reselect the same one - so if
94 // you've picked a custom setup and now want to change it, you need a
95 // way to specify that.
96 var gametypeselector = null, gametypeoptions = [];
97 var gametypethiscustom = null, gametypehiddencustom = null;
98
99 // The two anchors used to give permalinks to the current puzzle. Used
100 // by js_update_permalinks().
101 var permalink_seed, permalink_desc;
102
103 // The undo and redo buttons. Used by js_enable_undo_redo().
104 var undo_button, redo_button;
105
106 // A div element enclosing both the puzzle and its status bar, used
107 // for positioning the resize handle.
108 var resizable_div;
109
110 // Helper function to find the absolute position of a given DOM
111 // element on a page, by iterating upwards through the DOM finding
112 // each element's offset from its parent, and thus calculating the
113 // page-relative position of the target element.
114 function element_coords(element) {
115     var ex = 0, ey = 0;
116     while (element.offsetParent) {
117         ex += element.offsetLeft;
118         ey += element.offsetTop;
119         element = element.offsetParent;
120     }
121     return {x: ex, y:ey};
122 }
123
124 // Helper function which is passed a mouse event object and a DOM
125 // element, and returns the coordinates of the mouse event relative to
126 // the top left corner of the element by subtracting element_coords
127 // from event.page{X,Y}.
128 function relative_mouse_coords(event, element) {
129     var ecoords = element_coords(element);
130     return {x: event.pageX - ecoords.x,
131             y: event.pageY - ecoords.y};
132 }
133
134 // Init function called from body.onload.
135 function initPuzzle() {
136     // Construct the off-screen canvas used for double buffering.
137     onscreen_canvas = document.getElementById("puzzlecanvas");
138     offscreen_canvas = document.createElement("canvas");
139     offscreen_canvas.width = onscreen_canvas.width;
140     offscreen_canvas.height = onscreen_canvas.height;
141
142     // Stop right-clicks on the puzzle from popping up a context menu.
143     // We need those right-clicks!
144     onscreen_canvas.oncontextmenu = function(event) { return false; }
145
146     // Set up mouse handlers. We do a bit of tracking of the currently
147     // pressed mouse buttons, to avoid sending mousemoves with no
148     // button down (our puzzles don't want those events).
149     mousedown = Module.cwrap('mousedown', 'void',
150                              ['number', 'number', 'number']);
151     buttons_down = 0;
152     onscreen_canvas.onmousedown = function(event) {
153         var xy = relative_mouse_coords(event, onscreen_canvas);
154         mousedown(xy.x, xy.y, event.button);
155         buttons_down |= 1 << event.button;
156         onscreen_canvas.setCapture(true);
157     };
158     mousemove = Module.cwrap('mousemove', 'void',
159                              ['number', 'number', 'number']);
160     onscreen_canvas.onmousemove = function(event) {
161         if (buttons_down) {
162             var xy = relative_mouse_coords(event, onscreen_canvas);
163             mousemove(xy.x, xy.y, buttons_down);
164         }
165     };
166     mouseup = Module.cwrap('mouseup', 'void',
167                            ['number', 'number', 'number']);
168     onscreen_canvas.onmouseup = function(event) {
169         if (buttons_down & (1 << event.button)) {
170             buttons_down ^= 1 << event.button;
171             var xy = relative_mouse_coords(event, onscreen_canvas);
172             mouseup(xy.x, xy.y, event.button);
173         }
174     };
175
176     // Set up keyboard handlers. We do all the actual keyboard
177     // handling in onkeydown; but we also call event.preventDefault()
178     // in both the keydown and keypress handlers. This means that
179     // while the canvas itself has focus, _all_ keypresses go only to
180     // the puzzle - so users of this puzzle collection in other media
181     // can indulge their instinct to press ^R for redo, for example,
182     // without accidentally reloading the page.
183     key = Module.cwrap('key', 'void', ['number', 'number', 'string',
184                                        'string', 'number', 'number']);
185     onscreen_canvas.onkeydown = function(event) {
186         key(event.keyCode, event.charCode, event.key, event.char,
187             event.shiftKey ? 1 : 0, event.ctrlKey ? 1 : 0);
188         event.preventDefault();
189     };
190     onscreen_canvas.onkeypress = function(event) {
191         event.preventDefault();
192     };
193
194     // command() is a C function called to pass back events which
195     // don't fall into other categories like mouse and key events.
196     // Mostly those are button presses, but there's also one for the
197     // game-type dropdown having been changed.
198     command = Module.cwrap('command', 'void', ['number']);
199
200     // Event handlers for buttons and things, which call command().
201     document.getElementById("specific").onclick = function(event) {
202         // Ensure we don't accidentally process these events when a
203         // dialog is actually active, e.g. because the button still
204         // has keyboard focus
205         if (dlg_dimmer === null)
206             command(0);
207     };
208     document.getElementById("random").onclick = function(event) {
209         if (dlg_dimmer === null)
210             command(1);
211     };
212     document.getElementById("new").onclick = function(event) {
213         if (dlg_dimmer === null)
214             command(5);
215     };
216     document.getElementById("restart").onclick = function(event) {
217         if (dlg_dimmer === null)
218             command(6);
219     };
220     undo_button = document.getElementById("undo");
221     undo_button.onclick = function(event) {
222         if (dlg_dimmer === null)
223             command(7);
224     };
225     redo_button = document.getElementById("redo");
226     redo_button.onclick = function(event) {
227         if (dlg_dimmer === null)
228             command(8);
229     };
230     document.getElementById("solve").onclick = function(event) {
231         if (dlg_dimmer === null)
232             command(9);
233     };
234
235     gametypeselector = document.getElementById("gametype");
236     gametypeselector.onchange = function(event) {
237         if (dlg_dimmer === null)
238             command(2);
239     };
240
241     // In IE, the canvas doesn't automatically gain focus on a mouse
242     // click, so make sure it does
243     onscreen_canvas.addEventListener("mousedown", function(event) {
244         onscreen_canvas.focus();
245     });
246
247     // In our dialog boxes, Return and Escape should be like pressing
248     // OK and Cancel respectively
249     document.addEventListener("keydown", function(event) {
250
251         if (dlg_dimmer !== null && event.keyCode == 13) {
252             for (var i in dlg_return_funcs)
253                 dlg_return_funcs[i]();
254             command(3);
255         }
256
257         if (dlg_dimmer !== null && event.keyCode == 27)
258             command(4);
259     });
260
261     // Set up the function pointers we haven't already grabbed. 
262     dlg_return_sval = Module.cwrap('dlg_return_sval', 'void',
263                                    ['number','string']);
264     dlg_return_ival = Module.cwrap('dlg_return_ival', 'void',
265                                    ['number','number']);
266     timer_callback = Module.cwrap('timer_callback', 'void', ['number']);
267
268     // Save references to the two permalinks.
269     permalink_desc = document.getElementById("permalink-desc");
270     permalink_seed = document.getElementById("permalink-seed");
271
272     // Default to giving keyboard focus to the puzzle.
273     onscreen_canvas.focus();
274
275     // Create the resize handle.
276     var resize_handle = document.createElement("canvas");
277     resize_handle.width = 10;
278     resize_handle.height = 10;
279     {
280         var ctx = resize_handle.getContext("2d");
281         ctx.beginPath();
282         for (var i = 1; i <= 7; i += 3) {
283             ctx.moveTo(8.5, i + 0.5);
284             ctx.lineTo(i + 0.5, 8.5);
285         }
286         ctx.lineWidth = '1px';
287         ctx.lineCap = 'round';
288         ctx.lineJoin = 'round';
289         ctx.strokeStyle = '#000000';
290         ctx.stroke();
291     }
292     resizable_div = document.getElementById("resizable");
293     resizable_div.appendChild(resize_handle);
294     resize_handle.style.position = 'absolute';
295     resize_handle.style.zIndex = 98;
296     resize_handle.style.bottom = "0";
297     resize_handle.style.right = "0";
298     resize_handle.style.cursor = "se-resize";
299     resize_handle.title = "Drag to resize the puzzle. Right-click to restore the default size.";
300     var resize_xbase = null, resize_ybase = null, restore_pending = false;
301     var resize_xoffset = null, resize_yoffset = null;
302     var resize_puzzle = Module.cwrap('resize_puzzle',
303                                      'void', ['number', 'number']);
304     var restore_puzzle_size = Module.cwrap('restore_puzzle_size', 'void', []);
305     resize_handle.oncontextmenu = function(event) { return false; }
306     resize_handle.onmousedown = function(event) {
307         if (event.button == 0) {
308             var xy = element_coords(onscreen_canvas);
309             resize_xbase = xy.x + onscreen_canvas.width / 2;
310             resize_ybase = xy.y;
311             resize_xoffset = xy.x + onscreen_canvas.width - event.pageX;
312             resize_yoffset = xy.y + onscreen_canvas.height - event.pageY;
313         } else {
314             restore_pending = true;
315         }
316         resize_handle.setCapture(true);
317         event.preventDefault();
318     };
319     window.addEventListener("mousemove", function(event) {
320         if (resize_xbase !== null && resize_ybase !== null) {
321             resize_puzzle((event.pageX + resize_xoffset - resize_xbase) * 2,
322                           (event.pageY + resize_yoffset - resize_ybase));
323             event.preventDefault();
324             // Chrome insists on selecting text during a resize drag
325             // no matter what I do
326             if (window.getSelection)
327                 window.getSelection().removeAllRanges();
328             else
329                 document.selection.empty();        }
330     });
331     window.addEventListener("mouseup", function(event) {
332         if (resize_xbase !== null && resize_ybase !== null) {
333             resize_xbase = null;
334             resize_ybase = null;
335             onscreen_canvas.focus(); // return focus to the puzzle
336             event.preventDefault();
337         } else if (restore_pending) {
338             // If you have the puzzle at larger than normal size and
339             // then right-click to restore, I haven't found any way to
340             // stop Chrome and IE popping up a context menu on the
341             // revealed piece of document when you release the button
342             // except by putting the actual restore into a setTimeout.
343             // Gah.
344             setTimeout(function() {
345                 restore_pending = false;
346                 restore_puzzle_size();
347                 onscreen_canvas.focus();
348             }, 20);
349             event.preventDefault();
350         }
351     });
352
353     // Run the C setup function, passing argv[1] as the fragment
354     // identifier (so that permalinks of the form puzzle.html#game-id
355     // can launch the specified id).
356     Module.callMain([location.hash]);
357
358     // And if we get here with everything having gone smoothly, i.e.
359     // we haven't crashed for one reason or another during setup, then
360     // it's probably safe to hide the 'sorry, no puzzle here' div and
361     // show the div containing the actual puzzle.
362     document.getElementById("apology").style.display = "none";
363     document.getElementById("puzzle").style.display = "inline";
364 }