diff --git a/Buildscr b/Buildscr
index 6d2237d..6925ce3 100644
--- 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 425f419..5d63ae5 100644
--- 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
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
+
+#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
index 0000000..51c8f93
--- /dev/null
+++ b/emcclib.js
@@ -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
index 0000000..548e2a5
--- /dev/null
+++ b/emccpre.js
@@ -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