By default, CSS uses "object-fit: fill", which means that an object is
independently scaled in both dimensions to fit its containing box.
This is simpler than what I'd assumed (which was "object-fill:
contain"). Obviously, the HTML could be changed to use a different
object-fit, in which case this code would have to detect it, but for
now following the CSS default is more correct than not.
This is so that (given time for caches to expire) I can switch to having
a persistent dialogue box in HTML rather than fabricating it from
scratch in JavaScript each time it's used.
I expect Escape to exit the menu, and SoftRight should do that as well
for KaiOS. Backspace goes up one level through the menus, again because
that's conventional on KaiOS and not too confusing elsewhere.
In the bubbling phase it managed to catch the "Enter" keypress that
opened a dialogue box from the menu and use it to close the dialogue
box again. I think it's probably reasonable to have it run earlier and
just permanently steal any keypresses it wants.
When we disable a button, it loses focus but doesn't generate a "blur"
event. This means our "focus-within" class goes wrong. Instead of
relying on "blur" events to remove the class, remove it from any
inappropriate elements in the "focus" handler. This requires attaching
the handler to the root element of the document, but I've got plans that
need that anyway.
Old browsers (like KaiOS 2.5) don't have :focus-within, but it's pretty
easy to replace the pseudo-class with a real .focus-within class
maintained by JavaScript event handlers. This is made only marginally
fiddlier by the odd fact that "focus" and "blur" events don't bubble.
Once the input focus is in the menu system (for instance by Shift+Tab
from the puzzle), you can move left and right through the menu bar and
up and down within each menu. Enter selects a menu item. The current
menu item is tracked by giving it the input focus.
I'm generally in favour of putting HTML in HTML rather the constructing
it in JavaScript, and this will allow for simplifying the code
eventually. This only changes the JavaScript to make sure that's in
people's caches before I change the HTML itself.
The C code in the Emscripten front-end already keeps a timer_active
variable to ensure that the timer can be activated only when it's
inactive, and deactivated only when it's active. Adjusting the
JavaScript side to rely on this makes the code much simpler. The only
oddity is that it now requests a new animation frame before calling the
callback so that it's ready to be cancelled if the callback decides to
deactivate the timer.
I think this has been broken since a752e73, when the canvas changed to
being hidden, and hence unable to receive keyboard focus, when the page
loaded. I've now moved the focus() call to after the canvas gets
displayed.
They can now be specified by sticking some JSON in a <script> element in
the Web page:
<script id="environment" type="application/json">
{ "LOOPY_DEFAULT": "20x10t11dh" }
</script>
This isn't brilliantly useful, but it does allow for changing settings
without recompiling.
Presets are now radio buttons with labels, and menu items that take
actions are now buttons. The <li> representing each menu item is now a
thin wrapper around another element: a <label> for radio buttons, a
<button> for other buttons, and a <div> for submenu headings. All of
the things that previously applied to the <li> now apply to that inner
element instead.
This means that presets can now use the standard "checked" attribute to
indicate which one is selected, and buttons can be disabled using the
standard "disabled" attribute. It also means that we can query and set
the state of all the presets at once through their RadioNodeList.
I think this should also make the menus more accessible, and make it
easier to make them keyboard-controllable.
... and then decide there was no excuse for renaming the variable, so
now it has the same name it had before I started using
Window.requestAnimationFrame().
This is an API specifically designed for the purposes of timing
animations. Unlike setInterval, it tries to synchronise with the screen
refresh rate. It naturally passes us timing information, saving the
need to construct a Date object every frame. It has the nice feature
that browsers (at least Firefox 91) will call it less frequently when
the puzzle page isn't visible, which saves CPU time in puzzles that run
a timer continuously.
With very small tile sizes, js_canvas_find_font_midpoint() can throw an
exception. When it was called from update_pixel_ratio(), this prevented
the new MediaQueryList from being created, which meant that the puzzle
stopped noticing changes of device pixel ratio.
Now update_pixel_ratio() establishes a new MediaQueryList before calling
rescale_puzzle(), so the exception can't break it. Catching the
exception properly would be even better, of course.
This adds a new callback, rescale_puzzle(), that's called when the
device pixel ratio changes. This means that resize_puzzle() can safely
set the nominal canvas size, which means that manual resizing of the
puzzle now sticks.
Still missing: paying attention to the device pixel ratio when choosing
the initial (or reset) size.
This has the entertaining consequence that repeatedly zooming in and out
causes puzzles to gradually shrink, thus demonstrating that recording
the nominal size correctly will be necessary.
Despite my stylistic downgrades, it still used two features not present
in Firefox 48, and hence KaiOS 2.5: passing options to addEventListener,
and calling addEventListener on a MediaQueryList at all. Now it uses
the older addListener method and explicitly removes each listener as
soon as it's called.
Now we only cancel a keydown event if the C keyboard handler recognises
the key and passes it on to the midend. This doesn't necessarily mean
that the midend has actually done anything with it, of course. Still,
this is enough to allow F12 to open the developer tools even when the
input focus is on the puzzle. It also allows for tabbing out of the
puzzle into the links below it.
At least in modern browsers (and I suspect in all browsers), cancelling
a keydown event ensures that the subsequent keypress event doesn't fire.
See <https://w3c.github.io/uievents/#keys-cancelable-keys>.
So there's no point in having a handler on keypress events that just
tries to cancel them as well. Removing the handler doesn't do much now,
but it opens the possibility of being a bit more selective about which
keydown events we cancel.
This requires passing in KeyboardEvent.location from JavaScript so
that we can detect the numeric keypad properly. Out of caution we
currently only set MOD_NUM_KEYPAD on numbers, like we always have,
but we have enough information to set it on arrow keys, Enter, "+",
etc.
This finally gets '/' and '\' working in Slant again.
Stealing code from the MDN has the consequence that it uses shiny ES6
features like "const", "let", and "=>". This looks a bit odd among
the more conservative style of the rest of Puzzles, so I've
downgraded it to "var" and "function". I'll let the template string
stay because that actually helps readability.
This requires looking at the CSS size of the puzzle canvas rather than
its internal size, and then adjusting the new size to account for the
device pixel ratio.
Because it's the simplest thing to do, when we notice such a change we
keep the current puzzle at its existing size measured in device
pixels. This has the rather odd consequence that when changing the
text size in Firefox, the size of the puzzle remains constant.
Our system for mapping mouse coordinates to canvas coordinates assumed
that the puzzle canvas had the same dimensions in CSS as its own
internal width and height. This is true in the current wrapper HTML,
but it's very easy to accidentally change and there are circumstances
where we might want to deliberately change it in future.
To fix this, we now inspect the CSS size of the canvas when processing
mouse events, and map the coordinates through the scaling and
translation necessary to convert CSS pixels into canvas pixels.
This is necessary to allow all random seeds to round-trip properly.
It's probably not currently necessary for descriptive game IDs, but it
won't hurt.
I've deliberately gone for encoding only those characters that are not
valid in fragment identifiers to minimise the ugliness of the generated
URLs. For slightly interesting historical reasons, '#' is not valid in
a fragment identifier, so all random seed links end up a little bit
ugly.
Now that save files are (even more) officially ASCII, it's perfectly
safe to pass them to JavaScript as UTF-8 strings.
This means that the form in which save files are shipped from C to
JavaScript is the same is the form in which they're shipped from
JavaScript to C. That allows for doing new things with them, like
writing them to local storage.
This reverts commit f729f51e475ff98d0caf529f0723ef810b1c88ef.
This removes any assumption in the JavaScript code about precisely what
"display" setting the element should have.
This means that now the only places where the JavaScript manipulates
elements' styles are to set the width of the puzzle container and to
mark and unmark elements with "display: none". These both seem like
reasonable kinds of semantic markup that just happen to be expressed as
styles.
Some elements (generally those created by JavaScript) had their style
parameters set directly by JavaScript. Putting styles in CSS generally
makes them easier to understand (and fiddle with), so I've done that.
The only styles left in JavaScript are those that are calculated by
JavaScript (like the status-bar size) and the random-seed permalink
visibility because I wasn't quite sure how to handle it.
The previous fix worked OK, but it was conceptually wrong. Puzzles
save files are better regarded as binary, not text: the length fields
are measured in bytes, so translating the file into a different
multibyte character encoding would invalidate them.
So it was wrong to fetch a C byte string containing the exactly right
binary data, then translate it into a Javascript string as if decoding
from UTF-8, then retranslate to a representation of a bytewise
encoding via encodeURIComponent, and then label the result as
application/octet-stream.
This probably wouldn't have caused any problems in practice, because I
don't remember any situation in which my save files use characters
outside printable ASCII (plus newline). But it's not actually
forbidden, so a save file might choose to do that some day, so that
UTF-8 decode/reencode hidden in the JS was a latent bug.
Now the URI-encoding is done on the C side, while we still know
exactly what the binary data ought to look like and can be sure we're
translating it byte for byte into the output encoding for the data:
URI. By the time the JS receives a string pointer from get_save_file,
it's already URI-encoded, which _guarantees_ that it's in ASCII and
won't be messed about with by Emscripten's UTF8ToString.
In commit f6434e84964d840 I said I had replaced all uses of
old-Emscripten's Pointer_stringify() function with new-Emscripten's
UTF8ToString(). In fact, I only replaced the ones in emcclib.js, but I
missed one in emccpre.js used in preparing downloadable save files.
Those were therefore broken, with a Javascript undefined-name error.
Using a stunt webserver which artificially introduces a 3s delay just
before the last line of the HTML output, I have reproduced a
uwer-reported loading/startup race bug:
Previously the wasm loading was started by the <script> element,
synchronously. If the wasm loading is fast, and finishes before the
HTML loading, the onRuntimeInitialized event may occur before
initPuzzles. But initPuzzles sets up the event handler.
Fix this bug, and introduce a new comment containing an argument for
the correctness of the new approach.
Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
I presume this will improve performance. Also, if I've understood
correctly, WASM-based compiled web code is capable of automatically
growing its memory, which the previous asm.js build of the puzzles
could not do, and occasionally caused people to complain that if they
tried to play a _really big_ game in their browser, the JS would
eventually freeze because the emulated memory ran out.
I've been putting off doing this for ages because my previous
Emscripten build setup was so finicky that I didn't like to meddle
with it. But now that the new cmake system in this source tree makes
things generally easier, and particularly since I've just found out
that the up-to-date Emscripten is available as a Docker image (namely
"emscripten/emsdk"), this seemed like a good moment to give it a try.
The source and build changes required for this update weren't too
onerous. I was half expecting a huge API upheaval, and indeed there
was _some_ change, but very little:
- in the JS initPuzzle function, move the call to Module.callMain()
into Module.onRuntimeInitialized instead of doing it at the top
level, because New Emscripten's .js output likes to load the
accompanying .wasm file asynchronously, so you can't call the WASM
main() until it actually exists.
- in the JS-side library code, replace all uses of Emscripten's
Pointer_stringify() function with the new name UTF8ToString(). (The
new version also has an ASCIIToString(), so I guess the reason for
the name change is that now you get to choose which character set
you meant. I need to use UTF-8, so that the × and ÷ signs in Keen
will work.)
- set EXTRA_EXPORTED_RUNTIME_METHODS=[cwrap,callMain] on the emcc
link command line, otherwise they aren't available for my JS setup
code to call.
- (removed -s ASM_JS=1 from the link options, though I'm not actually
sure it made any difference one way or the other in the new WASM
world)
- be prepared for a set of .wasm files to show up as build products
alongside the .js ones.
- stop building with -DCMAKE_BUILD_TYPE=Release! I'm not sure why
that was needed, but if I leave that flag on my cmake command line,
the output .js file fails to embed my emccpre.js, so the initial
call to initPuzzle() fails from the HTML wrapper page, meaning
nothing at all happens.
In other front ends, Shift-click is an alternative to the middle
button, and Ctrl-click the right button. Apparently I completely
forgot to implement this in the JS front end. Better late than never.
This is done by showing a dialog containing an <input type="file">
through which the user can 'upload' a save file - though, of course,
the 'upload' doesn't go to any HTTP server, but only into the mind of
the Javascript running in the same browser.
It would be even nicer to support drag-and-drop as an alternative UI
for getting the save file into the browser, but that isn't critical to
getting the first version of this feature out of the door.
This is done by getting midend_serialise to produce the complete
saved-game file as an in-memory string buffer, and then encoding that
into a data: URI which we provide to the user as a hyperlink in a
dialog box. The hyperlink has the 'download' attribute, which means
clicking on it should automatically offer to save the file, and also
lets me specify a not-too-silly default file name.