diff options
author | Franklin Wei <frankhwei536@gmail.com> | 2016-11-20 15:16:41 -0500 |
---|---|---|
committer | Franklin Wei <me@fwei.tk> | 2016-12-18 18:13:22 +0100 |
commit | 1a6a8b52f7aa4e2da6f4c34a0c743c760b8cfd99 (patch) | |
tree | 8e7f2d6b0cbdb5d15c13457b2c3e1de69f598440 /apps/plugins/puzzles/emcclib.js | |
parent | 3ee79724f6fb033d50e26ef37b33d3f8cedf0c5b (diff) | |
download | rockbox-1a6a8b52f7aa4e2da6f4c34a0c743c760b8cfd99.tar.gz rockbox-1a6a8b52f7aa4e2da6f4c34a0c743c760b8cfd99.zip |
Port of Simon Tatham's Puzzle Collection
Original revision: 5123b1bf68777ffa86e651f178046b26a87cf2d9
MIT Licensed. Some games still crash and others are unplayable due to
issues with controls. Still need a "real" polygon filling algorithm.
Currently builds one plugin per puzzle (about 40 in total, around 100K
each on ARM), but can easily be made to build a single monolithic
overlay (800K or so on ARM).
The following games are at least partially broken for various reasons,
and have been disabled on this commit:
Cube: failed assertion with "Icosahedron" setting
Keen: input issues
Mines: weird stuff happens on target
Palisade: input issues
Solo: input issues, occasional crash on target
Towers: input issues
Undead: input issues
Unequal: input and drawing issues (concave polys)
Untangle: input issues
Features left to do:
- In-game help system
- Figure out the weird bugs
Change-Id: I7c69b6860ab115f973c8d76799502e9bb3d52368
Diffstat (limited to 'apps/plugins/puzzles/emcclib.js')
-rw-r--r-- | apps/plugins/puzzles/emcclib.js | 757 |
1 files changed, 757 insertions, 0 deletions
diff --git a/apps/plugins/puzzles/emcclib.js b/apps/plugins/puzzles/emcclib.js new file mode 100644 index 0000000000..385281ad0b --- /dev/null +++ b/apps/plugins/puzzles/emcclib.js | |||
@@ -0,0 +1,757 @@ | |||
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 | |||
18 | mergeInto(LibraryManager.library, { | ||
19 | /* | ||
20 | * void js_debug(const char *message); | ||
21 | * | ||
22 | * A function to write a diagnostic to the Javascript console. | ||
23 | * Unused in production, but handy in development. | ||
24 | */ | ||
25 | js_debug: function(ptr) { | ||
26 | console.log(Pointer_stringify(ptr)); | ||
27 | }, | ||
28 | |||
29 | /* | ||
30 | * void js_error_box(const char *message); | ||
31 | * | ||
32 | * A wrapper around Javascript's alert(), so the C code can print | ||
33 | * simple error message boxes (e.g. when invalid data is entered | ||
34 | * in a configuration dialog). | ||
35 | */ | ||
36 | js_error_box: function(ptr) { | ||
37 | alert(Pointer_stringify(ptr)); | ||
38 | }, | ||
39 | |||
40 | /* | ||
41 | * void js_remove_type_dropdown(void); | ||
42 | * | ||
43 | * Get rid of the drop-down list on the web page for selecting | ||
44 | * game presets. Called at setup time if the game back end | ||
45 | * provides neither presets nor configurability. | ||
46 | */ | ||
47 | js_remove_type_dropdown: function() { | ||
48 | document.getElementById("gametype").style.display = "none"; | ||
49 | }, | ||
50 | |||
51 | /* | ||
52 | * void js_remove_solve_button(void); | ||
53 | * | ||
54 | * Get rid of the Solve button on the web page. Called at setup | ||
55 | * time if the game doesn't support an in-game solve function. | ||
56 | */ | ||
57 | js_remove_solve_button: function() { | ||
58 | document.getElementById("solve").style.display = "none"; | ||
59 | }, | ||
60 | |||
61 | /* | ||
62 | * void js_add_preset(const char *name); | ||
63 | * | ||
64 | * Add a preset to the drop-down types menu. The provided text is | ||
65 | * the name of the preset. (The corresponding game_params stays on | ||
66 | * the C side and never comes out this far; we just pass a numeric | ||
67 | * index back to the C code when a selection is made.) | ||
68 | * | ||
69 | * The special 'Custom' preset is requested by passing NULL to | ||
70 | * this function, rather than the string "Custom", since in that | ||
71 | * case we need to do something special - see below. | ||
72 | */ | ||
73 | js_add_preset: function(ptr) { | ||
74 | var name = (ptr == 0 ? "Customise..." : Pointer_stringify(ptr)); | ||
75 | var value = gametypeoptions.length; | ||
76 | |||
77 | var option = document.createElement("option"); | ||
78 | option.value = value; | ||
79 | option.appendChild(document.createTextNode(name)); | ||
80 | gametypeselector.appendChild(option); | ||
81 | gametypeoptions.push(option); | ||
82 | |||
83 | if (ptr == 0) { | ||
84 | // The option we've just created is the one for inventing | ||
85 | // a new custom setup. | ||
86 | gametypenewcustom = option; | ||
87 | option.value = -1; | ||
88 | |||
89 | // Now create another element called 'Custom', which will | ||
90 | // be auto-selected by us to indicate the custom settings | ||
91 | // you've previously selected. However, we don't add it to | ||
92 | // the game type selector; it will only appear when the | ||
93 | // user actually has custom settings selected. | ||
94 | option = document.createElement("option"); | ||
95 | option.value = -2; | ||
96 | option.appendChild(document.createTextNode("Custom")); | ||
97 | gametypethiscustom = option; | ||
98 | } | ||
99 | }, | ||
100 | |||
101 | /* | ||
102 | * int js_get_selected_preset(void); | ||
103 | * | ||
104 | * Return the index of the currently selected value in the type | ||
105 | * dropdown. | ||
106 | */ | ||
107 | js_get_selected_preset: function() { | ||
108 | for (var i in gametypeoptions) { | ||
109 | if (gametypeoptions[i].selected) { | ||
110 | return gametypeoptions[i].value; | ||
111 | } | ||
112 | } | ||
113 | return 0; | ||
114 | }, | ||
115 | |||
116 | /* | ||
117 | * void js_select_preset(int n); | ||
118 | * | ||
119 | * Cause a different value to be selected in the type dropdown | ||
120 | * (for when the user selects values from the Custom configurer | ||
121 | * which turn out to exactly match a preset). | ||
122 | */ | ||
123 | js_select_preset: function(n) { | ||
124 | if (gametypethiscustom !== null) { | ||
125 | // Fiddle with the Custom/Customise options. If we're | ||
126 | // about to select the Custom option, then it should be in | ||
127 | // the menu, and the other one should read "Re-customise"; | ||
128 | // if we're about to select another one, then the static | ||
129 | // Custom option should disappear and the other one should | ||
130 | // read "Customise". | ||
131 | |||
132 | if (gametypethiscustom.parentNode == gametypeselector) | ||
133 | gametypeselector.removeChild(gametypethiscustom); | ||
134 | if (gametypenewcustom.parentNode == gametypeselector) | ||
135 | gametypeselector.removeChild(gametypenewcustom); | ||
136 | |||
137 | if (n < 0) { | ||
138 | gametypeselector.appendChild(gametypethiscustom); | ||
139 | gametypenewcustom.lastChild.data = "Re-customise..."; | ||
140 | } else { | ||
141 | gametypenewcustom.lastChild.data = "Customise..."; | ||
142 | } | ||
143 | gametypeselector.appendChild(gametypenewcustom); | ||
144 | gametypenewcustom.selected = false; | ||
145 | } | ||
146 | |||
147 | if (n < 0) { | ||
148 | gametypethiscustom.selected = true; | ||
149 | } else { | ||
150 | gametypeoptions[n].selected = true; | ||
151 | } | ||
152 | }, | ||
153 | |||
154 | /* | ||
155 | * void js_get_date_64(unsigned *p); | ||
156 | * | ||
157 | * Return the current date, in milliseconds since the epoch | ||
158 | * (Javascript's native format), as a 64-bit integer. Used to | ||
159 | * invent an initial random seed for puzzle generation. | ||
160 | */ | ||
161 | js_get_date_64: function(ptr) { | ||
162 | var d = (new Date()).valueOf(); | ||
163 | setValue(ptr, d, 'i64'); | ||
164 | }, | ||
165 | |||
166 | /* | ||
167 | * void js_update_permalinks(const char *desc, const char *seed); | ||
168 | * | ||
169 | * Update the permalinks on the web page for a new game | ||
170 | * description and optional random seed. desc can never be NULL, | ||
171 | * but seed might be (if the game was generated by entering a | ||
172 | * descriptive id by hand), in which case we suppress display of | ||
173 | * the random seed permalink. | ||
174 | */ | ||
175 | js_update_permalinks: function(desc, seed) { | ||
176 | desc = Pointer_stringify(desc); | ||
177 | permalink_desc.href = "#" + desc; | ||
178 | |||
179 | if (seed == 0) { | ||
180 | permalink_seed.style.display = "none"; | ||
181 | } else { | ||
182 | seed = Pointer_stringify(seed); | ||
183 | permalink_seed.href = "#" + seed; | ||
184 | permalink_seed.style.display = "inline"; | ||
185 | } | ||
186 | }, | ||
187 | |||
188 | /* | ||
189 | * void js_enable_undo_redo(int undo, int redo); | ||
190 | * | ||
191 | * Set the enabled/disabled states of the undo and redo buttons, | ||
192 | * after a move. | ||
193 | */ | ||
194 | js_enable_undo_redo: function(undo, redo) { | ||
195 | undo_button.disabled = (undo == 0); | ||
196 | redo_button.disabled = (redo == 0); | ||
197 | }, | ||
198 | |||
199 | /* | ||
200 | * void js_activate_timer(); | ||
201 | * | ||
202 | * Start calling the C timer_callback() function every 20ms. | ||
203 | */ | ||
204 | js_activate_timer: function() { | ||
205 | if (timer === null) { | ||
206 | timer_reference_date = (new Date()).valueOf(); | ||
207 | timer = setInterval(function() { | ||
208 | var now = (new Date()).valueOf(); | ||
209 | timer_callback((now - timer_reference_date) / 1000.0); | ||
210 | timer_reference_date = now; | ||
211 | return true; | ||
212 | }, 20); | ||
213 | } | ||
214 | }, | ||
215 | |||
216 | /* | ||
217 | * void js_deactivate_timer(); | ||
218 | * | ||
219 | * Stop calling the C timer_callback() function every 20ms. | ||
220 | */ | ||
221 | js_deactivate_timer: function() { | ||
222 | if (timer !== null) { | ||
223 | clearInterval(timer); | ||
224 | timer = null; | ||
225 | } | ||
226 | }, | ||
227 | |||
228 | /* | ||
229 | * void js_canvas_start_draw(void); | ||
230 | * | ||
231 | * Prepare to do some drawing on the canvas. | ||
232 | */ | ||
233 | js_canvas_start_draw: function() { | ||
234 | ctx = offscreen_canvas.getContext('2d'); | ||
235 | update_xmin = update_xmax = update_ymin = update_ymax = undefined; | ||
236 | }, | ||
237 | |||
238 | /* | ||
239 | * void js_canvas_draw_update(int x, int y, int w, int h); | ||
240 | * | ||
241 | * Mark a rectangle of the off-screen canvas as needing to be | ||
242 | * copied to the on-screen one. | ||
243 | */ | ||
244 | js_canvas_draw_update: function(x, y, w, h) { | ||
245 | /* | ||
246 | * Currently we do this in a really simple way, just by taking | ||
247 | * the smallest rectangle containing all updates so far. We | ||
248 | * could instead keep the data in a richer form (e.g. retain | ||
249 | * multiple smaller rectangles needing update, and only redraw | ||
250 | * the whole thing beyond a certain threshold) but this will | ||
251 | * do for now. | ||
252 | */ | ||
253 | if (update_xmin === undefined || update_xmin > x) update_xmin = x; | ||
254 | if (update_ymin === undefined || update_ymin > y) update_ymin = y; | ||
255 | if (update_xmax === undefined || update_xmax < x+w) update_xmax = x+w; | ||
256 | if (update_ymax === undefined || update_ymax < y+h) update_ymax = y+h; | ||
257 | }, | ||
258 | |||
259 | /* | ||
260 | * void js_canvas_end_draw(void); | ||
261 | * | ||
262 | * Finish the drawing, by actually copying the newly drawn stuff | ||
263 | * to the on-screen canvas. | ||
264 | */ | ||
265 | js_canvas_end_draw: function() { | ||
266 | if (update_xmin !== undefined) { | ||
267 | var onscreen_ctx = onscreen_canvas.getContext('2d'); | ||
268 | onscreen_ctx.drawImage(offscreen_canvas, | ||
269 | update_xmin, update_ymin, | ||
270 | update_xmax - update_xmin, | ||
271 | update_ymax - update_ymin, | ||
272 | update_xmin, update_ymin, | ||
273 | update_xmax - update_xmin, | ||
274 | update_ymax - update_ymin); | ||
275 | } | ||
276 | ctx = null; | ||
277 | }, | ||
278 | |||
279 | /* | ||
280 | * void js_canvas_draw_rect(int x, int y, int w, int h, | ||
281 | * const char *colour); | ||
282 | * | ||
283 | * Draw a rectangle. | ||
284 | */ | ||
285 | js_canvas_draw_rect: function(x, y, w, h, colptr) { | ||
286 | ctx.fillStyle = Pointer_stringify(colptr); | ||
287 | ctx.fillRect(x, y, w, h); | ||
288 | }, | ||
289 | |||
290 | /* | ||
291 | * void js_canvas_clip_rect(int x, int y, int w, int h); | ||
292 | * | ||
293 | * Set a clipping rectangle. | ||
294 | */ | ||
295 | js_canvas_clip_rect: function(x, y, w, h) { | ||
296 | ctx.save(); | ||
297 | ctx.beginPath(); | ||
298 | ctx.rect(x, y, w, h); | ||
299 | ctx.clip(); | ||
300 | }, | ||
301 | |||
302 | /* | ||
303 | * void js_canvas_unclip(void); | ||
304 | * | ||
305 | * Reset to no clipping. | ||
306 | */ | ||
307 | js_canvas_unclip: function() { | ||
308 | ctx.restore(); | ||
309 | }, | ||
310 | |||
311 | /* | ||
312 | * void js_canvas_draw_line(float x1, float y1, float x2, float y2, | ||
313 | * int width, const char *colour); | ||
314 | * | ||
315 | * Draw a line. We must adjust the coordinates by 0.5 because | ||
316 | * Javascript's canvas coordinates appear to be pixel corners, | ||
317 | * whereas we want pixel centres. Also, we manually draw the pixel | ||
318 | * at each end of the line, which our clients will expect but | ||
319 | * Javascript won't reliably do by default (in common with other | ||
320 | * Postscriptish drawing frameworks). | ||
321 | */ | ||
322 | js_canvas_draw_line: function(x1, y1, x2, y2, width, colour) { | ||
323 | colour = Pointer_stringify(colour); | ||
324 | |||
325 | ctx.beginPath(); | ||
326 | ctx.moveTo(x1 + 0.5, y1 + 0.5); | ||
327 | ctx.lineTo(x2 + 0.5, y2 + 0.5); | ||
328 | ctx.lineWidth = width; | ||
329 | ctx.lineCap = 'round'; | ||
330 | ctx.lineJoin = 'round'; | ||
331 | ctx.strokeStyle = colour; | ||
332 | ctx.stroke(); | ||
333 | ctx.fillStyle = colour; | ||
334 | ctx.fillRect(x1, y1, 1, 1); | ||
335 | ctx.fillRect(x2, y2, 1, 1); | ||
336 | }, | ||
337 | |||
338 | /* | ||
339 | * void js_canvas_draw_poly(int *points, int npoints, | ||
340 | * const char *fillcolour, | ||
341 | * const char *outlinecolour); | ||
342 | * | ||
343 | * Draw a polygon. | ||
344 | */ | ||
345 | js_canvas_draw_poly: function(pointptr, npoints, fill, outline) { | ||
346 | ctx.beginPath(); | ||
347 | ctx.moveTo(getValue(pointptr , 'i32') + 0.5, | ||
348 | getValue(pointptr+4, 'i32') + 0.5); | ||
349 | for (var i = 1; i < npoints; i++) | ||
350 | ctx.lineTo(getValue(pointptr+8*i , 'i32') + 0.5, | ||
351 | getValue(pointptr+8*i+4, 'i32') + 0.5); | ||
352 | ctx.closePath(); | ||
353 | if (fill != 0) { | ||
354 | ctx.fillStyle = Pointer_stringify(fill); | ||
355 | ctx.fill(); | ||
356 | } | ||
357 | ctx.lineWidth = '1'; | ||
358 | ctx.lineCap = 'round'; | ||
359 | ctx.lineJoin = 'round'; | ||
360 | ctx.strokeStyle = Pointer_stringify(outline); | ||
361 | ctx.stroke(); | ||
362 | }, | ||
363 | |||
364 | /* | ||
365 | * void js_canvas_draw_circle(int x, int y, int r, | ||
366 | * const char *fillcolour, | ||
367 | * const char *outlinecolour); | ||
368 | * | ||
369 | * Draw a circle. | ||
370 | */ | ||
371 | js_canvas_draw_circle: function(x, y, r, fill, outline) { | ||
372 | ctx.beginPath(); | ||
373 | ctx.arc(x + 0.5, y + 0.5, r, 0, 2*Math.PI); | ||
374 | if (fill != 0) { | ||
375 | ctx.fillStyle = Pointer_stringify(fill); | ||
376 | ctx.fill(); | ||
377 | } | ||
378 | ctx.lineWidth = '1'; | ||
379 | ctx.lineCap = 'round'; | ||
380 | ctx.lineJoin = 'round'; | ||
381 | ctx.strokeStyle = Pointer_stringify(outline); | ||
382 | ctx.stroke(); | ||
383 | }, | ||
384 | |||
385 | /* | ||
386 | * int js_canvas_find_font_midpoint(int height, const char *fontptr); | ||
387 | * | ||
388 | * Return the adjustment required for text displayed using | ||
389 | * ALIGN_VCENTRE. We want to place the midpoint between the | ||
390 | * baseline and the cap-height at the specified position; so this | ||
391 | * function returns the adjustment which, when added to the | ||
392 | * desired centre point, returns the y-coordinate at which you | ||
393 | * should put the baseline. | ||
394 | * | ||
395 | * There is no sensible method of querying this kind of font | ||
396 | * metric in Javascript, so instead we render a piece of test text | ||
397 | * to a throwaway offscreen canvas and then read the pixel data | ||
398 | * back out to find the highest and lowest pixels. That's good | ||
399 | * _enough_ (in that we only needed the answer to the nearest | ||
400 | * pixel anyway), but rather disgusting! | ||
401 | * | ||
402 | * Since this is a very expensive operation, we cache the results | ||
403 | * per (font,height) pair. | ||
404 | */ | ||
405 | js_canvas_find_font_midpoint: function(height, font) { | ||
406 | font = Pointer_stringify(font); | ||
407 | |||
408 | // Reuse cached value if possible | ||
409 | if (midpoint_cache[font] !== undefined) | ||
410 | return midpoint_cache[font]; | ||
411 | |||
412 | // Find the width of the string | ||
413 | var ctx1 = onscreen_canvas.getContext('2d'); | ||
414 | ctx1.font = font; | ||
415 | var width = (ctx1.measureText(midpoint_test_str).width + 1) | 0; | ||
416 | |||
417 | // Construct a test canvas of appropriate size, initialise it to | ||
418 | // black, and draw the string on it in white | ||
419 | var measure_canvas = document.createElement('canvas'); | ||
420 | var ctx2 = measure_canvas.getContext('2d'); | ||
421 | ctx2.canvas.width = width; | ||
422 | ctx2.canvas.height = 2*height; | ||
423 | ctx2.fillStyle = "#000000"; | ||
424 | ctx2.fillRect(0, 0, width, 2*height); | ||
425 | var baseline = (1.5*height) | 0; | ||
426 | ctx2.fillStyle = "#ffffff"; | ||
427 | ctx2.font = font; | ||
428 | ctx2.fillText(midpoint_test_str, 0, baseline); | ||
429 | |||
430 | // Scan the contents of the test canvas to find the top and bottom | ||
431 | // set pixels. | ||
432 | var pixels = ctx2.getImageData(0, 0, width, 2*height).data; | ||
433 | var ymin = 2*height, ymax = -1; | ||
434 | for (var y = 0; y < 2*height; y++) { | ||
435 | for (var x = 0; x < width; x++) { | ||
436 | if (pixels[4*(y*width+x)] != 0) { | ||
437 | if (ymin > y) ymin = y; | ||
438 | if (ymax < y) ymax = y; | ||
439 | break; | ||
440 | } | ||
441 | } | ||
442 | } | ||
443 | |||
444 | var ret = (baseline - (ymin + ymax) / 2) | 0; | ||
445 | midpoint_cache[font] = ret; | ||
446 | return ret; | ||
447 | }, | ||
448 | |||
449 | /* | ||
450 | * void js_canvas_draw_text(int x, int y, int halign, | ||
451 | * const char *colptr, const char *fontptr, | ||
452 | * const char *text); | ||
453 | * | ||
454 | * Draw text. Vertical alignment has been taken care of on the C | ||
455 | * side, by optionally calling the above function. Horizontal | ||
456 | * alignment is handled here, since we can get the canvas draw | ||
457 | * function to do it for us with almost no extra effort. | ||
458 | */ | ||
459 | js_canvas_draw_text: function(x, y, halign, colptr, fontptr, text) { | ||
460 | ctx.font = Pointer_stringify(fontptr); | ||
461 | ctx.fillStyle = Pointer_stringify(colptr); | ||
462 | ctx.textAlign = (halign == 0 ? 'left' : | ||
463 | halign == 1 ? 'center' : 'right'); | ||
464 | ctx.textBaseline = 'alphabetic'; | ||
465 | ctx.fillText(Pointer_stringify(text), x, y); | ||
466 | }, | ||
467 | |||
468 | /* | ||
469 | * int js_canvas_new_blitter(int w, int h); | ||
470 | * | ||
471 | * Create a new blitter object, which is just an offscreen canvas | ||
472 | * of the specified size. | ||
473 | */ | ||
474 | js_canvas_new_blitter: function(w, h) { | ||
475 | var id = blittercount++; | ||
476 | blitters[id] = document.createElement("canvas"); | ||
477 | blitters[id].width = w; | ||
478 | blitters[id].height = h; | ||
479 | return id; | ||
480 | }, | ||
481 | |||
482 | /* | ||
483 | * void js_canvas_free_blitter(int id); | ||
484 | * | ||
485 | * Free a blitter (or rather, destroy our reference to it so JS | ||
486 | * can garbage-collect it, and also enforce that we don't | ||
487 | * accidentally use it again afterwards). | ||
488 | */ | ||
489 | js_canvas_free_blitter: function(id) { | ||
490 | blitters[id] = null; | ||
491 | }, | ||
492 | |||
493 | /* | ||
494 | * void js_canvas_copy_to_blitter(int id, int x, int y, int w, int h); | ||
495 | * | ||
496 | * Copy from the puzzle image to a blitter. The size is passed to | ||
497 | * us, partly so we don't have to remember the size of each | ||
498 | * blitter, but mostly so that the C side can adjust the copy | ||
499 | * rectangle in the case where it partially overlaps the edge of | ||
500 | * the screen. | ||
501 | */ | ||
502 | js_canvas_copy_to_blitter: function(id, x, y, w, h) { | ||
503 | var blitter_ctx = blitters[id].getContext('2d'); | ||
504 | blitter_ctx.drawImage(offscreen_canvas, | ||
505 | x, y, w, h, | ||
506 | 0, 0, w, h); | ||
507 | }, | ||
508 | |||
509 | /* | ||
510 | * void js_canvas_copy_from_blitter(int id, int x, int y, int w, int h); | ||
511 | * | ||
512 | * Copy from a blitter back to the puzzle image. As above, the | ||
513 | * size of the copied rectangle is passed to us from the C side | ||
514 | * and may already have been modified. | ||
515 | */ | ||
516 | js_canvas_copy_from_blitter: function(id, x, y, w, h) { | ||
517 | ctx.drawImage(blitters[id], | ||
518 | 0, 0, w, h, | ||
519 | x, y, w, h); | ||
520 | }, | ||
521 | |||
522 | /* | ||
523 | * void js_canvas_make_statusbar(void); | ||
524 | * | ||
525 | * Cause a status bar to exist. Called at setup time if the puzzle | ||
526 | * back end turns out to want one. | ||
527 | */ | ||
528 | js_canvas_make_statusbar: function() { | ||
529 | var statusholder = document.getElementById("statusbarholder"); | ||
530 | statusbar = document.createElement("div"); | ||
531 | statusbar.style.overflow = "hidden"; | ||
532 | statusbar.style.width = (onscreen_canvas.width - 4) + "px"; | ||
533 | statusholder.style.width = onscreen_canvas.width + "px"; | ||
534 | statusbar.style.height = "1.2em"; | ||
535 | statusbar.style.textAlign = "left"; | ||
536 | statusbar.style.background = "#d8d8d8"; | ||
537 | statusbar.style.borderLeft = '2px solid #c8c8c8'; | ||
538 | statusbar.style.borderTop = '2px solid #c8c8c8'; | ||
539 | statusbar.style.borderRight = '2px solid #e8e8e8'; | ||
540 | statusbar.style.borderBottom = '2px solid #e8e8e8'; | ||
541 | statusbar.appendChild(document.createTextNode(" ")); | ||
542 | statusholder.appendChild(statusbar); | ||
543 | }, | ||
544 | |||
545 | /* | ||
546 | * void js_canvas_set_statusbar(const char *text); | ||
547 | * | ||
548 | * Set the text in the status bar. | ||
549 | */ | ||
550 | js_canvas_set_statusbar: function(ptr) { | ||
551 | var text = Pointer_stringify(ptr); | ||
552 | statusbar.replaceChild(document.createTextNode(text), | ||
553 | statusbar.lastChild); | ||
554 | }, | ||
555 | |||
556 | /* | ||
557 | * void js_canvas_set_size(int w, int h); | ||
558 | * | ||
559 | * Set the size of the puzzle canvas. Called at setup, and every | ||
560 | * time the user picks new puzzle settings requiring a different | ||
561 | * size. | ||
562 | */ | ||
563 | js_canvas_set_size: function(w, h) { | ||
564 | onscreen_canvas.width = w; | ||
565 | offscreen_canvas.width = w; | ||
566 | if (statusbar !== null) { | ||
567 | statusbar.style.width = (w - 4) + "px"; | ||
568 | document.getElementById("statusbarholder").style.width = w + "px"; | ||
569 | } | ||
570 | resizable_div.style.width = w + "px"; | ||
571 | |||
572 | onscreen_canvas.height = h; | ||
573 | offscreen_canvas.height = h; | ||
574 | }, | ||
575 | |||
576 | /* | ||
577 | * void js_dialog_init(const char *title); | ||
578 | * | ||
579 | * Begin constructing a 'dialog box' which will be popped up in an | ||
580 | * overlay on top of the rest of the puzzle web page. | ||
581 | */ | ||
582 | js_dialog_init: function(titletext) { | ||
583 | // Create an overlay on the page which darkens everything | ||
584 | // beneath it. | ||
585 | dlg_dimmer = document.createElement("div"); | ||
586 | dlg_dimmer.style.width = "100%"; | ||
587 | dlg_dimmer.style.height = "100%"; | ||
588 | dlg_dimmer.style.background = '#000000'; | ||
589 | dlg_dimmer.style.position = 'fixed'; | ||
590 | dlg_dimmer.style.opacity = 0.3; | ||
591 | dlg_dimmer.style.top = dlg_dimmer.style.left = 0; | ||
592 | dlg_dimmer.style["z-index"] = 99; | ||
593 | |||
594 | // Now create a form which sits on top of that in turn. | ||
595 | dlg_form = document.createElement("form"); | ||
596 | dlg_form.style.width = (window.innerWidth * 2 / 3) + "px"; | ||
597 | dlg_form.style.opacity = 1; | ||
598 | dlg_form.style.background = '#ffffff'; | ||
599 | dlg_form.style.color = '#000000'; | ||
600 | dlg_form.style.position = 'absolute'; | ||
601 | dlg_form.style.border = "2px solid black"; | ||
602 | dlg_form.style.padding = "20px"; | ||
603 | dlg_form.style.top = (window.innerHeight / 10) + "px"; | ||
604 | dlg_form.style.left = (window.innerWidth / 6) + "px"; | ||
605 | dlg_form.style["z-index"] = 100; | ||
606 | |||
607 | var title = document.createElement("p"); | ||
608 | title.style.marginTop = "0px"; | ||
609 | title.appendChild(document.createTextNode | ||
610 | (Pointer_stringify(titletext))); | ||
611 | dlg_form.appendChild(title); | ||
612 | |||
613 | dlg_return_funcs = []; | ||
614 | dlg_next_id = 0; | ||
615 | }, | ||
616 | |||
617 | /* | ||
618 | * void js_dialog_string(int i, const char *title, const char *initvalue); | ||
619 | * | ||
620 | * Add a string control (that is, an edit box) to the dialog under | ||
621 | * construction. | ||
622 | */ | ||
623 | js_dialog_string: function(index, title, initialtext) { | ||
624 | dlg_form.appendChild(document.createTextNode(Pointer_stringify(title))); | ||
625 | var editbox = document.createElement("input"); | ||
626 | editbox.type = "text"; | ||
627 | editbox.value = Pointer_stringify(initialtext); | ||
628 | dlg_form.appendChild(editbox); | ||
629 | dlg_form.appendChild(document.createElement("br")); | ||
630 | |||
631 | dlg_return_funcs.push(function() { | ||
632 | dlg_return_sval(index, editbox.value); | ||
633 | }); | ||
634 | }, | ||
635 | |||
636 | /* | ||
637 | * void js_dialog_choices(int i, const char *title, const char *choicelist, | ||
638 | * int initvalue); | ||
639 | * | ||
640 | * Add a choices control (i.e. a drop-down list) to the dialog | ||
641 | * under construction. The 'choicelist' parameter is unchanged | ||
642 | * from the way the puzzle back end will have supplied it: i.e. | ||
643 | * it's still encoded as a single string whose first character | ||
644 | * gives the separator. | ||
645 | */ | ||
646 | js_dialog_choices: function(index, title, choicelist, initvalue) { | ||
647 | dlg_form.appendChild(document.createTextNode(Pointer_stringify(title))); | ||
648 | var dropdown = document.createElement("select"); | ||
649 | var choicestr = Pointer_stringify(choicelist); | ||
650 | var items = choicestr.slice(1).split(choicestr[0]); | ||
651 | var options = []; | ||
652 | for (var i in items) { | ||
653 | var option = document.createElement("option"); | ||
654 | option.value = i; | ||
655 | option.appendChild(document.createTextNode(items[i])); | ||
656 | if (i == initvalue) option.selected = true; | ||
657 | dropdown.appendChild(option); | ||
658 | options.push(option); | ||
659 | } | ||
660 | dlg_form.appendChild(dropdown); | ||
661 | dlg_form.appendChild(document.createElement("br")); | ||
662 | |||
663 | dlg_return_funcs.push(function() { | ||
664 | var val = 0; | ||
665 | for (var i in options) { | ||
666 | if (options[i].selected) { | ||
667 | val = options[i].value; | ||
668 | break; | ||
669 | } | ||
670 | } | ||
671 | dlg_return_ival(index, val); | ||
672 | }); | ||
673 | }, | ||
674 | |||
675 | /* | ||
676 | * void js_dialog_boolean(int i, const char *title, int initvalue); | ||
677 | * | ||
678 | * Add a boolean control (a checkbox) to the dialog under | ||
679 | * construction. Checkboxes are generally expected to be sensitive | ||
680 | * on their label text as well as the box itself, so for this | ||
681 | * control we create an actual label rather than merely a text | ||
682 | * node (and hence we must allocate an id to the checkbox so that | ||
683 | * the label can refer to it). | ||
684 | */ | ||
685 | js_dialog_boolean: function(index, title, initvalue) { | ||
686 | var checkbox = document.createElement("input"); | ||
687 | checkbox.type = "checkbox"; | ||
688 | checkbox.id = "cb" + String(dlg_next_id++); | ||
689 | checkbox.checked = (initvalue != 0); | ||
690 | dlg_form.appendChild(checkbox); | ||
691 | var checkboxlabel = document.createElement("label"); | ||
692 | checkboxlabel.setAttribute("for", checkbox.id); | ||
693 | checkboxlabel.textContent = Pointer_stringify(title); | ||
694 | dlg_form.appendChild(checkboxlabel); | ||
695 | dlg_form.appendChild(document.createElement("br")); | ||
696 | |||
697 | dlg_return_funcs.push(function() { | ||
698 | dlg_return_ival(index, checkbox.checked ? 1 : 0); | ||
699 | }); | ||
700 | }, | ||
701 | |||
702 | /* | ||
703 | * void js_dialog_launch(void); | ||
704 | * | ||
705 | * Finish constructing a dialog, and actually display it, dimming | ||
706 | * everything else on the page. | ||
707 | */ | ||
708 | js_dialog_launch: function() { | ||
709 | // Put in the OK and Cancel buttons at the bottom. | ||
710 | var button; | ||
711 | |||
712 | button = document.createElement("input"); | ||
713 | button.type = "button"; | ||
714 | button.value = "OK"; | ||
715 | button.onclick = function(event) { | ||
716 | for (var i in dlg_return_funcs) | ||
717 | dlg_return_funcs[i](); | ||
718 | command(3); | ||
719 | } | ||
720 | dlg_form.appendChild(button); | ||
721 | |||
722 | button = document.createElement("input"); | ||
723 | button.type = "button"; | ||
724 | button.value = "Cancel"; | ||
725 | button.onclick = function(event) { | ||
726 | command(4); | ||
727 | } | ||
728 | dlg_form.appendChild(button); | ||
729 | |||
730 | document.body.appendChild(dlg_dimmer); | ||
731 | document.body.appendChild(dlg_form); | ||
732 | }, | ||
733 | |||
734 | /* | ||
735 | * void js_dialog_cleanup(void); | ||
736 | * | ||
737 | * Stop displaying a dialog, and clean up the internal state | ||
738 | * associated with it. | ||
739 | */ | ||
740 | js_dialog_cleanup: function() { | ||
741 | document.body.removeChild(dlg_dimmer); | ||
742 | document.body.removeChild(dlg_form); | ||
743 | dlg_dimmer = dlg_form = null; | ||
744 | onscreen_canvas.focus(); | ||
745 | }, | ||
746 | |||
747 | /* | ||
748 | * void js_focus_canvas(void); | ||
749 | * | ||
750 | * Return keyboard focus to the puzzle canvas. Called after a | ||
751 | * puzzle-control button is pressed, which tends to have the side | ||
752 | * effect of taking focus away from the canvas. | ||
753 | */ | ||
754 | js_focus_canvas: function() { | ||
755 | onscreen_canvas.focus(); | ||
756 | } | ||
757 | }); | ||