From 881746789a489fad85aae8317555f73dbe261556 Mon Sep 17 00:00:00 2001 From: Franklin Wei Date: Sat, 29 Apr 2017 18:21:56 -0400 Subject: puzzles: refactor and resync with upstream This brings puzzles up-to-date with upstream revision 2d333750272c3967cfd5cd3677572cddeaad5932, though certain changes made by me, including cursor-only Untangle and some compilation fixes remain. Upstream code has been moved to its separate subdirectory and future syncs can be done by simply copying over the new sources. Change-Id: Ia6506ca5f78c3627165ea6791d38db414ace0804 --- apps/plugins/puzzles/src/emcclib.js | 752 ++++++++++++++++++++++++++++++++++++ 1 file changed, 752 insertions(+) create mode 100644 apps/plugins/puzzles/src/emcclib.js (limited to 'apps/plugins/puzzles/src/emcclib.js') diff --git a/apps/plugins/puzzles/src/emcclib.js b/apps/plugins/puzzles/src/emcclib.js new file mode 100644 index 0000000000..cd8876e76d --- /dev/null +++ b/apps/plugins/puzzles/src/emcclib.js @@ -0,0 +1,752 @@ +/* + * 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. It + * also depends on being run in the context of a web page containing + * an appropriate collection of bits and pieces (a canvas, some + * buttons and links etc), which is generated for each puzzle by the + * script html/jspage.pl. + * + * 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() { + gametypelist.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(int menuid, const char *name, int value); + * + * Add a preset to the drop-down types menu, or to a submenu of + * it. 'menuid' specifies an index into our array of submenus + * where the item might be placed; 'value' specifies the number + * that js_get_selected_preset() will return when this item is + * clicked. + */ + js_add_preset: function(menuid, ptr, value) { + var name = Pointer_stringify(ptr); + var item = document.createElement("li"); + item.setAttribute("data-index", value); + var tick = document.createElement("span"); + tick.appendChild(document.createTextNode("\u2713")); + tick.style.color = "transparent"; + tick.style.paddingRight = "0.5em"; + item.appendChild(tick); + item.appendChild(document.createTextNode(name)); + gametypesubmenus[menuid].appendChild(item); + gametypeitems.push(item); + + item.onclick = function(event) { + if (dlg_dimmer === null) { + gametypeselectedindex = value; + command(2); + } + } + }, + + /* + * int js_add_preset_submenu(int menuid, const char *name); + * + * Add a submenu in the presets menu hierarchy. Returns its index, + * for passing as the 'menuid' argument in further calls to + * js_add_preset or this function. + */ + js_add_preset_submenu: function(menuid, ptr, value) { + var name = Pointer_stringify(ptr); + var item = document.createElement("li"); + // We still create a transparent tick element, even though it + // won't ever be selected, to make submenu titles line up + // nicely with their neighbours. + var tick = document.createElement("span"); + tick.appendChild(document.createTextNode("\u2713")); + tick.style.color = "transparent"; + tick.style.paddingRight = "0.5em"; + item.appendChild(tick); + item.appendChild(document.createTextNode(name)); + var submenu = document.createElement("ul"); + submenu.className = "left"; + item.appendChild(submenu); + gametypesubmenus[menuid].appendChild(item); + var toret = gametypesubmenus.length; + gametypesubmenus.push(submenu); + return toret; + }, + + /* + * int js_get_selected_preset(void); + * + * Return the index of the currently selected value in the type + * dropdown. + */ + js_get_selected_preset: function() { + return gametypeselectedindex; + }, + + /* + * 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) { + gametypeselectedindex = n; + for (var i in gametypeitems) { + var item = gametypeitems[i]; + var tick = item.firstChild; + if (item.getAttribute("data-index") == n) { + tick.style.color = "inherit"; + } else { + tick.style.color = "transparent"; + } + } + }, + + /* + * 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) { + disable_menu_item(undo_button, (undo == 0)); + disable_menu_item(redo_button, (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 = 'round'; + ctx.lineJoin = 'round'; + 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 = 'round'; + ctx.lineJoin = 'round'; + 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 = 'round'; + ctx.lineJoin = 'round'; + 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 + 1) | 0; + + // 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; + return id; + }, + + /* + * 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 statusholder = document.getElementById("statusbarholder"); + statusbar = document.createElement("div"); + statusbar.style.overflow = "hidden"; + statusbar.style.width = (onscreen_canvas.width - 4) + "px"; + statusholder.style.width = onscreen_canvas.width + "px"; + statusbar.style.height = "1.2em"; + statusbar.style.textAlign = "left"; + 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(" ")); + statusholder.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) + "px"; + document.getElementById("statusbarholder").style.width = w + "px"; + } + resizable_div.style.width = w + "px"; + + 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) + "px"; + 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 = "20px"; + dlg_form.style.top = (window.innerHeight / 10) + "px"; + dlg_form.style.left = (window.innerWidth / 6) + "px"; + 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 = i; + option.appendChild(document.createTextNode(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 = options[i].value; + 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(); + } +}); -- cgit v1.2.3