chiark / gitweb /
New front end! To complement the webification of my puzzles via Java
authorSimon Tatham <anakin@pobox.com>
Sat, 30 Mar 2013 20:16:21 +0000 (20:16 +0000)
committerSimon Tatham <anakin@pobox.com>
Sat, 30 Mar 2013 20:16:21 +0000 (20:16 +0000)
applets, here's an alternative webification in Javascript, using
Emscripten in asm.js mode (so that as browsers incorporate asm.js
optimisation, the game generation should run really fast).

[originally from svn r9781]

Buildscr
Recipe
emcc.c [new file with mode: 0644]
emcclib.js [new file with mode: 0644]
emccpre.js [new file with mode: 0644]
emccx.json [new file with mode: 0644]
html/jspage.pl [new file with mode: 0644]
mkfiles.pl

index 6d2237d23a92f4798a7158522abf7d30297ea51c..6925ce35dc9487c479622a6cda3dc3f725a2c72e 100644 (file)
--- a/Buildscr
+++ b/Buildscr
@@ -112,6 +112,16 @@ ifneq "$(JAVA_UNFINISHED)" "" in puzzles do perl mkfiles.pl
 # compiler and tools.
 in puzzles do make -f Makefile.nestedvm NESTEDVM=$(NESTEDVM) VER=-DREVISION=$(revision)
 
