emcclib.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845
  1. /*
  2. * emcclib.js: one of the Javascript components of an Emscripten-based
  3. * web/Javascript front end for Puzzles.
  4. *
  5. * The other parts of this system live in emcc.c and emccpre.js. It
  6. * also depends on being run in the context of a web page containing
  7. * an appropriate collection of bits and pieces (a canvas, some
  8. * buttons and links etc), which is generated for each puzzle by the
  9. * script html/jspage.pl.
  10. *
  11. * This file contains a set of Javascript functions which we insert
  12. * into Emscripten's library object via the --js-library option; this
  13. * allows us to provide JS code which can be called from the
  14. * Emscripten-compiled C, mostly dealing with UI interaction of
  15. * various kinds.
  16. */
  17. mergeInto(LibraryManager.library, {
  18. /*
  19. * void js_init_puzzle(void);
  20. *
  21. * Called at the start of main() to set up event handlers.
  22. */
  23. js_init_puzzle: function() {
  24. initPuzzle();
  25. },
  26. /*
  27. * void js_post_init(void);
  28. *
  29. * Called at the end of main() once the initial puzzle has been
  30. * started.
  31. */
  32. js_post_init: function() {
  33. post_init();
  34. },
  35. /*
  36. * void js_debug(const char *message);
  37. *
  38. * A function to write a diagnostic to the Javascript console.
  39. * Unused in production, but handy in development.
  40. */
  41. js_debug: function(ptr) {
  42. console.log(UTF8ToString(ptr));
  43. },
  44. /*
  45. * void js_error_box(const char *message);
  46. *
  47. * A wrapper around Javascript's alert(), so the C code can print
  48. * simple error message boxes (e.g. when invalid data is entered
  49. * in a configuration dialog).
  50. */
  51. js_error_box: function(ptr) {
  52. alert(UTF8ToString(ptr));
  53. },
  54. /*
  55. * void js_remove_type_dropdown(void);
  56. *
  57. * Get rid of the drop-down list on the web page for selecting
  58. * game presets. Called at setup time if the game back end
  59. * provides neither presets nor configurability.
  60. */
  61. js_remove_type_dropdown: function() {
  62. if (gametypelist === null) return;
  63. var gametypeitem = gametypelist.closest("li");
  64. if (gametypeitem === null) return;
  65. gametypeitem.parentNode.removeChild(gametypeitem);
  66. gametypelist = null;
  67. },
  68. /*
  69. * void js_remove_solve_button(void);
  70. *
  71. * Get rid of the Solve button on the web page. Called at setup
  72. * time if the game doesn't support an in-game solve function.
  73. */
  74. js_remove_solve_button: function() {
  75. if (solve_button === null) return;
  76. var solve_item = solve_button.closest("li");
  77. if (solve_item === null) return;
  78. solve_item.parentNode.removeChild(solve_item);
  79. solve_button = null;
  80. },
  81. /*
  82. * void js_add_preset(int menuid, const char *name, int value);
  83. *
  84. * Add a preset to the drop-down types menu, or to a submenu of
  85. * it. 'menuid' specifies an index into our array of submenus
  86. * where the item might be placed; 'value' specifies the number
  87. * that js_get_selected_preset() will return when this item is
  88. * clicked.
  89. */
  90. js_add_preset: function(menuid, ptr, value) {
  91. var name = UTF8ToString(ptr);
  92. var item = document.createElement("li");
  93. var label = document.createElement("label");
  94. var tick = document.createElement("input");
  95. tick.type = "radio";
  96. tick.className = "tick";
  97. tick.name = "preset";
  98. tick.value = value;
  99. label.appendChild(tick);
  100. label.appendChild(document.createTextNode(" " + name));
  101. item.appendChild(label);
  102. gametypesubmenus[menuid].appendChild(item);
  103. tick.onclick = function(event) {
  104. if (dlg_dimmer === null) {
  105. command(2);
  106. }
  107. }
  108. },
  109. /*
  110. * int js_add_preset_submenu(int menuid, const char *name);
  111. *
  112. * Add a submenu in the presets menu hierarchy. Returns its index,
  113. * for passing as the 'menuid' argument in further calls to
  114. * js_add_preset or this function.
  115. */
  116. js_add_preset_submenu: function(menuid, ptr, value) {
  117. var name = UTF8ToString(ptr);
  118. var item = document.createElement("li");
  119. // We still create a transparent tick element, even though it
  120. // won't ever be selected, to make submenu titles line up
  121. // nicely with their neighbours.
  122. var label = document.createElement("div");
  123. var tick = document.createElement("span");
  124. tick.className = "tick";
  125. label.appendChild(tick);
  126. label.tabIndex = 0;
  127. label.appendChild(document.createTextNode(" " + name));
  128. item.appendChild(label);
  129. var submenu = document.createElement("ul");
  130. label.appendChild(submenu);
  131. gametypesubmenus[menuid].appendChild(item);
  132. var toret = gametypesubmenus.length;
  133. gametypesubmenus.push(submenu);
  134. return toret;
  135. },
  136. /*
  137. * int js_get_selected_preset(void);
  138. *
  139. * Return the index of the currently selected value in the type
  140. * dropdown.
  141. */
  142. js_get_selected_preset: function() {
  143. return menuform.elements["preset"].value;
  144. },
  145. /*
  146. * void js_select_preset(int n);
  147. *
  148. * Cause a different value to be selected in the type dropdown
  149. * (for when the user selects values from the Custom configurer
  150. * which turn out to exactly match a preset).
  151. */
  152. js_select_preset: function(n) {
  153. menuform.elements["preset"].value = n;
  154. },
  155. /*
  156. * void js_default_colour(float *output);
  157. *
  158. * Try to extract a default colour from the CSS computed
  159. * background colour of the canvas element.
  160. */
  161. js_default_colour: function(output) {
  162. var col = window.getComputedStyle(onscreen_canvas).backgroundColor;
  163. /* We only support opaque sRGB colours. */
  164. var m = col.match(
  165. /^rgb\((\d+(?:\.\d+)?), (\d+(?:\.\d+)?), (\d+(?:\.\d+)?)\)$/);
  166. if (m) {
  167. setValue(output, +m[1] / 255, "float");
  168. setValue(output + 4, +m[2] / 255, "float");
  169. setValue(output + 8, +m[3] / 255, "float");
  170. }
  171. },
  172. /*
  173. * void js_set_background_colour(const char *bg);
  174. *
  175. * Record the puzzle background colour in a CSS variable so
  176. * the style sheet can use it if it wants.
  177. */
  178. js_set_background_colour: function(bgptr) {
  179. document.documentElement.style.setProperty("--puzzle-background",
  180. UTF8ToString(bgptr));
  181. },
  182. /*
  183. * void js_get_date_64(unsigned *p);
  184. *
  185. * Return the current date, in milliseconds since the epoch
  186. * (Javascript's native format), as a 64-bit integer. Used to
  187. * invent an initial random seed for puzzle generation.
  188. */
  189. js_get_date_64: function(ptr) {
  190. var d = (new Date()).valueOf();
  191. setValue(ptr, d, 'i64');
  192. },
  193. /*
  194. * void js_update_permalinks(const char *desc, const char *seed);
  195. *
  196. * Update the permalinks on the web page for a new game
  197. * description and optional random seed. desc can never be NULL,
  198. * but seed might be (if the game was generated by entering a
  199. * descriptive id by hand), in which case we suppress display of
  200. * the random seed permalink.
  201. */
  202. js_update_permalinks: function(desc, seed) {
  203. desc = encodeURI(UTF8ToString(desc)).replace(/#/g, "%23");
  204. if (permalink_desc !== null)
  205. permalink_desc.href = "#" + desc;
  206. if (permalink_seed !== null) {
  207. if (seed == 0) {
  208. permalink_seed.style.display = "none";
  209. } else {
  210. seed = encodeURI(UTF8ToString(seed)).replace(/#/g, "%23");;
  211. permalink_seed.href = "#" + seed;
  212. permalink_seed.style.display = "";
  213. }
  214. }
  215. },
  216. /*
  217. * void js_enable_undo_redo(int undo, int redo);
  218. *
  219. * Set the enabled/disabled states of the undo and redo buttons,
  220. * after a move.
  221. */
  222. js_enable_undo_redo: function(undo, redo) {
  223. disable_menu_item(undo_button, (undo == 0));
  224. disable_menu_item(redo_button, (redo == 0));
  225. },
  226. /*
  227. * void js_enable_undo_redo(bool undo, bool redo);
  228. *
  229. * Update any labels for the SoftLeft and Enter keys.
  230. */
  231. js_update_key_labels: function(lsk_ptr, csk_ptr) {
  232. var elem;
  233. var lsk_text = UTF8ToString(lsk_ptr);
  234. var csk_text = UTF8ToString(csk_ptr);
  235. for (elem of document.querySelectorAll("#puzzle .lsk"))
  236. elem.textContent = lsk_text == csk_text ? "" : lsk_text;
  237. for (elem of document.querySelectorAll("#puzzle .csk"))
  238. elem.textContent = csk_text;
  239. },
  240. /*
  241. * void js_activate_timer();
  242. *
  243. * Start calling the C timer_callback() function every frame.
  244. * The C code ensures that the activate and deactivate functions
  245. * are called in a sensible order.
  246. */
  247. js_activate_timer: function() {
  248. timer_reference = performance.now();
  249. var frame = function(now) {
  250. timer = window.requestAnimationFrame(frame);
  251. // The callback might call js_deactivate_timer() below.
  252. timer_callback((now - timer_reference) / 1000.0);
  253. timer_reference = now;
  254. };
  255. timer = window.requestAnimationFrame(frame);
  256. },
  257. /*
  258. * void js_deactivate_timer();
  259. *
  260. * Stop calling the C timer_callback() function every frame.
  261. */
  262. js_deactivate_timer: function() {
  263. window.cancelAnimationFrame(timer);
  264. },
  265. /*
  266. * void js_canvas_start_draw(void);
  267. *
  268. * Prepare to do some drawing on the canvas.
  269. */
  270. js_canvas_start_draw: function() {
  271. update_xmin = update_xmax = update_ymin = update_ymax = undefined;
  272. },
  273. /*
  274. * void js_canvas_draw_update(int x, int y, int w, int h);
  275. *
  276. * Mark a rectangle of the off-screen canvas as needing to be
  277. * copied to the on-screen one.
  278. */
  279. js_canvas_draw_update: function(x, y, w, h) {
  280. /*
  281. * Currently we do this in a really simple way, just by taking
  282. * the smallest rectangle containing all updates so far. We
  283. * could instead keep the data in a richer form (e.g. retain
  284. * multiple smaller rectangles needing update, and only redraw
  285. * the whole thing beyond a certain threshold) but this will
  286. * do for now.
  287. */
  288. if (update_xmin === undefined || update_xmin > x) update_xmin = x;
  289. if (update_ymin === undefined || update_ymin > y) update_ymin = y;
  290. if (update_xmax === undefined || update_xmax < x+w) update_xmax = x+w;
  291. if (update_ymax === undefined || update_ymax < y+h) update_ymax = y+h;
  292. },
  293. /*
  294. * void js_canvas_end_draw(void);
  295. *
  296. * Finish the drawing, by actually copying the newly drawn stuff
  297. * to the on-screen canvas.
  298. */
  299. js_canvas_end_draw: function() {
  300. if (update_xmin !== undefined) {
  301. var onscreen_ctx =
  302. onscreen_canvas.getContext('2d', { alpha: false });
  303. onscreen_ctx.drawImage(offscreen_canvas,
  304. update_xmin, update_ymin,
  305. update_xmax - update_xmin,
  306. update_ymax - update_ymin,
  307. update_xmin, update_ymin,
  308. update_xmax - update_xmin,
  309. update_ymax - update_ymin);
  310. }
  311. },
  312. /*
  313. * void js_canvas_draw_rect(int x, int y, int w, int h,
  314. * const char *colour);
  315. *
  316. * Draw a rectangle.
  317. */
  318. js_canvas_draw_rect: function(x, y, w, h, colptr) {
  319. ctx.fillStyle = UTF8ToString(colptr);
  320. ctx.fillRect(x, y, w, h);
  321. },
  322. /*
  323. * void js_canvas_clip_rect(int x, int y, int w, int h);
  324. *
  325. * Set a clipping rectangle.
  326. */
  327. js_canvas_clip_rect: function(x, y, w, h) {
  328. ctx.save();
  329. ctx.beginPath();
  330. ctx.rect(x, y, w, h);
  331. ctx.clip();
  332. },
  333. /*
  334. * void js_canvas_unclip(void);
  335. *
  336. * Reset to no clipping.
  337. */
  338. js_canvas_unclip: function() {
  339. ctx.restore();
  340. },
  341. /*
  342. * void js_canvas_draw_line(float x1, float y1, float x2, float y2,
  343. * int width, const char *colour);
  344. *
  345. * Draw a line. We must adjust the coordinates by 0.5 because
  346. * Javascript's canvas coordinates appear to be pixel corners,
  347. * whereas we want pixel centres. Also, we manually draw the pixel
  348. * at each end of the line, which our clients will expect but
  349. * Javascript won't reliably do by default (in common with other
  350. * Postscriptish drawing frameworks).
  351. */
  352. js_canvas_draw_line: function(x1, y1, x2, y2, width, colour) {
  353. colour = UTF8ToString(colour);
  354. ctx.beginPath();
  355. ctx.moveTo(x1 + 0.5, y1 + 0.5);
  356. ctx.lineTo(x2 + 0.5, y2 + 0.5);
  357. ctx.lineWidth = width;
  358. ctx.lineCap = 'round';
  359. ctx.lineJoin = 'round';
  360. ctx.strokeStyle = colour;
  361. ctx.stroke();
  362. ctx.fillStyle = colour;
  363. ctx.fillRect(x1, y1, 1, 1);
  364. ctx.fillRect(x2, y2, 1, 1);
  365. },
  366. /*
  367. * void js_canvas_draw_poly(int *points, int npoints,
  368. * const char *fillcolour,
  369. * const char *outlinecolour);
  370. *
  371. * Draw a polygon.
  372. */
  373. js_canvas_draw_poly: function(pointptr, npoints, fill, outline) {
  374. ctx.beginPath();
  375. ctx.moveTo(getValue(pointptr , 'i32') + 0.5,
  376. getValue(pointptr+4, 'i32') + 0.5);
  377. for (var i = 1; i < npoints; i++)
  378. ctx.lineTo(getValue(pointptr+8*i , 'i32') + 0.5,
  379. getValue(pointptr+8*i+4, 'i32') + 0.5);
  380. ctx.closePath();
  381. if (fill != 0) {
  382. ctx.fillStyle = UTF8ToString(fill);
  383. ctx.fill();
  384. }
  385. ctx.lineWidth = '1';
  386. ctx.lineCap = 'round';
  387. ctx.lineJoin = 'round';
  388. ctx.strokeStyle = UTF8ToString(outline);
  389. ctx.stroke();
  390. },
  391. /*
  392. * void js_canvas_draw_circle(int x, int y, int r,
  393. * const char *fillcolour,
  394. * const char *outlinecolour);
  395. *
  396. * Draw a circle.
  397. */
  398. js_canvas_draw_circle: function(x, y, r, fill, outline) {
  399. ctx.beginPath();
  400. ctx.arc(x + 0.5, y + 0.5, r, 0, 2*Math.PI);
  401. if (fill != 0) {
  402. ctx.fillStyle = UTF8ToString(fill);
  403. ctx.fill();
  404. }
  405. ctx.lineWidth = '1';
  406. ctx.lineCap = 'round';
  407. ctx.lineJoin = 'round';
  408. ctx.strokeStyle = UTF8ToString(outline);
  409. ctx.stroke();
  410. },
  411. /*
  412. * int js_canvas_find_font_midpoint(int height, bool monospaced);
  413. *
  414. * Return the adjustment required for text displayed using
  415. * ALIGN_VCENTRE. We want to place the midpoint between the
  416. * baseline and the cap-height at the specified position; so this
  417. * function returns the adjustment which, when added to the
  418. * desired centre point, returns the y-coordinate at which you
  419. * should put the baseline.
  420. *
  421. * There is no sensible method of querying this kind of font
  422. * metric in Javascript, so instead we render a piece of test text
  423. * to a throwaway offscreen canvas and then read the pixel data
  424. * back out to find the highest and lowest pixels. That's good
  425. * _enough_ (in that we only needed the answer to the nearest
  426. * pixel anyway), but rather disgusting!
  427. *
  428. * Since this is a very expensive operation, we cache the results
  429. * per (font,height) pair.
  430. */
  431. js_canvas_find_font_midpoint: function(height, monospaced) {
  432. if (height == 0) {
  433. // Handle this degenerate case by hand. Otherwise we end
  434. // up passing height=0 to the getImageData call below,
  435. // causing browsers to report errors.
  436. return 0;
  437. }
  438. // Resolve the font into a string.
  439. var ctx1 = onscreen_canvas.getContext('2d', { alpha: false });
  440. canvas_set_font(ctx1, height, monospaced);
  441. // Reuse cached value if possible
  442. if (midpoint_cache[ctx1.font] !== undefined)
  443. return midpoint_cache[ctx1.font];
  444. // Find the width of the string
  445. var width = (ctx1.measureText(midpoint_test_str).width + 1) | 0;
  446. // Construct a test canvas of appropriate size, initialise it to
  447. // black, and draw the string on it in white
  448. var measure_canvas = document.createElement('canvas');
  449. var ctx2 = measure_canvas.getContext('2d', { alpha: false });
  450. ctx2.canvas.width = width;
  451. ctx2.canvas.height = 2*height;
  452. ctx2.fillStyle = "#000000";
  453. ctx2.fillRect(0, 0, width, 2*height);
  454. var baseline = (1.5*height) | 0;
  455. ctx2.fillStyle = "#ffffff";
  456. canvas_set_font(ctx2, height, monospaced);
  457. ctx2.fillText(midpoint_test_str, 0, baseline);
  458. // Scan the contents of the test canvas to find the top and bottom
  459. // set pixels.
  460. var pixels = ctx2.getImageData(0, 0, width, 2*height).data;
  461. var ymin = 2*height, ymax = -1;
  462. for (var y = 0; y < 2*height; y++) {
  463. for (var x = 0; x < width; x++) {
  464. if (pixels[4*(y*width+x)] != 0) {
  465. if (ymin > y) ymin = y;
  466. if (ymax < y) ymax = y;
  467. break;
  468. }
  469. }
  470. }
  471. var ret = (baseline - (ymin + ymax) / 2) | 0;
  472. midpoint_cache[ctx1.font] = ret;
  473. return ret;
  474. },
  475. /*
  476. * void js_canvas_draw_text(int x, int y, int halign,
  477. * const char *colptr, int height,
  478. * bool monospaced, const char *text);
  479. *
  480. * Draw text. Vertical alignment has been taken care of on the C
  481. * side, by optionally calling the above function. Horizontal
  482. * alignment is handled here, since we can get the canvas draw
  483. * function to do it for us with almost no extra effort.
  484. */
  485. js_canvas_draw_text: function(x, y, halign, colptr, fontsize, monospaced,
  486. text) {
  487. canvas_set_font(ctx, fontsize, monospaced);
  488. ctx.fillStyle = UTF8ToString(colptr);
  489. ctx.textAlign = (halign == 0 ? 'left' :
  490. halign == 1 ? 'center' : 'right');
  491. ctx.textBaseline = 'alphabetic';
  492. ctx.fillText(UTF8ToString(text), x, y);
  493. },
  494. /*
  495. * int js_canvas_new_blitter(int w, int h);
  496. *
  497. * Create a new blitter object, which is just an offscreen canvas
  498. * of the specified size.
  499. */
  500. js_canvas_new_blitter: function(w, h) {
  501. var id = blittercount++;
  502. blitters[id] = document.createElement("canvas");
  503. blitters[id].width = w;
  504. blitters[id].height = h;
  505. return id;
  506. },
  507. /*
  508. * void js_canvas_free_blitter(int id);
  509. *
  510. * Free a blitter (or rather, destroy our reference to it so JS
  511. * can garbage-collect it, and also enforce that we don't
  512. * accidentally use it again afterwards).
  513. */
  514. js_canvas_free_blitter: function(id) {
  515. blitters[id] = null;
  516. },
  517. /*
  518. * void js_canvas_copy_to_blitter(int id, int x, int y, int w, int h);
  519. *
  520. * Copy from the puzzle image to a blitter. The size is passed to
  521. * us, partly so we don't have to remember the size of each
  522. * blitter, but mostly so that the C side can adjust the copy
  523. * rectangle in the case where it partially overlaps the edge of
  524. * the screen.
  525. */
  526. js_canvas_copy_to_blitter: function(id, x, y, w, h) {
  527. var blitter_ctx = blitters[id].getContext('2d', { alpha: false });
  528. blitter_ctx.drawImage(offscreen_canvas,
  529. x, y, w, h,
  530. 0, 0, w, h);
  531. },
  532. /*
  533. * void js_canvas_copy_from_blitter(int id, int x, int y, int w, int h);
  534. *
  535. * Copy from a blitter back to the puzzle image. As above, the
  536. * size of the copied rectangle is passed to us from the C side
  537. * and may already have been modified.
  538. */
  539. js_canvas_copy_from_blitter: function(id, x, y, w, h) {
  540. ctx.drawImage(blitters[id],
  541. 0, 0, w, h,
  542. x, y, w, h);
  543. },
  544. /*
  545. * void js_canvas_remove_statusbar(void);
  546. *
  547. * Cause a status bar not to exist. Called at setup time if the
  548. * puzzle back end turns out not to want one.
  549. */
  550. js_canvas_remove_statusbar: function() {
  551. if (statusbar !== null)
  552. statusbar.parentNode.removeChild(statusbar);
  553. statusbar = null;
  554. },
  555. /*
  556. * void js_canvas_set_statusbar(const char *text);
  557. *
  558. * Set the text in the status bar.
  559. */
  560. js_canvas_set_statusbar: function(ptr) {
  561. statusbar.textContent = UTF8ToString(ptr);
  562. },
  563. /*
  564. * bool js_canvas_get_preferred_size(int *wp, int *hp);
  565. *
  566. * This is called before calling midend_size() to set a puzzle to
  567. * the default size. If the JavaScript layer has an opinion about
  568. * how big the puzzle should be, it can overwrite *wp and *hp with
  569. * its preferred size, and return true if the "user" parameter to
  570. * midend_size() should be true. Otherwise it should leave them
  571. * alone and return false.
  572. */
  573. js_canvas_get_preferred_size: function(wp, hp) {
  574. if (document.readyState == "complete" && containing_div !== null) {
  575. var dpr = window.devicePixelRatio || 1;
  576. setValue(wp, containing_div.clientWidth * dpr, "i32");
  577. setValue(hp, containing_div.clientHeight * dpr, "i32");
  578. return true;
  579. }
  580. return false;
  581. },
  582. /*
  583. * void js_canvas_set_size(int w, int h);
  584. *
  585. * Set the size of the puzzle canvas. Called whenever the size of
  586. * the canvas needs to change. That might be because of a change
  587. * of configuration, because the user has resized the puzzle, or
  588. * because the device pixel ratio has changed.
  589. */
  590. js_canvas_set_size: function(w, h) {
  591. onscreen_canvas.width = w;
  592. offscreen_canvas.width = w;
  593. if (resizable_div !== null)
  594. resizable_div.style.width =
  595. w / (window.devicePixelRatio || 1) + "px";
  596. else {
  597. onscreen_canvas.style.width =
  598. w / (window.devicePixelRatio || 1) + "px";
  599. onscreen_canvas.style.height =
  600. h / (window.devicePixelRatio || 1) + "px";
  601. }
  602. onscreen_canvas.height = h;
  603. offscreen_canvas.height = h;
  604. },
  605. /*
  606. * double js_get_device_pixel_ratio();
  607. *
  608. * Return the current device pixel ratio.
  609. */
  610. js_get_device_pixel_ratio: function() {
  611. return window.devicePixelRatio || 1;
  612. },
  613. /*
  614. * void js_dialog_init(const char *title);
  615. *
  616. * Begin constructing a 'dialog box' which will be popped up in an
  617. * overlay on top of the rest of the puzzle web page.
  618. */
  619. js_dialog_init: function(titletext) {
  620. dialog_init(UTF8ToString(titletext));
  621. },
  622. /*
  623. * void js_dialog_string(int i, const char *title, const char *initvalue);
  624. *
  625. * Add a string control (that is, an edit box) to the dialog under
  626. * construction.
  627. */
  628. js_dialog_string: function(index, title, initialtext) {
  629. var label = document.createElement("label");
  630. label.textContent = UTF8ToString(title);
  631. dlg_form.appendChild(label);
  632. var editbox = document.createElement("input");
  633. editbox.type = "text";
  634. editbox.value = UTF8ToString(initialtext);
  635. label.appendChild(editbox);
  636. dlg_form.appendChild(document.createElement("br"));
  637. dlg_return_funcs.push(function() {
  638. dlg_return_sval(index, editbox.value);
  639. });
  640. },
  641. /*
  642. * void js_dialog_choices(int i, const char *title, const char *choicelist,
  643. * int initvalue);
  644. *
  645. * Add a choices control (i.e. a drop-down list) to the dialog
  646. * under construction. The 'choicelist' parameter is unchanged
  647. * from the way the puzzle back end will have supplied it: i.e.
  648. * it's still encoded as a single string whose first character
  649. * gives the separator.
  650. */
  651. js_dialog_choices: function(index, title, choicelist, initvalue) {
  652. var label = document.createElement("label");
  653. label.textContent = UTF8ToString(title);
  654. dlg_form.appendChild(label);
  655. var dropdown = document.createElement("select");
  656. var choicestr = UTF8ToString(choicelist);
  657. var items = choicestr.slice(1).split(choicestr[0]);
  658. var options = [];
  659. for (var i in items) {
  660. var option = document.createElement("option");
  661. option.value = i;
  662. option.appendChild(document.createTextNode(items[i]));
  663. if (i == initvalue) option.selected = true;
  664. dropdown.appendChild(option);
  665. options.push(option);
  666. }
  667. label.appendChild(dropdown);
  668. dlg_form.appendChild(document.createElement("br"));
  669. dlg_return_funcs.push(function() {
  670. var val = 0;
  671. for (var i in options) {
  672. if (options[i].selected) {
  673. val = options[i].value;
  674. break;
  675. }
  676. }
  677. dlg_return_ival(index, val);
  678. });
  679. },
  680. /*
  681. * void js_dialog_boolean(int i, const char *title, int initvalue);
  682. *
  683. * Add a boolean control (a checkbox) to the dialog under
  684. * construction. Checkboxes are generally expected to be sensitive
  685. * on their label text as well as the box itself, so for this
  686. * control we create an actual label rather than merely a text
  687. * node (and hence we must allocate an id to the checkbox so that
  688. * the label can refer to it).
  689. */
  690. js_dialog_boolean: function(index, title, initvalue) {
  691. var checkbox = document.createElement("input");
  692. checkbox.type = "checkbox";
  693. checkbox.checked = (initvalue != 0);
  694. var checkboxlabel = document.createElement("label");
  695. checkboxlabel.appendChild(checkbox);
  696. checkboxlabel.appendChild(document.createTextNode(UTF8ToString(title)));
  697. dlg_form.appendChild(checkboxlabel);
  698. dlg_form.appendChild(document.createElement("br"));
  699. dlg_return_funcs.push(function() {
  700. dlg_return_ival(index, checkbox.checked ? 1 : 0);
  701. });
  702. },
  703. /*
  704. * void js_dialog_launch(void);
  705. *
  706. * Finish constructing a dialog, and actually display it, dimming
  707. * everything else on the page.
  708. */
  709. js_dialog_launch: function() {
  710. dialog_launch(function(event) {
  711. for (var i in dlg_return_funcs)
  712. dlg_return_funcs[i]();
  713. command(3); // OK
  714. }, function(event) {
  715. command(4); // Cancel
  716. });
  717. },
  718. /*
  719. * void js_dialog_cleanup(void);
  720. *
  721. * Stop displaying a dialog, and clean up the internal state
  722. * associated with it.
  723. */
  724. js_dialog_cleanup: function() {
  725. dialog_cleanup();
  726. },
  727. /*
  728. * void js_focus_canvas(void);
  729. *
  730. * Return keyboard focus to the puzzle canvas. Called after a
  731. * puzzle-control button is pressed, which tends to have the side
  732. * effect of taking focus away from the canvas.
  733. */
  734. js_focus_canvas: function() {
  735. onscreen_canvas.focus();
  736. },
  737. /*
  738. * bool js_savefile_read(void *buf, int len);
  739. *
  740. * Read len bytes from the save file that we're currently loading.
  741. */
  742. js_savefile_read: function(buf, len) {
  743. return savefile_read_callback(buf, len);
  744. },
  745. /*
  746. * void js_save_prefs(const char *);
  747. *
  748. * Write a buffer of serialised preferences data into localStorage.
  749. */
  750. js_save_prefs: function(buf) {
  751. var prefsdata = UTF8ToString(buf);
  752. try {
  753. localStorage.setItem(location.pathname + " preferences", prefsdata);
  754. } catch (error) {
  755. // Tell the user their preferences have not been saved.
  756. console.error(error);
  757. alert("Saving of preferences failed: " + error.message);
  758. }
  759. },
  760. /*
  761. * void js_load_prefs(midend *);
  762. *
  763. * Retrieve preferences data from localStorage. If there is any,
  764. * pass it back in as a string, via prefs_load_callback.
  765. */
  766. js_load_prefs: function(me) {
  767. try {
  768. var prefsdata =
  769. localStorage.getItem(location.pathname + " preferences");
  770. if (prefsdata !== undefined && prefsdata !== null) {
  771. var lenbytes = lengthBytesUTF8(prefsdata) + 1;
  772. var dest = _malloc(lenbytes);
  773. if (dest != 0) {
  774. stringToUTF8(prefsdata, dest, lenbytes);
  775. prefs_load_callback(me, dest);
  776. _free(dest);
  777. }
  778. }
  779. } catch (error) {
  780. // Log the error but otherwise pretend the settings were
  781. // absent.
  782. console.warn(error);
  783. }
  784. }
  785. });