2 * emcclib.js: one of the Javascript components of an Emscripten-based
3 * web/Javascript front end for Puzzles.
5 * The other parts of this system live in emcc.c and emccpre.js.
7 * This file contains a set of Javascript functions which we insert
8 * into Emscripten's library object via the --js-library option; this
9 * allows us to provide JS code which can be called from the
10 * Emscripten-compiled C, mostly dealing with UI interaction of
14 mergeInto(LibraryManager.library, {
16 * void js_debug(const char *message);
18 * A function to write a diagnostic to the Javascript console.
19 * Unused in production, but handy in development.
21 js_debug: function(ptr) {
22 console.log(Pointer_stringify(ptr));
26 * void js_error_box(const char *message);
28 * A wrapper around Javascript's alert(), so the C code can print
29 * simple error message boxes (e.g. when invalid data is entered
30 * in a configuration dialog).
32 js_error_box: function(ptr) {
33 alert(Pointer_stringify(ptr));
37 * void js_remove_type_dropdown(void);
39 * Get rid of the drop-down list on the web page for selecting
40 * game presets. Called at setup time if the game back end
41 * provides neither presets nor configurability.
43 js_remove_type_dropdown: function() {
44 document.getElementById("gametype").style.display = "none";
48 * void js_remove_solve_button(void);
50 * Get rid of the Solve button on the web page. Called at setup
51 * time if the game doesn't support an in-game solve function.
53 js_remove_solve_button: function() {
54 document.getElementById("solve").style.display = "none";
58 * void js_add_preset(const char *name);
60 * Add a preset to the drop-down types menu. The provided text is
61 * the name of the preset. (The corresponding game_params stays on
62 * the C side and never comes out this far; we just pass a numeric
63 * index back to the C code when a selection is made.)
65 * The special 'Custom' preset is requested by passing NULL to
66 * this function, rather than the string "Custom", since in that
67 * case we need to do something special - see below.
69 js_add_preset: function(ptr) {
70 var name = (ptr == 0 ? "Custom" : Pointer_stringify(ptr));
71 var value = gametypeoptions.length;
73 var option = document.createElement("option");
75 option.appendChild(document.createTextNode(name));
76 gametypeselector.appendChild(option);
77 gametypeoptions.push(option);
80 // Create a _second_ element called 'Custom', which is
83 // Hiding this element (that is, setting it display:none)
84 // has the effect of making it not show up when the
85 // drop-down list is actually opened, but still show up
86 // when the item is selected.
88 // So what happens is that there's one element marked
89 // 'Custom' that the _user_ selects, but a second one to
90 // which we reset the dropdown after the config box
91 // returns (if we don't then turn out to select a
92 // different preset anyway). The point is that if the user
93 // has 'Custom' selected, but then wants to customise
94 // their settings a second time, we still get an onchange
95 // event when they select the Custom option again, which
96 // we wouldn't get if the browser thought it was already
97 // the selected one. But here, it's _not_ the selected
98 // option already; its invisible evil twin is selected.
99 option = document.createElement("option");
100 option.value = value;
101 option.appendChild(document.createTextNode(name));
102 option.style.display = "none";
103 gametypeselector.appendChild(option);
104 gametypehiddencustom = option;
109 * int js_get_selected_preset(void);
111 * Return the index of the currently selected value in the type
114 js_get_selected_preset: function() {
115 for (var i in gametypeoptions) {
116 if (gametypeoptions[i].selected) {
117 return gametypeoptions[i].value;
124 * void js_select_preset(int n);
126 * Cause a different value to be selected in the type dropdown
127 * (for when the user selects values from the Custom configurer
128 * which turn out to exactly match a preset).
130 js_select_preset: function(n) {
131 if (gametypeoptions[n].value == gametypehiddencustom.value) {
132 // If we're asked to select the visible Custom option,
133 // select the invisible one instead. See comment above in
135 gametypehiddencustom.selected = true;
137 gametypeoptions[n].selected = true;
142 * void js_get_date_64(unsigned *p);
144 * Return the current date, in milliseconds since the epoch
145 * (Javascript's native format), as a 64-bit integer. Used to
146 * invent an initial random seed for puzzle generation.
148 js_get_date_64: function(ptr) {
149 var d = (new Date()).valueOf();
150 setValue(ptr, d, 'i64');
154 * void js_update_permalinks(const char *desc, const char *seed);
156 * Update the permalinks on the web page for a new game
157 * description and optional random seed. desc can never be NULL,
158 * but seed might be (if the game was generated by entering a
159 * descriptive id by hand), in which case we suppress display of
160 * the random seed permalink.
162 js_update_permalinks: function(desc, seed) {
163 desc = Pointer_stringify(desc);
164 permalink_desc.href = "#" + desc;
167 permalink_seed.style.display = "none";
169 seed = Pointer_stringify(seed);
170 permalink_seed.href = "#" + seed;
171 permalink_seed.style.display = "inline";
176 * void js_enable_undo_redo(int undo, int redo);
178 * Set the enabled/disabled states of the undo and redo buttons,
181 js_enable_undo_redo: function(undo, redo) {
182 undo_button.disabled = (undo == 0);
183 redo_button.disabled = (redo == 0);
187 * void js_activate_timer();
189 * Start calling the C timer_callback() function every 20ms.
191 js_activate_timer: function() {
192 if (timer === null) {
193 timer_reference_date = (new Date()).valueOf();
194 timer = setInterval(function() {
195 var now = (new Date()).valueOf();
196 timer_callback((now - timer_reference_date) / 1000.0);
197 timer_reference_date = now;
204 * void js_deactivate_timer();
206 * Stop calling the C timer_callback() function every 20ms.
208 js_deactivate_timer: function() {
209 if (timer !== null) {
210 clearInterval(timer);
216 * void js_canvas_start_draw(void);
218 * Prepare to do some drawing on the canvas.
220 js_canvas_start_draw: function() {
221 ctx = offscreen_canvas.getContext('2d');
222 update_xmin = update_xmax = update_ymin = update_ymax = undefined;
226 * void js_canvas_draw_update(int x, int y, int w, int h);
228 * Mark a rectangle of the off-screen canvas as needing to be
229 * copied to the on-screen one.
231 js_canvas_draw_update: function(x, y, w, h) {
233 * Currently we do this in a really simple way, just by taking
234 * the smallest rectangle containing all updates so far. We
235 * could instead keep the data in a richer form (e.g. retain
236 * multiple smaller rectangles needing update, and only redraw
237 * the whole thing beyond a certain threshold) but this will
240 if (update_xmin === undefined || update_xmin > x) update_xmin = x;
241 if (update_ymin === undefined || update_ymin > y) update_ymin = y;
242 if (update_xmax === undefined || update_xmax < x+w) update_xmax = x+w;
243 if (update_ymax === undefined || update_ymax < y+h) update_ymax = y+h;
247 * void js_canvas_end_draw(void);
249 * Finish the drawing, by actually copying the newly drawn stuff
250 * to the on-screen canvas.
252 js_canvas_end_draw: function() {
253 if (update_xmin !== undefined) {
254 var onscreen_ctx = onscreen_canvas.getContext('2d');
255 onscreen_ctx.drawImage(offscreen_canvas,
256 update_xmin, update_ymin,
257 update_xmax - update_xmin,
258 update_ymax - update_ymin,
259 update_xmin, update_ymin,
260 update_xmax - update_xmin,
261 update_ymax - update_ymin);
267 * void js_canvas_draw_rect(int x, int y, int w, int h,
268 * const char *colour);
272 js_canvas_draw_rect: function(x, y, w, h, colptr) {
273 ctx.fillStyle = Pointer_stringify(colptr);
274 ctx.fillRect(x, y, w, h);
278 * void js_canvas_clip_rect(int x, int y, int w, int h);
280 * Set a clipping rectangle.
282 js_canvas_clip_rect: function(x, y, w, h) {
285 ctx.rect(x, y, w, h);
290 * void js_canvas_unclip(void);
292 * Reset to no clipping.
294 js_canvas_unclip: function() {
299 * void js_canvas_draw_line(float x1, float y1, float x2, float y2,
300 * int width, const char *colour);
302 * Draw a line. We must adjust the coordinates by 0.5 because
303 * Javascript's canvas coordinates appear to be pixel corners,
304 * whereas we want pixel centres. Also, we manually draw the pixel
305 * at each end of the line, which our clients will expect but
306 * Javascript won't reliably do by default (in common with other
307 * Postscriptish drawing frameworks).
309 js_canvas_draw_line: function(x1, y1, x2, y2, width, colour) {
310 colour = Pointer_stringify(colour);
313 ctx.moveTo(x1 + 0.5, y1 + 0.5);
314 ctx.lineTo(x2 + 0.5, y2 + 0.5);
315 ctx.lineWidth = width;
316 ctx.lineCap = 'round';
317 ctx.lineJoin = 'round';
318 ctx.strokeStyle = colour;
320 ctx.fillStyle = colour;
321 ctx.fillRect(x1, y1, 1, 1);
322 ctx.fillRect(x2, y2, 1, 1);
326 * void js_canvas_draw_poly(int *points, int npoints,
327 * const char *fillcolour,
328 * const char *outlinecolour);
332 js_canvas_draw_poly: function(pointptr, npoints, fill, outline) {
334 ctx.moveTo(getValue(pointptr , 'i32') + 0.5,
335 getValue(pointptr+4, 'i32') + 0.5);
336 for (var i = 1; i < npoints; i++)
337 ctx.lineTo(getValue(pointptr+8*i , 'i32') + 0.5,
338 getValue(pointptr+8*i+4, 'i32') + 0.5);
341 ctx.fillStyle = Pointer_stringify(fill);
345 ctx.lineCap = 'round';
346 ctx.lineJoin = 'round';
347 ctx.strokeStyle = Pointer_stringify(outline);
352 * void js_canvas_draw_circle(int x, int y, int r,
353 * const char *fillcolour,
354 * const char *outlinecolour);
358 js_canvas_draw_circle: function(x, y, r, fill, outline) {
360 ctx.arc(x + 0.5, y + 0.5, r, 0, 2*Math.PI);
362 ctx.fillStyle = Pointer_stringify(fill);
366 ctx.lineCap = 'round';
367 ctx.lineJoin = 'round';
368 ctx.strokeStyle = Pointer_stringify(outline);
373 * int js_canvas_find_font_midpoint(int height, const char *fontptr);
375 * Return the adjustment required for text displayed using
376 * ALIGN_VCENTRE. We want to place the midpoint between the
377 * baseline and the cap-height at the specified position; so this
378 * function returns the adjustment which, when added to the
379 * desired centre point, returns the y-coordinate at which you
380 * should put the baseline.
382 * There is no sensible method of querying this kind of font
383 * metric in Javascript, so instead we render a piece of test text
384 * to a throwaway offscreen canvas and then read the pixel data
385 * back out to find the highest and lowest pixels. That's good
386 * _enough_ (in that we only needed the answer to the nearest
387 * pixel anyway), but rather disgusting!
389 * Since this is a very expensive operation, we cache the results
390 * per (font,height) pair.
392 js_canvas_find_font_midpoint: function(height, font) {
393 font = Pointer_stringify(font);
395 // Reuse cached value if possible
396 if (midpoint_cache[font] !== undefined)
397 return midpoint_cache[font];
399 // Find the width of the string
400 var ctx1 = onscreen_canvas.getContext('2d');
402 var width = ctx1.measureText(midpoint_test_str).width;
404 // Construct a test canvas of appropriate size, initialise it to
405 // black, and draw the string on it in white
406 var measure_canvas = document.createElement('canvas');
407 var ctx2 = measure_canvas.getContext('2d');
408 ctx2.canvas.width = width;
409 ctx2.canvas.height = 2*height;
410 ctx2.fillStyle = "#000000";
411 ctx2.fillRect(0, 0, width, 2*height);
412 var baseline = (1.5*height) | 0;
413 ctx2.fillStyle = "#ffffff";
415 ctx2.fillText(midpoint_test_str, 0, baseline);
417 // Scan the contents of the test canvas to find the top and bottom
419 var pixels = ctx2.getImageData(0, 0, width, 2*height).data;
420 var ymin = 2*height, ymax = -1;
421 for (var y = 0; y < 2*height; y++) {
422 for (var x = 0; x < width; x++) {
423 if (pixels[4*(y*width+x)] != 0) {
424 if (ymin > y) ymin = y;
425 if (ymax < y) ymax = y;
431 var ret = (baseline - (ymin + ymax) / 2) | 0;
432 midpoint_cache[font] = ret;
437 * void js_canvas_draw_text(int x, int y, int halign,
438 * const char *colptr, const char *fontptr,
441 * Draw text. Vertical alignment has been taken care of on the C
442 * side, by optionally calling the above function. Horizontal
443 * alignment is handled here, since we can get the canvas draw
444 * function to do it for us with almost no extra effort.
446 js_canvas_draw_text: function(x, y, halign, colptr, fontptr, text) {
447 ctx.font = Pointer_stringify(fontptr);
448 ctx.fillStyle = Pointer_stringify(colptr);
449 ctx.textAlign = (halign == 0 ? 'left' :
450 halign == 1 ? 'center' : 'right');
451 ctx.textBaseline = 'alphabetic';
452 ctx.fillText(Pointer_stringify(text), x, y);
456 * int js_canvas_new_blitter(int w, int h);
458 * Create a new blitter object, which is just an offscreen canvas
459 * of the specified size.
461 js_canvas_new_blitter: function(w, h) {
462 var id = blittercount++;
463 blitters[id] = document.createElement("canvas");
464 blitters[id].width = w;
465 blitters[id].height = h;
470 * void js_canvas_free_blitter(int id);
472 * Free a blitter (or rather, destroy our reference to it so JS
473 * can garbage-collect it, and also enforce that we don't
474 * accidentally use it again afterwards).
476 js_canvas_free_blitter: function(id) {
481 * void js_canvas_copy_to_blitter(int id, int x, int y, int w, int h);
483 * Copy from the puzzle image to a blitter. The size is passed to
484 * us, partly so we don't have to remember the size of each
485 * blitter, but mostly so that the C side can adjust the copy
486 * rectangle in the case where it partially overlaps the edge of
489 js_canvas_copy_to_blitter: function(id, x, y, w, h) {
490 var blitter_ctx = blitters[id].getContext('2d');
491 blitter_ctx.drawImage(offscreen_canvas,
497 * void js_canvas_copy_from_blitter(int id, int x, int y, int w, int h);
499 * Copy from a blitter back to the puzzle image. As above, the
500 * size of the copied rectangle is passed to us from the C side
501 * and may already have been modified.
503 js_canvas_copy_from_blitter: function(id, x, y, w, h) {
504 ctx.drawImage(blitters[id],
510 * void js_canvas_make_statusbar(void);
512 * Cause a status bar to exist. Called at setup time if the puzzle
513 * back end turns out to want one.
515 js_canvas_make_statusbar: function() {
516 var statustd = document.getElementById("statusbarholder");
517 statusbar = document.createElement("div");
518 statusbar.style.overflow = "hidden";
519 statusbar.style.width = onscreen_canvas.width - 4;
520 statusbar.style.height = "1.2em";
521 statusbar.style.background = "#d8d8d8";
522 statusbar.style.borderLeft = '2px solid #c8c8c8';
523 statusbar.style.borderTop = '2px solid #c8c8c8';
524 statusbar.style.borderRight = '2px solid #e8e8e8';
525 statusbar.style.borderBottom = '2px solid #e8e8e8';
526 statusbar.appendChild(document.createTextNode(" "));
527 statustd.appendChild(statusbar);
531 * void js_canvas_set_statusbar(const char *text);
533 * Set the text in the status bar.
535 js_canvas_set_statusbar: function(ptr) {
536 var text = Pointer_stringify(ptr);
537 statusbar.replaceChild(document.createTextNode(text),
538 statusbar.lastChild);
542 * void js_canvas_set_size(int w, int h);
544 * Set the size of the puzzle canvas. Called at setup, and every
545 * time the user picks new puzzle settings requiring a different
548 js_canvas_set_size: function(w, h) {
549 onscreen_canvas.width = w;
550 offscreen_canvas.width = w;
551 if (statusbar !== null)
552 statusbar.style.width = w - 4;
554 onscreen_canvas.height = h;
555 offscreen_canvas.height = h;
559 * void js_dialog_init(const char *title);
561 * Begin constructing a 'dialog box' which will be popped up in an
562 * overlay on top of the rest of the puzzle web page.
564 js_dialog_init: function(titletext) {
565 // Create an overlay on the page which darkens everything
567 dlg_dimmer = document.createElement("div");
568 dlg_dimmer.style.width = "100%";
569 dlg_dimmer.style.height = "100%";
570 dlg_dimmer.style.background = '#000000';
571 dlg_dimmer.style.position = 'fixed';
572 dlg_dimmer.style.opacity = 0.3;
573 dlg_dimmer.style.top = dlg_dimmer.style.left = 0;
574 dlg_dimmer.style["z-index"] = 99;
576 // Now create a form which sits on top of that in turn.
577 dlg_form = document.createElement("form");
578 dlg_form.style.width = window.innerWidth * 2 / 3;
579 dlg_form.style.opacity = 1;
580 dlg_form.style.background = '#ffffff';
581 dlg_form.style.color = '#000000';
582 dlg_form.style.position = 'absolute';
583 dlg_form.style.border = "2px solid black";
584 dlg_form.style.padding = 20;
585 dlg_form.style.top = window.innerHeight / 10;
586 dlg_form.style.left = window.innerWidth / 6;
587 dlg_form.style["z-index"] = 100;
589 var title = document.createElement("p");
590 title.style.marginTop = "0px";
591 title.appendChild(document.createTextNode
592 (Pointer_stringify(titletext)));
593 dlg_form.appendChild(title);
595 dlg_return_funcs = [];
600 * void js_dialog_string(int i, const char *title, const char *initvalue);
602 * Add a string control (that is, an edit box) to the dialog under
605 js_dialog_string: function(index, title, initialtext) {
606 dlg_form.appendChild(document.createTextNode(Pointer_stringify(title)));
607 var editbox = document.createElement("input");
608 editbox.type = "text";
609 editbox.value = Pointer_stringify(initialtext);
610 dlg_form.appendChild(editbox);
611 dlg_form.appendChild(document.createElement("br"));
613 dlg_return_funcs.push(function() {
614 dlg_return_sval(index, editbox.value);
619 * void js_dialog_choices(int i, const char *title, const char *choicelist,
622 * Add a choices control (i.e. a drop-down list) to the dialog
623 * under construction. The 'choicelist' parameter is unchanged
624 * from the way the puzzle back end will have supplied it: i.e.
625 * it's still encoded as a single string whose first character
626 * gives the separator.
628 js_dialog_choices: function(index, title, choicelist, initvalue) {
629 dlg_form.appendChild(document.createTextNode(Pointer_stringify(title)));
630 var dropdown = document.createElement("select");
631 var choicestr = Pointer_stringify(choicelist);
632 var items = choicestr.slice(1).split(choicestr[0]);
634 for (var i in items) {
635 var option = document.createElement("option");
637 option.appendChild(document.createTextNode(items[i]));
638 if (i == initvalue) option.selected = true;
639 dropdown.appendChild(option);
640 options.push(option);
642 dlg_form.appendChild(dropdown);
643 dlg_form.appendChild(document.createElement("br"));
645 dlg_return_funcs.push(function() {
647 for (var i in options) {
648 if (options[i].selected) {
649 val = options[i].value;
653 dlg_return_ival(index, val);
658 * void js_dialog_boolean(int i, const char *title, int initvalue);
660 * Add a boolean control (a checkbox) to the dialog under
661 * construction. Checkboxes are generally expected to be sensitive
662 * on their label text as well as the box itself, so for this
663 * control we create an actual label rather than merely a text
664 * node (and hence we must allocate an id to the checkbox so that
665 * the label can refer to it).
667 js_dialog_boolean: function(index, title, initvalue) {
668 var checkbox = document.createElement("input");
669 checkbox.type = "checkbox";
670 checkbox.id = "cb" + String(dlg_next_id++);
671 checkbox.checked = (initvalue != 0);
672 dlg_form.appendChild(checkbox);
673 var checkboxlabel = document.createElement("label");
674 checkboxlabel.setAttribute("for", checkbox.id);
675 checkboxlabel.textContent = Pointer_stringify(title);
676 dlg_form.appendChild(checkboxlabel);
677 dlg_form.appendChild(document.createElement("br"));
679 dlg_return_funcs.push(function() {
680 dlg_return_ival(index, checkbox.checked ? 1 : 0);
685 * void js_dialog_launch(void);
687 * Finish constructing a dialog, and actually display it, dimming
688 * everything else on the page.
690 js_dialog_launch: function() {
691 // Put in the OK and Cancel buttons at the bottom.
694 button = document.createElement("input");
695 button.type = "button";
697 button.onclick = function(event) {
698 for (var i in dlg_return_funcs)
699 dlg_return_funcs[i]();
702 dlg_form.appendChild(button);
704 button = document.createElement("input");
705 button.type = "button";
706 button.value = "Cancel";
707 button.onclick = function(event) {
710 dlg_form.appendChild(button);
712 document.body.appendChild(dlg_dimmer);
713 document.body.appendChild(dlg_form);
717 * void js_dialog_cleanup(void);
719 * Stop displaying a dialog, and clean up the internal state
720 * associated with it.
722 js_dialog_cleanup: function() {
723 document.body.removeChild(dlg_dimmer);
724 document.body.removeChild(dlg_form);
725 dlg_dimmer = dlg_form = null;
726 onscreen_canvas.focus();
730 * void js_focus_canvas(void);
732 * Return keyboard focus to the puzzle canvas. Called after a
733 * puzzle-control button is pressed, which tends to have the side
734 * effect of taking focus away from the canvas.
736 js_focus_canvas: function() {
737 onscreen_canvas.focus();