+# Build the Javascript applets. Since my master build machine doesn't
+# have the right dependencies installed for Emscripten, I do this by a
+# delegation.
+in puzzles do mkdir js # so we can tell output .js files from emcc*.js
+delegate emscripten
+  in puzzles do make -f Makefile.emcc OUTPREFIX=js/ clean
+  in puzzles do make -f Makefile.emcc OUTPREFIX=js/
+  return puzzles/js/*.js
+enddelegate
+
 # Set up .htaccess containing a redirect for the archive filename.
 in puzzles do echo "AddType application/octet-stream .chm" > .htaccess
 in puzzles do echo "AddType application/octet-stream .hlp" >> .htaccess
@@ -132,6 +142,7 @@ deliver puzzles/puzzles.cnt $@
 deliver puzzles/puzzles.zip $@
 deliver puzzles/Output/setup.exe puzzles-r$(revision)-installer.exe
 deliver puzzles/*.jar java/$@
+deliver puzzles/js/*.js js/$@
 deliver puzzles/html/*.html html/$@
 deliver puzzles/html/*.pl html/$@
 
diff --git a/Recipe b/Recipe
index 425f4190deb3edaccd6466eb995e2307fcd012d7..5d63ae5c41b865565bedbd5c3954260f49323434 100644 (file)
--- a/Recipe
+++ b/Recipe
@@ -15,6 +15,7 @@
 !makefile osx Makefile.osx
 !makefile gnustep Makefile.gnustep
 !makefile nestedvm Makefile.nestedvm
+!makefile emcc Makefile.emcc
 
 !srcdir icons/
 
diff --git a/emcc.c b/emcc.c
new file mode 100644 (file)
index 0000000..fbb549f
--- /dev/null
+++ b/emcc.c
@@ -0,0 +1,772 @@
+/*
+ * emcc.c: the C component of an Emscripten-based web/Javascript front
+ * end for Puzzles.
+ *
+ * The Javascript parts of this system live in emcclib.js and
+ * emccpre.js.
+ */
+
+/*
+ * Further thoughts on possible enhancements:
+ *
+ *  - I think it might be feasible to have these JS puzzles permit
+ *    loading and saving games in disk files. Saving would be done by
+ *    constructing a data: URI encapsulating the save file, and then
+ *    telling the browser to visit that URI with the effect that it
+ *    would naturally pop up a 'where would you like to save this'
+ *    dialog box. Loading, more or less similarly, might be feasible
+ *    by using the DOM File API to ask the user to select a file and
+ *    permit us to see its contents.
+ *
+ *  - it ought to be possible to make the puzzle canvases resizable,
+ *    by superimposing some kind of draggable resize handle. Also I
+ *    quite like the idea of having a few buttons for standard sizes:
+ *    reset to default size, maximise to the browser window dimensions
+ *    (if we can find those out), and perhaps even go full-screen.
+ *
+ *  - I should think about whether these webified puzzles can support
+ *    touchscreen-based tablet browsers (assuming there are any that
+ *    can cope with the reasonably modern JS and run it fast enough to
+ *    be worthwhile).
+ *
+ *  - think about making use of localStorage. It might be useful to
+ *    let the user save games into there as an alternative to disk
+ *    files - disk files are all very well for getting the save right
+ *    out of your browser to (e.g.) email to me as a bug report, but
+ *    for just resuming a game you were in the middle of, you'd
+ *    probably rather have a nice simple 'quick save' and 'quick load'
+ *    button pair. Also, that might be a useful place to store
+ *    preferences, if I ever get round to writing a preferences UI.
+ *
+ *  - some CSS to make the button bar and configuration dialogs a
+ *    little less ugly would probably not go amiss.
+ *
+ *  - this is a downright silly idea, but it does occur to me that if
+ *    I were to write a PDF output driver for the Puzzles printing
+ *    API, then I might be able to implement a sort of 'printing'
+ *    feature in this front end, using data: URIs again. (Ask the user
+ *    exactly what they want printed, then construct an appropriate
+ *    PDF and embed it in a gigantic data: URI. Then they can print
+ *    that using whatever they normally use to print PDFs!)
+ */
+
+#include <string.h>
+
+#include "puzzles.h"
+
+/*
+ * Extern references to Javascript functions provided in emcclib.js.
+ */
+extern void js_debug(const char *);
+extern void js_error_box(const char *message);
+extern void js_remove_type_dropdown(void);
+extern void js_remove_solve_button(void);
+extern void js_add_preset(const char *name);
+extern int js_get_selected_preset(void);
+extern void js_select_preset(int n);
+extern void js_get_date_64(unsigned *p);
+extern void js_update_permalinks(const char *desc, const char *seed);
+extern void js_enable_undo_redo(int undo, int redo);
+extern void js_activate_timer();
+extern void js_deactivate_timer();
+extern void js_canvas_start_draw(void);
+extern void js_canvas_draw_update(int x, int y, int w, int h);
+extern void js_canvas_end_draw(void);
+extern void js_canvas_draw_rect(int x, int y, int w, int h,
+                                const char *colour);
+extern void js_canvas_clip_rect(int x, int y, int w, int h);
+extern void js_canvas_unclip(void);
+extern void js_canvas_draw_line(float x1, float y1, float x2, float y2,
+                                int width, const char *colour);
+extern void js_canvas_draw_poly(int *points, int npoints,
+                                const char *fillcolour,
+                                const char *outlinecolour);
+extern void js_canvas_draw_circle(int x, int y, int r,
+                                  const char *fillcolour,
+                                  const char *outlinecolour);
+extern int js_canvas_find_font_midpoint(int height, const char *fontptr);
+extern void js_canvas_draw_text(int x, int y, int halign,
+                                const char *colptr, const char *fontptr,
+                                const char *text);
+extern int js_canvas_new_blitter(int w, int h);
+extern void js_canvas_free_blitter(int id);
+extern void js_canvas_copy_to_blitter(int id, int x, int y, int w, int h);
+extern void js_canvas_copy_from_blitter(int id, int x, int y, int w, int h);
+extern void js_canvas_make_statusbar(void);
+extern void js_canvas_set_statusbar(const char *text);
+extern void js_canvas_set_size(int w, int h);
+
+extern void js_dialog_init(const char *title);
+extern void js_dialog_string(int i, const char *title, const char *initvalue);
+extern void js_dialog_choices(int i, const char *title, const char *choicelist,
+                              int initvalue);
+extern void js_dialog_boolean(int i, const char *title, int initvalue);
+extern void js_dialog_launch(void);
+extern void js_dialog_cleanup(void);
+extern void js_focus_canvas(void);
+
+/*
+ * Call JS to get the date, and use that to initialise our random
+ * number generator to invent the first game seed.
+ */
+void get_random_seed(void **randseed, int *randseedsize)
+{
+    unsigned *ret = snewn(2, unsigned);
+    js_get_date_64(ret);
+    *randseed = ret;
+    *randseedsize = 2*sizeof(unsigned);
+}
+
+/*
+ * Fatal error, called in cases of complete despair such as when
+ * malloc() has returned NULL.
+ */
+void fatal(char *fmt, ...)
+{
+    char buf[512];
+    va_list ap;
+
+    strcpy(buf, "puzzle fatal error: ");
+
+    va_start(ap, fmt);
+    vsnprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), fmt, ap);
+    va_end(ap);
+
+    js_error_box(buf);
+}
+
+/*
+ * HTMLish names for the colours allocated by the puzzle.
+ */
+char **colour_strings;
+int ncolours;
+
+/*
+ * The global midend object.
+ */
+midend *me;
+
+/* ----------------------------------------------------------------------
+ * Timing functions.
+ */
+int timer_active = FALSE;
+void deactivate_timer(frontend *fe)
+{
+    js_deactivate_timer();
+    timer_active = FALSE;
+}
+void activate_timer(frontend *fe)
+{
+    if (!timer_active) {
+        js_activate_timer();
+        timer_active = TRUE;
+    }
+}
+void timer_callback(double tplus)
+{
+    if (timer_active)
+        midend_timer(me, tplus);
+}
+
+/* ----------------------------------------------------------------------
+ * Helper function to resize the canvas, and variables to remember its
+ * size for other functions (e.g. trimming blitter rectangles).
+ */
+static int canvas_w, canvas_h;
+static void resize(void)
+{
+    int w, h;
+    w = h = INT_MAX;
+    midend_size(me, &w, &h, FALSE);
+    js_canvas_set_size(w, h);
+    canvas_w = w;
+    canvas_h = h;
+}
+
+/*
+ * HTML doesn't give us a default frontend colour of its own, so we
+ * just make up a lightish grey ourselves.
+ */
+void frontend_default_colour(frontend *fe, float *output)
+{
+    output[0] = output[1] = output[2] = 0.9F;
+}
+
+/*
+ * Helper function called from all over the place to ensure the undo
+ * and redo buttons get properly enabled and disabled after every move
+ * or undo or new-game event.
+ */
+static void update_undo_redo(void)
+{
+    js_enable_undo_redo(midend_can_undo(me), midend_can_redo(me));
+}
+
+/*
+ * Mouse event handlers called from JS.
+ */
+void mousedown(int x, int y, int button)
+{
+    button = (button == 0 ? LEFT_BUTTON :
+              button == 1 ? MIDDLE_BUTTON : RIGHT_BUTTON);
+    midend_process_key(me, x, y, button);
+    update_undo_redo();
+}
+
+void mouseup(int x, int y, int button)
+{
+    button = (button == 0 ? LEFT_RELEASE :
+              button == 1 ? MIDDLE_RELEASE : RIGHT_RELEASE);
+    midend_process_key(me, x, y, button);
+    update_undo_redo();
+}
+
+void mousemove(int x, int y, int buttons)
+{
+    int button = (buttons & 2 ? MIDDLE_DRAG :
+                  buttons & 4 ? RIGHT_DRAG : LEFT_DRAG);
+    midend_process_key(me, x, y, button);
+    update_undo_redo();
+}
+
+/*
+ * Keyboard handler called from JS.
+ */
+void key(int keycode, int charcode, int shift, int ctrl)
+{
+    int keyevent = -1;
+    if (charcode != 0) {
+        keyevent = charcode & (ctrl ? 0x1F : 0xFF);
+    } else {
+        switch (keycode) {
+          case 8:
+            keyevent = '\177';         /* backspace */
+            break;
+          case 13:
+            keyevent = 13;             /* return */
+            break;
+          case 37:
+            keyevent = CURSOR_LEFT;
+            break;
+          case 38:
+            keyevent = CURSOR_UP;
+            break;
+          case 39:
+            keyevent = CURSOR_RIGHT;
+            break;
+          case 40:
+            keyevent = CURSOR_DOWN;
+            break;
+            /*
+             * We interpret Home, End, PgUp and PgDn as numeric keypad
+             * controls regardless of whether they're the ones on the
+             * numeric keypad (since we can't tell). The effect of
+             * this should only be that the non-numeric-pad versions
+             * of those keys generate directions in 8-way movement
+             * puzzles like Cube and Inertia.
+             */
+          case 35:                     /* End */
+            keyevent = MOD_NUM_KEYPAD | '1';
+            break;
+          case 34:                     /* PgDn */
+            keyevent = MOD_NUM_KEYPAD | '3';
+            break;
+          case 36:                     /* Home */
+            keyevent = MOD_NUM_KEYPAD | '7';
+            break;
+          case 33:                     /* PgUp */
+            keyevent = MOD_NUM_KEYPAD | '9';
+            break;
+          case 96: case 97: case 98: case 99: case 100:
+          case 101: case 102: case 103: case 104: case 105:
+            keyevent = MOD_NUM_KEYPAD | ('0' + keycode - 96);
+            break;
+          default:
+            /* not a key we care about */
+            return;
+        }
+    }
+    if (shift && keyevent >= 0x100)
+        keyevent |= MOD_SHFT;
+    if (ctrl && keyevent >= 0x100)
+        keyevent |= MOD_CTRL;
+
+    midend_process_key(me, 0, 0, keyevent);
+    update_undo_redo();
+}
+
+/*
+ * Helper function called from several places to update the permalinks
+ * whenever a new game is created.
+ */
+static void update_permalinks(void)
+{
+    char *desc, *seed;
+    desc = midend_get_game_id(me);
+    seed = midend_get_random_seed(me);
+    js_update_permalinks(desc, seed);
+    sfree(desc);
+    sfree(seed);
+}
+
+/* ----------------------------------------------------------------------
+ * Implementation of the drawing API by calling Javascript canvas
+ * drawing functions. (Well, half of it; the other half is on the JS
+ * side.)
+ */
+static void js_start_draw(void *handle)
+{
+    js_canvas_start_draw();
+}
+
+static void js_clip(void *handle, int x, int y, int w, int h)
+{
+    js_canvas_clip_rect(x, y, w, h);
+}
+
+static void js_unclip(void *handle)
+{
+    js_canvas_unclip();
+}
+
+static void js_draw_text(void *handle, int x, int y, int fonttype,
+                         int fontsize, int align, int colour, char *text)
+{
+    char fontstyle[80];
+    int halign;
+
+    sprintf(fontstyle, "%dpx %s", fontsize,
+            fonttype == FONT_FIXED ? "monospace" : "sans-serif");
+
+    if (align & ALIGN_VCENTRE)
+       y += js_canvas_find_font_midpoint(fontsize, fontstyle);
+
+    if (align & ALIGN_HCENTRE)
+       halign = 1;
+    else if (align & ALIGN_HRIGHT)
+        halign = 2;
+    else
+        halign = 0;
+
+    js_canvas_draw_text(x, y, halign, colour_strings[colour], fontstyle, text);
+}
+
+static void js_draw_rect(void *handle, int x, int y, int w, int h, int colour)
+{
+    js_canvas_draw_rect(x, y, w, h, colour_strings[colour]);
+}
+
+static void js_draw_line(void *handle, int x1, int y1, int x2, int y2,
+                         int colour)
+{
+    js_canvas_draw_line(x1, y1, x2, y2, 1, colour_strings[colour]);
+}
+
+static void js_draw_thick_line(void *handle, float thickness,
+                               float x1, float y1, float x2, float y2,
+                               int colour)
+{
+    js_canvas_draw_line(x1, y1, x2, y2, thickness, colour_strings[colour]);
+}
+
+static void js_draw_poly(void *handle, int *coords, int npoints,
+                         int fillcolour, int outlinecolour)
+{
+    js_canvas_draw_poly(coords, npoints,
+                        fillcolour >= 0 ? colour_strings[fillcolour] : NULL,
+                        colour_strings[outlinecolour]);
+}
+
+static void js_draw_circle(void *handle, int cx, int cy, int radius,
+                           int fillcolour, int outlinecolour)
+{
+    js_canvas_draw_circle(cx, cy, radius,
+                          fillcolour >= 0 ? colour_strings[fillcolour] : NULL,
+                          colour_strings[outlinecolour]);
+}
+
+struct blitter {
+    int id;                            /* allocated on the js side */
+    int w, h;                          /* easier to retain here */
+};
+
+static blitter *js_blitter_new(void *handle, int w, int h)
+{
+    blitter *bl = snew(blitter);
+    bl->w = w;
+    bl->h = h;
+    bl->id = js_canvas_new_blitter(w, h);
+    return bl;
+}
+
+static void js_blitter_free(void *handle, blitter *bl)
+{
+    js_canvas_free_blitter(bl->id);
+    sfree(bl);
+}
+
+static void trim_rect(int *x, int *y, int *w, int *h)
+{
+    /*
+     * Reduce the size of the copied rectangle to stop it going
+     * outside the bounds of the canvas.
+     */
+    if (*x < 0) {
+        *w += *x;
+        *x = 0;
+    }
+    if (*y < 0) {
+        *h += *y;
+        *y = 0;
+    }
+    if (*w > canvas_w - *x)
+        *w = canvas_w - *x;
+    if (*h > canvas_h - *y)
+        *h = canvas_h - *y;
+
+    if (*w < 0)
+        *w = 0;
+    if (*h < 0)
+        *h = 0;
+}
+
+static void js_blitter_save(void *handle, blitter *bl, int x, int y)
+{
+    int w = bl->w, h = bl->h;
+    trim_rect(&x, &y, &w, &h);
+    if (w > 0 && h > 0)
+        js_canvas_copy_to_blitter(bl->id, x, y, w, h);
+}
+
+static void js_blitter_load(void *handle, blitter *bl, int x, int y)
+{
+    int w = bl->w, h = bl->h;
+    trim_rect(&x, &y, &w, &h);
+    if (w > 0 && h > 0)
+        js_canvas_copy_from_blitter(bl->id, x, y, w, h);
+}
+
+static void js_draw_update(void *handle, int x, int y, int w, int h)
+{
+    trim_rect(&x, &y, &w, &h);
+    js_canvas_draw_update(x, y, w, h);
+}
+
+static void js_end_draw(void *handle)
+{
+    js_canvas_end_draw();
+}
+
+static void js_status_bar(void *handle, char *text)
+{
+    js_canvas_set_statusbar(text);
+}
+
+static char *js_text_fallback(void *handle, const char *const *strings,
+                              int nstrings)
+{
+    return dupstr(strings[0]); /* Emscripten has no trouble with UTF-8 */
+}
+
+const struct drawing_api js_drawing = {
+    js_draw_text,
+    js_draw_rect,
+    js_draw_line,
+    js_draw_poly,
+    js_draw_circle,
+    js_draw_update,
+    js_clip,
+    js_unclip,
+    js_start_draw,
+    js_end_draw,
+    js_status_bar,
+    js_blitter_new,
+    js_blitter_free,
+    js_blitter_save,
+    js_blitter_load,
+    NULL, NULL, NULL, NULL, NULL, NULL, /* {begin,end}_{doc,page,puzzle} */
+    NULL, NULL,                               /* line_width, line_dotted */
+    js_text_fallback,
+    js_draw_thick_line,
+};
+
+/* ----------------------------------------------------------------------
+ * Presets and game-configuration dialog support.
+ */
+static game_params **presets;
+static int custom_preset;
+
+static config_item *cfg = NULL;
+static int cfg_which;
+
+/*
+ * Set up a dialog box. This is pretty easy on the C side; most of the
+ * work is done in JS.
+ */
+static void cfg_start(int which)
+{
+    char *title;
+    int i;
+
+    cfg = midend_get_config(me, which, &title);
+    cfg_which = which;
+
+    js_dialog_init(title);
+    sfree(title);
+
+    for (i = 0; cfg[i].type != C_END; i++) {
+       switch (cfg[i].type) {
+         case C_STRING:
+            js_dialog_string(i, cfg[i].name, cfg[i].sval);
+           break;
+         case C_BOOLEAN:
+            js_dialog_boolean(i, cfg[i].name, cfg[i].ival);
+           break;
+         case C_CHOICES:
+            js_dialog_choices(i, cfg[i].name, cfg[i].sval, cfg[i].ival);
+           break;
+       }
+    }
+
+    js_dialog_launch();
+}
+
+/*
+ * Callbacks from JS when the OK button is clicked, to return the
+ * final state of each control.
+ */
+void dlg_return_sval(int index, const char *val)
+{
+    sfree(cfg[index].sval);
+    cfg[index].sval = dupstr(val);
+}
+void dlg_return_ival(int index, int val)
+{
+    cfg[index].ival = val;
+}
+
+/*
+ * Called when the user clicks OK or Cancel. use_results will be TRUE
+ * or FALSE respectively, in those cases. We terminate the dialog box,
+ * unless the user selected an invalid combination of parameters.
+ */
+static void cfg_end(int use_results)
+{
+    if (use_results) {
+        /*
+         * User hit OK.
+         */
+        char *err = midend_set_config(me, cfg_which, cfg);
+
+        if (err) {
+            /*
+             * The settings were unacceptable, so leave the config box
+             * open for the user to adjust them and try again.
+             */
+            js_error_box(err);
+        } else {
+            /*
+             * New settings are fine; start a new game and close the
+             * dialog.
+             */
+            int preset = midend_which_preset(me);
+            js_select_preset(preset < 0 ? custom_preset : preset);
+
+            midend_new_game(me);
+            resize();
+            midend_redraw(me);
+            update_permalinks();
+            free_cfg(cfg);
+            js_dialog_cleanup();
+        }
+    } else {
+        /*
+         * User hit Cancel. Just close the dialog.
+         */
+        free_cfg(cfg);
+        js_dialog_cleanup();
+    }
+}
+
+/* ----------------------------------------------------------------------
+ * Called from JS when a command is given to the puzzle by clicking a
+ * button or control of some sort.
+ */
+void command(int n)
+{
+    switch (n) {
+      case 0:                          /* specific game ID */
+        cfg_start(CFG_DESC);
+        break;
+      case 1:                          /* random game seed */
+        cfg_start(CFG_SEED);
+        break;
+      case 2:                          /* game parameter dropdown changed */
+        {
+            int i = js_get_selected_preset();
+            if (i == custom_preset) {
+                /*
+                 * The user selected 'Custom', so launch the config
+                 * box.
+                 */
+                if (thegame.can_configure) /* (double-check just in case) */
+                    cfg_start(CFG_SETTINGS);
+            } else {
+                /*
+                 * The user selected a preset, so just switch straight
+                 * to that.
+                 */
+                midend_set_params(me, presets[i]);
+                midend_new_game(me);
+                update_permalinks();
+                resize();
+                midend_redraw(me);
+                update_undo_redo();
+                js_focus_canvas();
+            }
+        }
+        break;
+      case 3:                          /* OK clicked in a config box */
+        cfg_end(TRUE);
+        update_undo_redo();
+        break;
+      case 4:                          /* Cancel clicked in a config box */
+        cfg_end(FALSE);
+        update_undo_redo();
+        break;
+      case 5:                          /* New Game */
+        midend_process_key(me, 0, 0, 'n');
+        update_undo_redo();
+        js_focus_canvas();
+        break;
+      case 6:                          /* Restart */
+        midend_restart_game(me);
+        update_undo_redo();
+        js_focus_canvas();
+        break;
+      case 7:                          /* Undo */
+        midend_process_key(me, 0, 0, 'u');
+        update_undo_redo();
+        js_focus_canvas();
+        break;
+      case 8:                          /* Redo */
+        midend_process_key(me, 0, 0, 'r');
+        update_undo_redo();
+        js_focus_canvas();
+        break;
+      case 9:                          /* Solve */
+        if (thegame.can_solve) {
+            char *msg = midend_solve(me);
+            if (msg)
+                js_error_box(msg);
+        }
+        update_undo_redo();
+        js_focus_canvas();
+        break;
+    }
+}
+
+/* ----------------------------------------------------------------------
+ * Setup function called at page load time. It's called main() because
+ * that's the most convenient thing in Emscripten, but it's not main()
+ * in the usual sense of bounding the program's entire execution.
+ * Instead, this function returns once the initial puzzle is set up
+ * and working, and everything thereafter happens by means of JS event
+ * handlers sending us callbacks.
+ */
+int main(int argc, char **argv)
+{
+    char *param_err;
+    float *colours;
+    int i;
+
+    /*
+     * Instantiate a midend.
+     */
+    me = midend_new(NULL, &thegame, &js_drawing, NULL);
+
+    /*
+     * Chuck in the HTML fragment ID if we have one (trimming the
+     * leading # off the front first). If that's invalid, we retain
+     * the error message and will display it at the end, after setting
+     * up a random puzzle as usual.
+     */
+    if (argc > 1 && argv[1][0] == '#' && argv[1][1] != '\0')
+        param_err = midend_game_id(me, argv[1] + 1);
+    else
+        param_err = NULL;
+
+    /*
+     * Create either a random game or the specified one, and set the
+     * canvas size appropriately.
+     */
+    midend_new_game(me);
+    resize();
+
+    /*
+     * Create a status bar, if needed.
+     */
+    if (midend_wants_statusbar(me))
+        js_canvas_make_statusbar();
+
+    /*
+     * Set up the game-type dropdown with presets and/or the Custom
+     * option. We remember the index of the Custom option (as
+     * custom_preset) so that we can easily treat it specially when
+     * it's selected.
+     */
+    custom_preset = midend_num_presets(me);
+    presets = snewn(custom_preset, game_params *);
+    for (i = 0; i < custom_preset; i++) {
+        char *name;
+        midend_fetch_preset(me, i, &name, &presets[i]);
+        js_add_preset(name);
+    }
+    if (thegame.can_configure)
+        js_add_preset("Custom");
+    else if (custom_preset == 0)
+        js_remove_type_dropdown();
+
+    /*
+     * Remove the Solve button if the game doesn't support it.
+     */
+    if (!thegame.can_solve)
+        js_remove_solve_button();
+
+    /*
+     * Retrieve the game's colours, and convert them into #abcdef type
+     * hex ID strings.
+     */
+    colours = midend_colours(me, &ncolours);
+    colour_strings = snewn(ncolours, char *);
+    for (i = 0; i < ncolours; i++) {
+        char col[40];
+        sprintf(col, "#%02x%02x%02x",
+                (unsigned)(0.5 + 255 * colours[i*3+0]),
+                (unsigned)(0.5 + 255 * colours[i*3+1]),
+                (unsigned)(0.5 + 255 * colours[i*3+2]));
+        colour_strings[i] = dupstr(col);
+    }
+
+    /*
+     * Draw the puzzle's initial state, and set up the permalinks and
+     * undo/redo greying out.
+     */
+    midend_redraw(me);
+    update_permalinks();
+    update_undo_redo();
+
+    /*
+     * If we were given an erroneous game ID in argv[1], now's the
+     * time to put up the error box about it, after we've fully set up
+     * a random puzzle. Then when the user clicks 'ok', we have a
+     * puzzle for them.
+     */
+    if (param_err)
+        js_error_box(param_err);
+
+    /*
+     * Done. Return to JS, and await callbacks!
+     */
+    return 0;
+}
diff --git a/emcclib.js b/emcclib.js
new file mode 100644 (file)
index 0000000..51c8f93
--- /dev/null
@@ -0,0 +1,698 @@
+/*
+ * emcclib.js: one of the Javascript components of an Emscripten-based
+ * web/Javascript front end for Puzzles.
+ *
+ * The other parts of this system live in emcc.c and emccpre.js.
+ *
+ * This file contains a set of Javascript functions which we insert
+ * into Emscripten's library object via the --js-library option; this
+ * allows us to provide JS code which can be called from the
+ * Emscripten-compiled C, mostly dealing with UI interaction of
+ * various kinds.
+ */
+
+mergeInto(LibraryManager.library, {
+    /*
+     * void js_debug(const char *message);
+     *
+     * A function to write a diagnostic to the Javascript console.
+     * Unused in production, but handy in development.
+     */
+    js_debug: function(ptr) {
+        console.log(Pointer_stringify(ptr));
+    },
+
+    /*
+     * void js_error_box(const char *message);
+     *
+     * A wrapper around Javascript's alert(), so the C code can print
+     * simple error message boxes (e.g. when invalid data is entered
+     * in a configuration dialog).
+     */
+    js_error_box: function(ptr) {
+        alert(Pointer_stringify(ptr));
+    },
+
+    /*
+     * void js_remove_type_dropdown(void);
+     *
+     * Get rid of the drop-down list on the web page for selecting
+     * game presets. Called at setup time if the game back end
+     * provides neither presets nor configurability.
+     */
+    js_remove_type_dropdown: function() {
+        document.getElementById("gametype").style.display = "none";
+    },
+
+    /*
+     * void js_remove_solve_button(void);
+     *
+     * Get rid of the Solve button on the web page. Called at setup
+     * time if the game doesn't support an in-game solve function.
+     */
+    js_remove_solve_button: function() {
+        document.getElementById("solve").style.display = "none";
+    },
+
+    /*
+     * void js_add_preset(const char *name);
+     *
+     * Add a preset to the drop-down types menu. The provided text is
+     * the name of the preset. (The corresponding game_params stays on
+     * the C side and never comes out this far; we just pass a numeric
+     * index back to the C code when a selection is made.)
+     */
+    js_add_preset: function(ptr) {
+        var option = document.createElement("option");
+        option.value = Pointer_stringify(ptr);
+        option.innerHTML = Pointer_stringify(ptr);
+        gametypeselector.appendChild(option);
+        gametypeoptions.push(option);
+    },
+
+    /*
+     * int js_get_selected_preset(void);
+     *
+     * Return the index of the currently selected value in the type
+     * dropdown.
+     */
+    js_get_selected_preset: function() {
+        var val = 0;
+        for (var i in gametypeoptions) {
+            if (gametypeoptions[i].selected) {
+                val = i;
+                break;
+            }
+        }
+        return val;
+    },
+
+    /*
+     * void js_select_preset(int n);
+     *
+     * Cause a different value to be selected in the type dropdown
+     * (for when the user selects values from the Custom configurer
+     * which turn out to exactly match a preset).
+     */
+    js_select_preset: function(n) {
+        gametypeoptions[n].selected = true;
+    },
+
+    /*
+     * void js_get_date_64(unsigned *p);
+     *
+     * Return the current date, in milliseconds since the epoch
+     * (Javascript's native format), as a 64-bit integer. Used to
+     * invent an initial random seed for puzzle generation.
+     */
+    js_get_date_64: function(ptr) {
+        var d = (new Date()).valueOf();
+        setValue(ptr, d, 'i64');
+    },
+
+    /*
+     * void js_update_permalinks(const char *desc, const char *seed);
+     *
+     * Update the permalinks on the web page for a new game
+     * description and optional random seed. desc can never be NULL,
+     * but seed might be (if the game was generated by entering a
+     * descriptive id by hand), in which case we suppress display of
+     * the random seed permalink.
+     */
+    js_update_permalinks: function(desc, seed) {
+        desc = Pointer_stringify(desc);
+        permalink_desc.href = "#" + desc;
+
+        if (seed == 0) {
+            permalink_seed.style.display = "none";
+        } else {
+            seed = Pointer_stringify(seed);
+            permalink_seed.href = "#" + seed;
+            permalink_seed.style.display = "inline";
+        }
+    },
+
+    /*
+     * void js_enable_undo_redo(int undo, int redo);
+     *
+     * Set the enabled/disabled states of the undo and redo buttons,
+     * after a move.
+     */
+    js_enable_undo_redo: function(undo, redo) {
+        undo_button.disabled = (undo == 0);
+        redo_button.disabled = (redo == 0);
+    },
+
+    /*
+     * void js_activate_timer();
+     *
+     * Start calling the C timer_callback() function every 20ms.
+     */
+    js_activate_timer: function() {
+        if (timer === null) {
+            timer_reference_date = (new Date()).valueOf();
+            timer = setInterval(function() {
+                var now = (new Date()).valueOf();
+                timer_callback((now - timer_reference_date) / 1000.0);
+                timer_reference_date = now;
+                return true;
+            }, 20);
+        }
+    },
+
+    /*
+     * void js_deactivate_timer();
+     *
+     * Stop calling the C timer_callback() function every 20ms.
+     */
+    js_deactivate_timer: function() {
+        if (timer !== null) {
+            clearInterval(timer);
+            timer = null;
+        }
+    },
+
+    /*
+     * void js_canvas_start_draw(void);
+     *
+     * Prepare to do some drawing on the canvas.
+     */
+    js_canvas_start_draw: function() {
+        ctx = offscreen_canvas.getContext('2d');
+        update_xmin = update_xmax = update_ymin = update_ymax = undefined;
+    },
+
+    /*
+     * void js_canvas_draw_update(int x, int y, int w, int h);
+     *
+     * Mark a rectangle of the off-screen canvas as needing to be
+     * copied to the on-screen one.
+     */
+    js_canvas_draw_update: function(x, y, w, h) {
+        /*
+         * Currently we do this in a really simple way, just by taking
+         * the smallest rectangle containing all updates so far. We
+         * could instead keep the data in a richer form (e.g. retain
+         * multiple smaller rectangles needing update, and only redraw
+         * the whole thing beyond a certain threshold) but this will
+         * do for now.
+         */
+        if (update_xmin === undefined || update_xmin > x) update_xmin = x;
+        if (update_ymin === undefined || update_ymin > y) update_ymin = y;
+        if (update_xmax === undefined || update_xmax < x+w) update_xmax = x+w;
+        if (update_ymax === undefined || update_ymax < y+h) update_ymax = y+h;
+    },
+
+    /*
+     * void js_canvas_end_draw(void);
+     *
+     * Finish the drawing, by actually copying the newly drawn stuff
+     * to the on-screen canvas.
+     */
+    js_canvas_end_draw: function() {
+        if (update_xmin !== undefined) {
+            var onscreen_ctx = onscreen_canvas.getContext('2d');
+            onscreen_ctx.drawImage(offscreen_canvas,
+                                   update_xmin, update_ymin,
+                                   update_xmax - update_xmin,
+                                   update_ymax - update_ymin,
+                                   update_xmin, update_ymin,
+                                   update_xmax - update_xmin,
+                                   update_ymax - update_ymin);
+        }
+        ctx = null;
+    },
+
+    /*
+     * void js_canvas_draw_rect(int x, int y, int w, int h,
+     *                          const char *colour);
+     * 
+     * Draw a rectangle.
+     */
+    js_canvas_draw_rect: function(x, y, w, h, colptr) {
+        ctx.fillStyle = Pointer_stringify(colptr);
+        ctx.fillRect(x, y, w, h);
+    },
+
+    /*
+     * void js_canvas_clip_rect(int x, int y, int w, int h);
+     * 
+     * Set a clipping rectangle.
+     */
+    js_canvas_clip_rect: function(x, y, w, h) {
+        ctx.save();
+        ctx.beginPath();
+        ctx.rect(x, y, w, h);
+        ctx.clip();
+    },
+
+    /*
+     * void js_canvas_unclip(void);
+     * 
+     * Reset to no clipping.
+     */
+    js_canvas_unclip: function() {
+        ctx.restore();
+    },
+
+    /*
+     * void js_canvas_draw_line(float x1, float y1, float x2, float y2,
+     *                          int width, const char *colour);
+     * 
+     * Draw a line. We must adjust the coordinates by 0.5 because
+     * Javascript's canvas coordinates appear to be pixel corners,
+     * whereas we want pixel centres. Also, we manually draw the pixel
+     * at each end of the line, which our clients will expect but
+     * Javascript won't reliably do by default (in common with other
+     * Postscriptish drawing frameworks).
+     */
+    js_canvas_draw_line: function(x1, y1, x2, y2, width, colour) {
+        colour = Pointer_stringify(colour);
+
+        ctx.beginPath();
+        ctx.moveTo(x1 + 0.5, y1 + 0.5);
+        ctx.lineTo(x2 + 0.5, y2 + 0.5);
+        ctx.lineWidth = width;
+        ctx.lineCap = '1';
+        ctx.lineJoin = '1';
+        ctx.strokeStyle = colour;
+        ctx.stroke();
+        ctx.fillStyle = colour;
+        ctx.fillRect(x1, y1, 1, 1);
+        ctx.fillRect(x2, y2, 1, 1);
+    },
+
+    /*
+     * void js_canvas_draw_poly(int *points, int npoints,
+     *                          const char *fillcolour,
+     *                          const char *outlinecolour);
+     * 
+     * Draw a polygon.
+     */
+    js_canvas_draw_poly: function(pointptr, npoints, fill, outline) {
+        ctx.beginPath();
+        ctx.moveTo(getValue(pointptr  , 'i32') + 0.5,
+                   getValue(pointptr+4, 'i32') + 0.5);
+        for (var i = 1; i < npoints; i++)
+            ctx.lineTo(getValue(pointptr+8*i  , 'i32') + 0.5,
+                       getValue(pointptr+8*i+4, 'i32') + 0.5);
+        ctx.closePath();
+        if (fill != 0) {
+            ctx.fillStyle = Pointer_stringify(fill);
+            ctx.fill();
+        }
+        ctx.lineWidth = '1';
+        ctx.lineCap = '1';
+        ctx.lineJoin = '1';
+        ctx.strokeStyle = Pointer_stringify(outline);
+        ctx.stroke();
+    },
+
+    /*
+     * void js_canvas_draw_circle(int x, int y, int r,
+     *                            const char *fillcolour,
+     *                            const char *outlinecolour);
+     * 
+     * Draw a circle.
+     */
+    js_canvas_draw_circle: function(x, y, r, fill, outline) {
+        ctx.beginPath();
+        ctx.arc(x + 0.5, y + 0.5, r, 0, 2*Math.PI);
+        if (fill != 0) {
+            ctx.fillStyle = Pointer_stringify(fill);
+            ctx.fill();
+        }
+        ctx.lineWidth = '1';
+        ctx.lineCap = '1';
+        ctx.lineJoin = '1';
+        ctx.strokeStyle = Pointer_stringify(outline);
+        ctx.stroke();
+    },
+
+    /*
+     * int js_canvas_find_font_midpoint(int height, const char *fontptr);
+     * 
+     * Return the adjustment required for text displayed using
+     * ALIGN_VCENTRE. We want to place the midpoint between the
+     * baseline and the cap-height at the specified position; so this
+     * function returns the adjustment which, when added to the
+     * desired centre point, returns the y-coordinate at which you
+     * should put the baseline.
+     *
+     * There is no sensible method of querying this kind of font
+     * metric in Javascript, so instead we render a piece of test text
+     * to a throwaway offscreen canvas and then read the pixel data
+     * back out to find the highest and lowest pixels. That's good
+     * _enough_ (in that we only needed the answer to the nearest
+     * pixel anyway), but rather disgusting!
+     *
+     * Since this is a very expensive operation, we cache the results
+     * per (font,height) pair.
+     */
+    js_canvas_find_font_midpoint: function(height, font) {
+        font = Pointer_stringify(font);
+
+        // Reuse cached value if possible
+        if (midpoint_cache[font] !== undefined)
+            return midpoint_cache[font];
+
+        // Find the width of the string
+        var ctx1 = onscreen_canvas.getContext('2d');
+        ctx1.font = font;
+        var width = ctx1.measureText(midpoint_test_str).width;
+
+        // Construct a test canvas of appropriate size, initialise it to
+        // black, and draw the string on it in white
+        var measure_canvas = document.createElement('canvas');
+        var ctx2 = measure_canvas.getContext('2d');
+        ctx2.canvas.width = width;
+        ctx2.canvas.height = 2*height;
+        ctx2.fillStyle = "#000000";
+        ctx2.fillRect(0, 0, width, 2*height);
+        var baseline = (1.5*height) | 0;
+        ctx2.fillStyle = "#ffffff";
+        ctx2.font = font;
+        ctx2.fillText(midpoint_test_str, 0, baseline);
+
+        // Scan the contents of the test canvas to find the top and bottom
+        // set pixels.
+        var pixels = ctx2.getImageData(0, 0, width, 2*height).data;
+        var ymin = 2*height, ymax = -1;
+        for (var y = 0; y < 2*height; y++) {
+            for (var x = 0; x < width; x++) {
+                if (pixels[4*(y*width+x)] != 0) {
+                    if (ymin > y) ymin = y;
+                    if (ymax < y) ymax = y;
+                    break;
+                }
+            }
+        }
+
+        var ret = (baseline - (ymin + ymax) / 2) | 0;
+        midpoint_cache[font] = ret;
+        return ret;
+    },
+
+    /*
+     * void js_canvas_draw_text(int x, int y, int halign,
+     *                          const char *colptr, const char *fontptr,
+     *                          const char *text);
+     * 
+     * Draw text. Vertical alignment has been taken care of on the C
+     * side, by optionally calling the above function. Horizontal
+     * alignment is handled here, since we can get the canvas draw
+     * function to do it for us with almost no extra effort.
+     */
+    js_canvas_draw_text: function(x, y, halign, colptr, fontptr, text) {
+        ctx.font = Pointer_stringify(fontptr);
+        ctx.fillStyle = Pointer_stringify(colptr);
+        ctx.textAlign = (halign == 0 ? 'left' :
+                         halign == 1 ? 'center' : 'right');
+        ctx.textBaseline = 'alphabetic';
+        ctx.fillText(Pointer_stringify(text), x, y);
+    },
+
+    /*
+     * int js_canvas_new_blitter(int w, int h);
+     * 
+     * Create a new blitter object, which is just an offscreen canvas
+     * of the specified size.
+     */
+    js_canvas_new_blitter: function(w, h) {
+        var id = blittercount++;
+        blitters[id] = document.createElement("canvas");
+        blitters[id].width = w;
+        blitters[id].height = h;
+    },
+
+    /*
+     * void js_canvas_free_blitter(int id);
+     * 
+     * Free a blitter (or rather, destroy our reference to it so JS
+     * can garbage-collect it, and also enforce that we don't
+     * accidentally use it again afterwards).
+     */
+    js_canvas_free_blitter: function(id) {
+        blitters[id] = null;
+    },
+
+    /*
+     * void js_canvas_copy_to_blitter(int id, int x, int y, int w, int h);
+     * 
+     * Copy from the puzzle image to a blitter. The size is passed to
+     * us, partly so we don't have to remember the size of each
+     * blitter, but mostly so that the C side can adjust the copy
+     * rectangle in the case where it partially overlaps the edge of
+     * the screen.
+     */
+    js_canvas_copy_to_blitter: function(id, x, y, w, h) {
+        var blitter_ctx = blitters[id].getContext('2d');
+        blitter_ctx.drawImage(offscreen_canvas,
+                              x, y, w, h,
+                              0, 0, w, h);
+    },
+
+    /*
+     * void js_canvas_copy_from_blitter(int id, int x, int y, int w, int h);
+     * 
+     * Copy from a blitter back to the puzzle image. As above, the
+     * size of the copied rectangle is passed to us from the C side
+     * and may already have been modified.
+     */
+    js_canvas_copy_from_blitter: function(id, x, y, w, h) {
+        ctx.drawImage(blitters[id],
+                      0, 0, w, h,
+                      x, y, w, h);
+    },
+
+    /*
+     * void js_canvas_make_statusbar(void);
+     * 
+     * Cause a status bar to exist. Called at setup time if the puzzle
+     * back end turns out to want one.
+     */
+    js_canvas_make_statusbar: function() {
+        var statustd = document.getElementById("statusbarholder");
+        statusbar = document.createElement("div");
+        statusbar.style.overflow = "hidden";
+        statusbar.style.width = onscreen_canvas.width - 4;
+        statusbar.style.height = "1.2em";
+        statusbar.style.background = "#d8d8d8";
+        statusbar.style.borderLeft = '2px solid #c8c8c8';
+        statusbar.style.borderTop = '2px solid #c8c8c8';
+        statusbar.style.borderRight = '2px solid #e8e8e8';
+        statusbar.style.borderBottom = '2px solid #e8e8e8';
+        statusbar.appendChild(document.createTextNode(" "));
+        statustd.appendChild(statusbar);
+    },
+
+    /*
+     * void js_canvas_set_statusbar(const char *text);
+     * 
+     * Set the text in the status bar.
+     */
+    js_canvas_set_statusbar: function(ptr) {
+        var text = Pointer_stringify(ptr);
+        statusbar.replaceChild(document.createTextNode(text),
+                               statusbar.lastChild);
+    },
+
+    /*
+     * void js_canvas_set_size(int w, int h);
+     * 
+     * Set the size of the puzzle canvas. Called at setup, and every
+     * time the user picks new puzzle settings requiring a different
+     * size.
+     */
+    js_canvas_set_size: function(w, h) {
+        onscreen_canvas.width = w;
+        offscreen_canvas.width = w;
+        if (statusbar !== null)
+            statusbar.style.width = w - 4;
+
+        onscreen_canvas.height = h;
+        offscreen_canvas.height = h;
+    },
+
+    /*
+     * void js_dialog_init(const char *title);
+     * 
+     * Begin constructing a 'dialog box' which will be popped up in an
+     * overlay on top of the rest of the puzzle web page.
+     */
+    js_dialog_init: function(titletext) {
+        // Create an overlay on the page which darkens everything
+        // beneath it.
+        dlg_dimmer = document.createElement("div");
+        dlg_dimmer.style.width = "100%";
+        dlg_dimmer.style.height = "100%";
+        dlg_dimmer.style.background = '#000000';
+        dlg_dimmer.style.position = 'fixed';
+        dlg_dimmer.style.opacity = 0.3;
+        dlg_dimmer.style.top = dlg_dimmer.style.left = 0;
+        dlg_dimmer.style["z-index"] = 99;
+
+        // Now create a form which sits on top of that in turn.
+        dlg_form = document.createElement("form");
+        dlg_form.style.width =  window.innerWidth * 2 / 3;
+        dlg_form.style.opacity = 1;
+        dlg_form.style.background = '#ffffff';
+        dlg_form.style.color = '#000000';
+        dlg_form.style.position = 'absolute';
+        dlg_form.style.border = "2px solid black";
+        dlg_form.style.padding = 20;
+        dlg_form.style.top = window.innerHeight / 10;
+        dlg_form.style.left = window.innerWidth / 6;
+        dlg_form.style["z-index"] = 100;
+
+        var title = document.createElement("p");
+        title.style.marginTop = "0px";
+        title.appendChild(document.createTextNode
+                          (Pointer_stringify(titletext)));
+        dlg_form.appendChild(title);
+
+        dlg_return_funcs = [];
+        dlg_next_id = 0;
+    },
+
+    /*
+     * void js_dialog_string(int i, const char *title, const char *initvalue);
+     * 
+     * Add a string control (that is, an edit box) to the dialog under
+     * construction.
+     */
+    js_dialog_string: function(index, title, initialtext) {
+        dlg_form.appendChild(document.createTextNode(Pointer_stringify(title)));
+        var editbox = document.createElement("input");
+        editbox.type = "text";
+        editbox.value = Pointer_stringify(initialtext);
+        dlg_form.appendChild(editbox);
+        dlg_form.appendChild(document.createElement("br"));
+
+        dlg_return_funcs.push(function() {
+            dlg_return_sval(index, editbox.value);
+        });
+    },
+
+    /*
+     * void js_dialog_choices(int i, const char *title, const char *choicelist,
+     *                        int initvalue);
+     * 
+     * Add a choices control (i.e. a drop-down list) to the dialog
+     * under construction. The 'choicelist' parameter is unchanged
+     * from the way the puzzle back end will have supplied it: i.e.
+     * it's still encoded as a single string whose first character
+     * gives the separator.
+     */
+    js_dialog_choices: function(index, title, choicelist, initvalue) {
+        dlg_form.appendChild(document.createTextNode(Pointer_stringify(title)));
+        var dropdown = document.createElement("select");
+        var choicestr = Pointer_stringify(choicelist);
+        var items = choicestr.slice(1).split(choicestr[0]);
+        var options = [];
+        for (var i in items) {
+            var option = document.createElement("option");
+            option.value = items[i];
+            option.innerHTML = items[i];
+            if (i == initvalue) option.selected = true;
+            dropdown.appendChild(option);
+            options.push(option);
+        }
+        dlg_form.appendChild(dropdown);
+        dlg_form.appendChild(document.createElement("br"));
+
+        dlg_return_funcs.push(function() {
+            var val = 0;
+            for (var i in options) {
+                if (options[i].selected) {
+                    val = i;
+                    break;
+                }
+            }
+            dlg_return_ival(index, val);
+        });
+    },
+
+    /*
+     * void js_dialog_boolean(int i, const char *title, int initvalue);
+     * 
+     * Add a boolean control (a checkbox) to the dialog under
+     * construction. Checkboxes are generally expected to be sensitive
+     * on their label text as well as the box itself, so for this
+     * control we create an actual label rather than merely a text
+     * node (and hence we must allocate an id to the checkbox so that
+     * the label can refer to it).
+     */
+    js_dialog_boolean: function(index, title, initvalue) {
+        var checkbox = document.createElement("input");
+        checkbox.type = "checkbox";
+        checkbox.id = "cb" + String(dlg_next_id++);
+        checkbox.checked = (initvalue != 0);
+        dlg_form.appendChild(checkbox);
+        var checkboxlabel = document.createElement("label");
+        checkboxlabel.setAttribute("for", checkbox.id);
+        checkboxlabel.textContent = Pointer_stringify(title);
+        dlg_form.appendChild(checkboxlabel);
+        dlg_form.appendChild(document.createElement("br"));
+
+        dlg_return_funcs.push(function() {
+            dlg_return_ival(index, checkbox.checked ? 1 : 0);
+        });
+    },
+
+    /*
+     * void js_dialog_launch(void);
+     * 
+     * Finish constructing a dialog, and actually display it, dimming
+     * everything else on the page.
+     */
+    js_dialog_launch: function() {
+        // Put in the OK and Cancel buttons at the bottom.
+        var button;
+
+        button = document.createElement("input");
+        button.type = "button";
+        button.value = "OK";
+        button.onclick = function(event) {
+            for (var i in dlg_return_funcs)
+                dlg_return_funcs[i]();
+            command(3);
+        }
+        dlg_form.appendChild(button);
+
+        button = document.createElement("input");
+        button.type = "button";
+        button.value = "Cancel";
+        button.onclick = function(event) {
+            command(4);
+        }
+        dlg_form.appendChild(button);
+
+        document.body.appendChild(dlg_dimmer);
+        document.body.appendChild(dlg_form);
+    },
+
+    /*
+     * void js_dialog_cleanup(void);
+     * 
+     * Stop displaying a dialog, and clean up the internal state
+     * associated with it.
+     */
+    js_dialog_cleanup: function() {
+        document.body.removeChild(dlg_dimmer);
+        document.body.removeChild(dlg_form);
+        dlg_dimmer = dlg_form = null;
+        onscreen_canvas.focus();
+    },
+
+    /*
+     * void js_focus_canvas(void);
+     * 
+     * Return keyboard focus to the puzzle canvas. Called after a
+     * puzzle-control button is pressed, which tends to have the side
+     * effect of taking focus away from the canvas.
+     */
+    js_focus_canvas: function() {
+        onscreen_canvas.focus();
+    },
+});
diff --git a/emccpre.js b/emccpre.js
new file mode 100644 (file)
index 0000000..548e2a5
--- /dev/null
@@ -0,0 +1,258 @@
+/*
+ * emccpre.js: one of the Javascript components of an Emscripten-based
+ * web/Javascript front end for Puzzles.
+ *
+ * The other parts of this system live in emcc.c and emcclib.js.
+ *
+ * This file contains the Javascript code which is prefixed unmodified
+ * to Emscripten's output via the --pre-js option. It declares all our
+ * global variables, and provides the puzzle init function and a
+ * couple of other helper functions.
+ */
+
+// To avoid flicker while doing complicated drawing, we use two
+// canvases, the same size. One is actually on the web page, and the
+// other is off-screen. We do all our drawing on the off-screen one
+// first, and then copy rectangles of it to the on-screen canvas in
+// response to draw_update() calls by the game backend.
+var onscreen_canvas, offscreen_canvas;
+
+// A persistent drawing context for the offscreen canvas, to save
+// constructing one per individual graphics operation.
+var ctx;
+
+// Bounding rectangle for the copy to the onscreen canvas that will be
+// done at drawing end time. Updated by js_canvas_draw_update and used
+// by js_canvas_end_draw.
+var update_xmin, update_xmax, update_ymin, update_ymax;
+
+// Module object for Emscripten. We fill in these parameters to ensure
+// that Module.run() won't be called until we're ready (we want to do
+// our own init stuff first), and that when main() returns nothing
+// will get cleaned up so we remain able to call the puzzle's various
+// callbacks.
+var Module = {
+    'noInitialRun': true,
+    'noExitRuntime': true,
+};
+
+// Variables used by js_canvas_find_font_midpoint().
+var midpoint_test_str = "ABCDEFGHIKLMNOPRSTUVWXYZ0123456789";
+var midpoint_cache = [];
+
+// Variables used by js_activate_timer() and js_deactivate_timer().
+var timer = null;
+var timer_reference_date;
+
+// void timer_callback(double tplus);
+//
+// Called every 20ms while timing is active.
+var timer_callback;
+
+// The status bar object, if we create one.
+var statusbar = null;
+
+// Currently live blitters. We keep an integer id for each one on the
+// JS side; the C side, which expects a blitter to look like a struct,
+// simply defines the struct to contain that integer id.
+var blittercount = 0;
+var blitters = [];
+
+// State for the dialog-box mechanism. dlg_dimmer and dlg_form are the
+// page-darkening overlay and the actual dialog box respectively;
+// dlg_next_id is used to allocate each checkbox a unique id to use
+// for linking its label to it (see js_dialog_boolean);
+// dlg_return_funcs is a list of JS functions to be called when the OK
+// button is pressed, to pass the results back to C.
+var dlg_dimmer = null, dlg_form = null;
+var dlg_next_id = 0;
+var dlg_return_funcs = null;
+
+// void dlg_return_sval(int index, const char *val);
+// void dlg_return_ival(int index, int val);
+//
+// C-side entry points called by functions in dlg_return_funcs, to
+// pass back the final value in each dialog control.
+var dlg_return_sval, dlg_return_ival;
+
+// The <select> object implementing the game-type drop-down, and a
+// list of the <option> objects inside it. Used by js_add_preset(),
+// js_get_selected_preset() and js_select_preset().
+var gametypeselector = null, gametypeoptions = [];
+
+// The two anchors used to give permalinks to the current puzzle. Used
+// by js_update_permalinks().
+var permalink_seed, permalink_desc;
+
+// The undo and redo buttons. Used by js_enable_undo_redo().
+var undo_button, redo_button;
+
+// Helper function which is passed a mouse event object and a DOM
+// element, and returns the coordinates of the mouse event relative to
+// the top left corner of the element by iterating upwards through the
+// DOM finding each element's offset from its parent, and thus
+// calculating the page-relative position of the target element so
+// that we can subtract that from event.page{X,Y}.
+function relative_mouse_coords(event, element) {
+    var ex = 0, ey = 0;
+    while (element.offsetParent) {
+        ex += element.offsetLeft;
+        ey += element.offsetTop;
+        element = element.offsetParent;
+    }
+    return {x: event.pageX - ex,
+            y: event.pageY - ey};
+}
+
+// Init function called from body.onload.
+function initPuzzle() {
+    // Construct the off-screen canvas used for double buffering.
+    onscreen_canvas = document.getElementById("puzzlecanvas");
+    offscreen_canvas = document.createElement("canvas");
+    offscreen_canvas.width = onscreen_canvas.width;
+    offscreen_canvas.height = onscreen_canvas.height;
+
+    // Stop right-clicks on the puzzle from popping up a context menu.
+    // We need those right-clicks!
+    onscreen_canvas.oncontextmenu = function(event) { return false; }
+
+    // Set up mouse handlers. We do a bit of tracking of the currently
+    // pressed mouse buttons, to avoid sending mousemoves with no
+    // button down (our puzzles don't want those events).
+    mousedown = Module.cwrap('mousedown', 'void',
+                             ['number', 'number', 'number']);
+    buttons_down = 0;
+    onscreen_canvas.onmousedown = function(event) {
+        var xy = relative_mouse_coords(event, onscreen_canvas);
+        mousedown(xy.x - onscreen_canvas.offsetLeft,
+                  xy.y - onscreen_canvas.offsetTop,
+                  event.button);
+        buttons_down |= 1 << event.button;
+        onscreen_canvas.setCapture(true);
+    };
+    mousemove = Module.cwrap('mousemove', 'void',
+                             ['number', 'number', 'number']);
+    onscreen_canvas.onmousemove = function(event) {
+        if (buttons_down) {
+            var xy = relative_mouse_coords(event, onscreen_canvas);
+            mousemove(xy.x - onscreen_canvas.offsetLeft,
+                      xy.y - onscreen_canvas.offsetTop,
+                      buttons_down);
+        }
+    };
+    mouseup = Module.cwrap('mouseup', 'void',
+                           ['number', 'number', 'number']);
+    onscreen_canvas.onmouseup = function(event) {
+        if (buttons_down & (1 << event.button)) {
+            buttons_down ^= 1 << event.button;
+            var xy = relative_mouse_coords(event, onscreen_canvas);
+            mouseup(xy.x - onscreen_canvas.offsetLeft,
+                    xy.y - onscreen_canvas.offsetTop,
+                    event.button);
+        }
+    };
+
+    // Set up keyboard handlers. We expect ordinary keys (with a
+    // charCode) to be handled by onkeypress, but function keys
+    // (arrows etc) to be handled by onkeydown.
+    //
+    // We also call event.preventDefault() in both handlers. This
+    // means that while the canvas itself has focus, _all_ keypresses
+    // go only to the puzzle - so users of this puzzle collection in
+    // other media can indulge their instinct to press ^R for redo,
+    // for example, without accidentally reloading the page.
+    key = Module.cwrap('key', 'void',
+                       ['number', 'number', 'number', 'number']);
+    onscreen_canvas.onkeydown = function(event) {
+        key(event.keyCode, event.charCode,
+            event.shiftKey ? 1 : 0, event.ctrlKey ? 1 : 0);
+        event.preventDefault();
+    };
+    onscreen_canvas.onkeypress = function(event) {
+        if (event.charCode != 0)
+            key(event.keyCode, event.charCode,
+                event.shiftKey ? 1 : 0, event.ctrlKey ? 1 : 0);
+        event.preventDefault();
+    };
+
+    // command() is a C function called to pass back events which
+    // don't fall into other categories like mouse and key events.
+    // Mostly those are button presses, but there's also one for the
+    // game-type dropdown having been changed.
+    command = Module.cwrap('command', 'void', ['number']);
+
+    // Event handlers for buttons and things, which call command().
+    document.getElementById("specific").onclick = function(event) {
+        // Ensure we don't accidentally process these events when a
+        // dialog is actually active, e.g. because the button still
+        // has keyboard focus
+        if (dlg_dimmer === null)
+            command(0);
+    };
+    document.getElementById("random").onclick = function(event) {
+        if (dlg_dimmer === null)
+            command(1);
+    };
+    document.getElementById("new").onclick = function(event) {
+        if (dlg_dimmer === null)
+            command(5);
+    };
+    document.getElementById("restart").onclick = function(event) {
+        if (dlg_dimmer === null)
+            command(6);
+    };
+    undo_button = document.getElementById("undo");
+    undo_button.onclick = function(event) {
+        if (dlg_dimmer === null)
+            command(7);
+    };
+    redo_button = document.getElementById("redo");
+    redo_button.onclick = function(event) {
+        if (dlg_dimmer === null)
+            command(8);
+    };
+    document.getElementById("solve").onclick = function(event) {
+        if (dlg_dimmer === null)
+            command(9);
+    };
+
+    gametypeselector = document.getElementById("gametype");
+    gametypeselector.onchange = function(event) {
+        if (dlg_dimmer === null)
+            command(2);
+    };
+
+    // In our dialog boxes, Return and Escape should be like pressing
+    // OK and Cancel respectively
+    document.addEventListener("keydown", function(event) {
+
+        if (dlg_dimmer !== null && event.keyCode == 13) {
+            for (var i in dlg_return_funcs)
+                dlg_return_funcs[i]();
+            command(3);
+        }
+
+        if (dlg_dimmer !== null && event.keyCode == 27)
+            command(4);
+    });
+
+    // Set up the function pointers we haven't already grabbed. 
+    dlg_return_sval = Module.cwrap('dlg_return_sval', 'void',
+                                   ['number','string']);
+    dlg_return_ival = Module.cwrap('dlg_return_ival', 'void',
+                                   ['number','number']);
+    timer_callback = Module.cwrap('timer_callback', 'void', ['number']);
+
+    // Save references to the two permalinks.
+    permalink_desc = document.getElementById("permalink-desc");
+    permalink_seed = document.getElementById("permalink-seed");
+
+    // Default to giving keyboard focus to the puzzle.
+    onscreen_canvas.focus();
+
+    // And run the C setup function, passing argv[1] as the fragment
+    // identifier (so that permalinks of the form puzzle.html#game-id
+    // can launch the specified id).
+    Module.arguments = [location.hash];
+    Module.run();
+}
diff --git a/emccx.json b/emccx.json
new file mode 100644 (file)
index 0000000..31d7234
--- /dev/null
@@ -0,0 +1,26 @@
+// -*- js -*-
+//
+// List of entry points exported by the C side of the Emscripten
+// puzzle builds. Passed in to emcc via the option '-s
+// EXPORTED_FUNCTIONS=[list]'.
+//
+// This file isn't actually a valid list in its current state, since
+// emcc doesn't like comments or newlines. However, it's a nicer
+// source form to keep the comments and newlines in, so we sed them
+// away at compile time.
+[
+    // Event handlers for mouse and keyboard input
+    '_mouseup',
+    '_mousedown',
+    '_mousemove',
+    '_key',
+    // Callback when the program activates timing
+    '_timer_callback',
+    // Callback from button presses in the UI outside the canvas
+    '_command',
+    // Callbacks to return values from dialog boxes
+    '_dlg_return_sval',
+    '_dlg_return_ival',
+    // Main program, run at initialisation time
+    '_main'
+]
diff --git a/html/jspage.pl b/html/jspage.pl
new file mode 100644 (file)
index 0000000..7172800
--- /dev/null
@@ -0,0 +1,113 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+open my $footerfile, "<", shift @ARGV or die "footer: open: $!\n";
+my $footer = "";
+$footer .= $_ while <$footerfile>;
+close $footerfile;
+
+for my $arg (@ARGV) {
+    $arg =~ /(.*\/)?([^\/]+)\.html$/ or die;
+    my $filename = $2;
+    open my $gamefile, "<", $arg or die "$arg: open: $!\n";
+    my $unfinished = 0;
+    my $docname = $filename;
+    chomp(my $puzzlename = <$gamefile>);
+    while ($puzzlename =~ s/^([^:=]+)(=([^:]+))?://) {
+        if ($1 eq "unfinished") {
+            $unfinished = 1;
+        } elsif ($1 eq "docname") {
+            $docname = $3;
+        } else {
+            die "$arg: unknown keyword '$1'\n";
+        }
+    }
+    my $instructions = "";
+    $instructions .= $_ while <$gamefile>;
+    close $gamefile;
+
+    open my $outpage, ">", "${filename}.html";
+
+    my $unfinishedtitlefragment = $unfinished ? "an unfinished puzzle " : "";
+    my $unfinishedheading = $unfinished ? "<h2 align=center>an unfinished puzzle</h2>\n" : "";
+    my $unfinishedpara;
+    my $links;
+    if ($unfinished) {
+        $unfinishedpara = <<EOF;
+<p>
+You have found your way to a page containing an <em>unfinished</em>
+puzzle in my collection, not linked from the <a href="../">main
+puzzles page</a>. Don't be surprised if things are hard to understand
+or don't work as you expect.
+EOF
+        $links = <<EOF;
+<p align="center">
+<a href="../">Back to main puzzles page</a> (which does not link to this)
+EOF
+    } else {
+        $unfinishedpara = "";
+        $links = <<EOF;
+<p align="center">
+<a href="../doc/${docname}.html#${docname}">Full instructions</a>
+|
+<a href="../">Back to main puzzles page</a>
+EOF
+    }
+
+    print $outpage <<EOF;
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=ASCII" />
+<title>${puzzlename}, ${unfinishedtitlefragment}from Simon Tatham's Portable Puzzle Collection</title>
+<script type="text/javascript" src="${filename}.js"></script>
+</head>
+<body onLoad="initPuzzle();">
+<h1 align=center>${puzzlename}</h1>
+${unfinishedheading}
+<h2 align=center>from Simon Tatham's Portable Puzzle Collection</h2>
+
+${unfinishedpara}
+
+<hr>
+<p align=center>
+  <input type="button" id="new" value="New game">
+  <input type="button" id="restart" value="Restart game">
+  <input type="button" id="undo" value="Undo move">
+  <input type="button" id="redo" value="Redo move">
+  <input type="button" id="solve" value="Solve game">
+  <input type="button" id="specific" value="Enter game ID">
+  <input type="button" id="random" value="Enter random seed">
+  <select id="gametype"></select>
+</p>
+<p align=center>
+  <table cellpadding="0" cellspacing="0">
+    <tr>
+      <td>
+        <canvas id="puzzlecanvas" width="1" height="1" tabindex="1">
+      </td>
+    <tr>
+      <td id="statusbarholder">
+      </td>
+    </tr>
+  </table>
+</p>
+<p align=center>
+  Link to this puzzle:
+  <a id="permalink-desc">by game ID</a>
+  <a id="permalink-seed">by random seed</a>
+</p>
+<hr>
+
+${instructions}
+
+${links}
+
+${footer}
+</body>
+</html>
+EOF
+
+    close $outpage;
+}
index 625b9abd423b7f76fc7d55c43c6f1883be5478c4..9629a293fbbe23fa090bf0abdd340edd404182ca 100755 (executable)
@@ -287,7 +287,7 @@ sub mfval($) {
     # Returns true if the argument is a known makefile type. Otherwise,
     # prints a warning and returns false;
     if (grep { $type eq $_ }
-       ("vc","vcproj","cygwin","borland","lcc","gtk","mpw","nestedvm","osx","wce","gnustep")) {
+       ("vc","vcproj","cygwin","borland","lcc","gtk","mpw","nestedvm","osx","wce","gnustep","emcc")) {
            return 1;
        }
     warn "$.:unknown makefile type '$type'\n";
@@ -1589,3 +1589,59 @@ if (defined $makefiles{'gnustep'}) {
     "\trm -rf *.app\n";
     select STDOUT; close OUT;
 }
+
+if (defined $makefiles{'emcc'}) {
+    $mftyp = 'emcc';
+    $dirpfx = &dirpfx($makefiles{'emcc'}, "/");
+
+    ##-- Makefile for building Javascript puzzles via Emscripten
+
+    open OUT, ">$makefiles{'emcc'}"; select OUT;
+    print
+    "# Makefile for $project_name using Emscripten. Requires GNU make.\n".
+    "#\n# This file was created by `mkfiles.pl' from the `Recipe' file.\n".
+    "# DO NOT EDIT THIS FILE DIRECTLY; edit Recipe or mkfiles.pl instead.\n";
+    # emcc command line option is -D not /D
+    ($_ = $help) =~ s/=\/D/=-D/gs;
+    print $_;
+    print
+    "\n".
+    "# This can be set on the command line to point at the emcc command,\n".
+    "# if it is not on your PATH.\n".
+    "EMCC = emcc\n".
+    "\n".
+    &splitline("CFLAGS = -DSLOW_SYSTEM " .
+              (join " ", map {"-I$dirpfx$_"} @srcdirs))."\n".
+    "\n";
+    $output_js_files = join "", map { " \$(OUTPREFIX)$_.js" } &progrealnames("X");
+    print &splitline("all:" . $output_js_files);
+    print "\n\n";
+    foreach $p (&prognames("X")) {
+      ($prog, $type) = split ",", $p;
+      $objstr = &objects($p, "X.o", undef, undef);
+      $objstr =~ s/gtk\.o/emcc\.o/g;
+      print &splitline("\$(OUTPREFIX)" . $prog . ".js: " . $objstr . " emccpre.js emcclib.js emccx.json"), "\n";
+      print "\t\$(EMCC) -o \$(OUTPREFIX)".$prog.".js ".
+          "-O2 ".
+          "-s ASM_JS=1 ".
+          "--pre-js emccpre.js ".
+          "--js-library emcclib.js ".
+          "-s EXPORTED_FUNCTIONS=\"`sed 's://.*::' emccx.json | tr -d ' \\n'`\" " . $objstr . "\n\n";
+    }
+    foreach $d (&deps("X.o", undef, $dirpfx, "/")) {
+      $oobjs = $d->{obj};
+      $ddeps= join " ", @{$d->{deps}};
+      $oobjs =~ s/gtk/emcc/g;
+      $ddeps =~ s/gtk/emcc/g;
+      print &splitline(sprintf("%s: %s", $oobjs, $ddeps)),
+          "\n";
+      $deflist = join "", map { " -D$_" } @{$d->{defs}};
+      print "\t\$(EMCC) \$(CFLAGS) \$(XFLAGS)$deflist" .
+         " -c \$< -o \$\@\n";
+    }
+    print "\n";
+    print $makefile_extra{'emcc'} || "";
+    print "\nclean:\n".
+    "\trm -rf *.o $output_js_files\n";
+    select STDOUT; close OUT;
+}