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 + + + + + + + +

+

+ + + + + + +
+ +
+
+

+

+ Link to this puzzle: + by game ID + by random seed +

+
+ +${instructions} + +${links} + +${footer} + + +EOF + + close $outpage; +} diff --git a/mkfiles.pl b/mkfiles.pl index 625b9ab..9629a29 100755 --- a/mkfiles.pl +++ b/mkfiles.pl @@ -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; +}