diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | README.md | 8 | ||||
-rw-r--r-- | lib/hitomezashi.c | 6 | ||||
-rw-r--r-- | lib/hitomezashi.h | 10 | ||||
-rw-r--r-- | lib/hitomezashi_utils.c | 2 | ||||
-rw-r--r-- | lib/hitomezashi_utils.h | 2 | ||||
-rw-r--r-- | web/hitomezashi_web.c | 169 | ||||
-rw-r--r-- | web/hitomezashi_web.h | 33 | ||||
-rw-r--r-- | web/hitomezashi_web_shell.html | 164 |
9 files changed, 178 insertions, 218 deletions
@@ -30,4 +30,4 @@ format-code: clang-format -i --verbose **/*.c **/*.h clean: - rm -f *.a *.o *.js *.wasm *.html hitomezashi_cli + rm -vf *.a *.o *.js *.wasm *.html hitomezashi_cli @@ -32,22 +32,16 @@ To build, run `CC=gcc make libhitomezashi.a`. This is in the `cli/` directory. Run `./hitomezashi_cli -h` for usage info. -Colours are supported! - -The output of the CLI app is a BMP image as this is supported by default in SDL2; this will change eventually to output PNGs since BMP is an antiquated format. For now, you can convert using FFmpeg or similar. - To build, run `CC=gcc make hitomezashi_cli`. ### Hitomezashi Web App -Try it: *link pending upload*. +Try it: *link pending*. To build, first run `make clean` if you previously built to native code, then (with Emscripten SDK in your PATH) run `emmake make hitomezashi_web.html`. You will need to serve, from a web server: `hitomezashi_web.html`, `hitomezashi_web.js`, `hitomezashi_web.wasm` in order for it to work; the `file://` protocol may not be sufficient. -The UI is self-explanatory. - ## Technical and copyright information Written in C11 with SDL2. Code style: `make format-code` will run `clang-format` with the correct parameters. diff --git a/lib/hitomezashi.c b/lib/hitomezashi.c index 9275517..5a3002d 100644 --- a/lib/hitomezashi.c +++ b/lib/hitomezashi.c @@ -7,9 +7,9 @@ SDL_Color HITOMEZASHI_BG_COLOUR = {.r = 255, .g = 255, .b = 255}; enum Hitomezashi_State_Init_Result hitomezashi_state_init(struct Hitomezashi_State *state, int x_pattern_len, - int y_pattern_len, char *x_pattern, char *y_pattern, - int gap, int line_thickness, Uint32 fg_colour, - Uint32 bg_colour) { + int y_pattern_len, const char *x_pattern, + const char *y_pattern, int gap, int line_thickness, + Uint32 fg_colour, Uint32 bg_colour) { assert(x_pattern_len >= 0); assert(y_pattern_len >= 0); assert(gap >= 0); diff --git a/lib/hitomezashi.h b/lib/hitomezashi.h index 51da75b..f1cf2fa 100644 --- a/lib/hitomezashi.h +++ b/lib/hitomezashi.h @@ -14,11 +14,11 @@ struct Hitomezashi_State { /** Pattern used to draw the vertical lines conceptually originating from the * x axis. This is an array of 0s and 1s. */ - char *x_pattern; + const char *x_pattern; /** Pattern used to draw the horizontal lines conceptually originating from * the y axis. This is an array of 0s and 1s. */ - char *y_pattern; + const char *y_pattern; /** Gap between parallel lines. */ int gap; @@ -57,9 +57,9 @@ enum Hitomezashi_Draw_Result { * hitomezashi_draw(). */ enum Hitomezashi_State_Init_Result hitomezashi_state_init(struct Hitomezashi_State *state, int x_pattern_len, - int y_pattern_len, char *x_pattern, char *y_pattern, - int gap, int line_thickness, Uint32 fg_colour, - Uint32 bg_colour); + int y_pattern_len, const char *x_pattern, + const char *y_pattern, int gap, int line_thickness, + Uint32 fg_colour, Uint32 bg_colour); /** Draw the hitomezashi pattern to state->surface. */ enum Hitomezashi_Draw_Result hitomezashi_draw(struct Hitomezashi_State *state); diff --git a/lib/hitomezashi_utils.c b/lib/hitomezashi_utils.c index 32832de..93ec850 100644 --- a/lib/hitomezashi_utils.c +++ b/lib/hitomezashi_utils.c @@ -3,7 +3,7 @@ #include <stddef.h> #include <stdlib.h> -char *hitomezashi_ascii_binary_str_to_ints(char *ascii_str, size_t len) { +char *hitomezashi_ascii_binary_str_to_ints(const char *ascii_str, size_t len) { char *res = malloc(len); for (int i = 0; i < len; ++i) { switch (ascii_str[i]) { diff --git a/lib/hitomezashi_utils.h b/lib/hitomezashi_utils.h index 040ea00..95a5a3f 100644 --- a/lib/hitomezashi_utils.h +++ b/lib/hitomezashi_utils.h @@ -12,6 +12,6 @@ * be freed**. * @param len Length of ascii_str, not including any terminating null bytes. */ -char *hitomezashi_ascii_binary_str_to_ints(char *ascii_str, size_t len); +char *hitomezashi_ascii_binary_str_to_ints(const char *ascii_str, size_t len); #endif // HITOMEZASHI_UTILS_H diff --git a/web/hitomezashi_web.c b/web/hitomezashi_web.c index 0a36a2c..413f727 100644 --- a/web/hitomezashi_web.c +++ b/web/hitomezashi_web.c @@ -1,120 +1,111 @@ #include "hitomezashi_web.h" - -#include <stdbool.h> -#include <stdio.h> -#include <string.h> - #include "SDL2/SDL.h" #include "emscripten.h" #include "hitomezashi.h" #include "hitomezashi_utils.h" +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> -int EMSCRIPTEN_KEEPALIVE main(void) { - char *param_str = hitomezashi_web_get_args(); +SDL_Window *window; - if (param_str[0] == 'i') { - return Hitomezashi_Web_Result_Err_Get_Params; - } +int EMSCRIPTEN_KEEPALIVE main(void) { + if (SDL_Init(SDL_INIT_VIDEO) != 0) + return EXIT_FAILURE; - char *x_pattern_str = strtok(param_str, "\n"); - int x_pattern_len = strlen(x_pattern_str); - // We needn't bother checking whether - // hitomezashi_ascii_binary_str_to_ints() was successful as the strings - // passed to it are validated by hitomezashi_web_get_args() in JS. - char *x_pattern = - hitomezashi_ascii_binary_str_to_ints(x_pattern_str, x_pattern_len); + // Stop SDL from capturing all input and stopping input elements from working + SDL_SetHint(SDL_HINT_EMSCRIPTEN_KEYBOARD_ELEMENT, "#canvas"); - char *y_pattern_str = strtok(NULL, "\n"); - int y_pattern_len = strlen(y_pattern_str); - char *y_pattern = - hitomezashi_ascii_binary_str_to_ints(y_pattern_str, y_pattern_len); + if (!(window = SDL_CreateWindow("", 0, 0, 100, 100, SDL_WINDOW_RESIZABLE))) + return EXIT_FAILURE; - int gap = strtol(strtok(NULL, "\n"), NULL, 0); - int line_thickness = strtol(strtok(NULL, "\n"), NULL, 0); + emscripten_set_main_loop(main_loop, -1, true); - Uint32 fg_colour = strtol(strtok(NULL, "\n"), NULL, 0); - Uint32 bg_colour = strtol(strtok(NULL, "\n"), NULL, 0); + return EXIT_SUCCESS; +} - if (SDL_Init(SDL_INIT_VIDEO) != 0) { - return Hitomezashi_Web_Result_Err_Sdl_Init; - } +void EMSCRIPTEN_KEEPALIVE main_loop(void) { + const char *x_pattern_raw = hitomezashi_web_get_x_pattern(); + const char *y_pattern_raw = hitomezashi_web_get_y_pattern(); + const int x_pattern_len = strlen(x_pattern_raw); + const int y_pattern_len = strlen(y_pattern_raw); + const char *x_pattern = + hitomezashi_ascii_binary_str_to_ints(x_pattern_raw, x_pattern_len); + const char *y_pattern = + hitomezashi_ascii_binary_str_to_ints(y_pattern_raw, y_pattern_len); - SDL_Window *window; - SDL_Renderer *renderer; - SDL_Texture *texture; + const int gap = hitomezashi_web_get_gap(); + const int line_thickness = hitomezashi_web_get_line_thickness(); - struct Hitomezashi_State state; - if (hitomezashi_state_init(&state, x_pattern_len, y_pattern_len, x_pattern, - y_pattern, gap, line_thickness, fg_colour, - bg_colour) != 0) { - return Hitomezashi_Web_Result_Err_State_Init; - } + const Uint32 fg_colour = hitomezashi_web_get_fg_colour(); + const Uint32 bg_colour = hitomezashi_web_get_bg_colour(); - // Stop SDL from capturing all input and stopping input elements from working - SDL_SetHint(SDL_HINT_EMSCRIPTEN_KEYBOARD_ELEMENT, "#canvas"); - SDL_CreateWindowAndRenderer(state.output_width, state.output_height, 0, - &window, &renderer); + // If any of these are 0, input was invalid + if (!(x_pattern_len && y_pattern_len && gap && line_thickness)) + return; + struct Hitomezashi_State state; + hitomezashi_state_init(&state, x_pattern_len, y_pattern_len, x_pattern, + y_pattern, gap, line_thickness, fg_colour, bg_colour); + SDL_SetWindowSize(window, state.output_width, state.output_height); hitomezashi_draw(&state); - texture = SDL_CreateTextureFromSurface(renderer, state.surface); - SDL_RenderClear(renderer); - SDL_RenderCopy(renderer, texture, NULL, NULL); - SDL_RenderPresent(renderer); - - // Since this is a short-lived application which is only run on page load, - // we do not need to delay the exit of main by freeing memory - it will be - // reclaimed by the browser automatically. + SDL_BlitSurface(state.surface, NULL, SDL_GetWindowSurface(window), NULL); + SDL_UpdateWindowSurface(window); - return Hitomezashi_Web_Result_Success; + free((char *)x_pattern_raw); + free((char *)y_pattern_raw); + free((char *)x_pattern); + free((char *)y_pattern); + SDL_FreeSurface(state.surface); } -EM_JS(char *, hitomezashi_web_get_args, (void), { - function validate_pattern(pattern) { - let valid = true; - for (let c of pattern) { - if (c != "0" && c != "1") { - valid = false; - break; - } +EM_JS(char *, hitomezashi_web_get_x_pattern, (void), { + let result_js = document.querySelector("input[name=\"x_pattern\"]").value; + for (let c of result_js) { + if (c != "0" && c != "1") { + result_js = ""; + break; } - return valid; } + const result_length_bytes = lengthBytesUTF8(result_js) + 1; + let result_wasm_heap = _malloc(result_length_bytes); + stringToUTF8(result_js, result_wasm_heap, result_length_bytes); + return result_wasm_heap; +}) - let result_js = ""; - - let search_params = new URLSearchParams(window.location.search); - - const x_pattern = search_params.get("x_pattern"); - const y_pattern = search_params.get("y_pattern"); - const gap = search_params.get("gap"); - const line_thickness = search_params.get("line_thickness"); - let fg_colour = search_params.get("fg_colour"); - let bg_colour = search_params.get("bg_colour"); - if (!fg_colour) { - fg_colour = "#000000"; - } - if (!bg_colour) { - bg_colour = "#FFFFFF"; - } - fg_colour = "0x" + fg_colour.slice(1); - bg_colour = "0x" + bg_colour.slice(1); - - if (!(x_pattern && y_pattern && gap && line_thickness)) { - result_js = "i"; - } else { - const patterns_are_valid = - validate_pattern(x_pattern) && validate_pattern(y_pattern); - if (!patterns_are_valid) { - result_js = "i"; - } else { - result_js = x_pattern + "\n" + y_pattern + "\n" + gap + "\n" + - line_thickness + "\n" + fg_colour + "\n" + bg_colour + "\n"; +EM_JS(char *, hitomezashi_web_get_y_pattern, (void), { + let result_js = document.querySelector("input[name=\"y_pattern\"]").value; + for (let c of result_js) { + if (c != "0" && c != "1") { + result_js = ""; + break; } } - const result_length_bytes = lengthBytesUTF8(result_js) + 1; let result_wasm_heap = _malloc(result_length_bytes); stringToUTF8(result_js, result_wasm_heap, result_length_bytes); return result_wasm_heap; }) + +EM_JS(unsigned int, hitomezashi_web_get_gap, (void), { + const result = +document.querySelector("input[name=\"gap\"").value; + return (result > 0 ? result : 0); +}) + +EM_JS(unsigned int, hitomezashi_web_get_line_thickness, (void), { + const result = +document.querySelector("input[name=\"line_thickness\"").value; + return (result > 0 ? result : 0); +}) + +EM_JS(Uint32, hitomezashi_web_get_fg_colour, (void), { + const result = parseInt( + document.querySelector("input[name=\"fg_colour\"]").value.slice(1), 16); + return (result ? result : 0); +}) + +EM_JS(Uint32, hitomezashi_web_get_bg_colour, (void), { + const result = parseInt( + document.querySelector("input[name=\"bg_colour\"]").value.slice(1), 16); + return (result ? result : 0); +}) diff --git a/web/hitomezashi_web.h b/web/hitomezashi_web.h index 361a8c2..c7db1c9 100644 --- a/web/hitomezashi_web.h +++ b/web/hitomezashi_web.h @@ -5,32 +5,13 @@ #include "SDL2/SDL.h" #include "emscripten.h" -enum Hitomezashi_Web_Result { - Hitomezashi_Web_Result_Success = 0, - Hitomezashi_Web_Result_Err_Get_Params = 1, - Hitomezashi_Web_Result_Err_Sdl_Init = 2, - Hitomezashi_Web_Result_Err_State_Init = 3, -}; +void main_loop(void); -int hitomezashi_web(int x_pattern_len, int y_pattern_len, char *x_pattern, - char *y_pattern, int gap, int line_thickness, - Uint32 fg_colour, Uint32 bg_colour); - -/** Parse the URL query string and return the parameters thereof. - * - * This is implemented in JS with EM_JS, in order to access the query string. - * The friction of interoperation is the reason for turning the query string - * into a different string that the C code must parse to the actual values, - * rather than sending the data back individually with the correct types. - * That is probably possible, but it seems unpleasant. - * - * The query string's x_pattern, y_pattern, gap, and line_thickness are - * parsed. If not all of them are present, "i" is returned. If x_pattern or - * y_pattern in the query string have characters other than ASCII digits 0 - * and 1, "i" is returned. Otherwise, the values are returned as a - * newline-delimited string in the order x_pattern, y_pattern, gap, - * line_thickness. - */ -char *hitomezashi_web_get_args(void); +char *hitomezashi_web_get_x_pattern(void); +char *hitomezashi_web_get_y_pattern(void); +unsigned int hitomezashi_web_get_gap(void); +unsigned int hitomezashi_web_get_line_thickness(void); +Uint32 hitomezashi_web_get_fg_colour(void); +Uint32 hitomezashi_web_get_bg_colour(void); #endif // HITOMEZASHI_WEB_H diff --git a/web/hitomezashi_web_shell.html b/web/hitomezashi_web_shell.html index a774f1e..e0e23d4 100644 --- a/web/hitomezashi_web_shell.html +++ b/web/hitomezashi_web_shell.html @@ -1,90 +1,84 @@ <!doctype html> <html lang="en-us"> - <head> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> - <!--This is code modified from https://github.com/emscripten-core/emscripten/blob/main/src/shell_minimal.html.--> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Hitomezashi</title> - <style> - canvas { - padding-right: 0; - margin-left: auto; - margin-right: auto; - display: block; - border: 0px none; - background-color: black; - } - form { - display: table; - margin: auto; - } - form > p { - display: table-row; - } - form > p > label { - display: table-cell; - } - form > p > label > input { - display: table-cell; - } - button#downloadCanvas { - display: block; - margin: 0 auto; - } - p#info { - text-align: center; - } - p#info > a { - margin-left: 1ex; - margin-right: 1ex; - } - </style> - </head> - <body> - <p id="info">See <a href="https://github.com/untir-l/hitomezashi/blob/main/README.md">the README</a> for information.</p> - <form name="hitomezashi parameters" autocomplete="off" method="get"> - <p><label>x pattern: <input name="x_pattern"></label></p> - <p><label>y pattern: <input name="y_pattern"></label></p> - <p><label>gap: <input name="gap" type="number"></label></p> - <p><label>line thickness: <input name="line_thickness" type="number"></label></p> - <p><label>foreground colour: <input name="fg_colour" type="color"></label></p> - <p><label>background colour: <input name="bg_colour" type="color"></label></p> - <p><input type="submit" value="submit"></p> - </form> - <br> - <canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas> - <br> - <button id="downloadCanvas">download as image</button> - <script type='text/javascript'> - // Make sure the WebGL context preserves the drawing buffer so the download button works - // Based on https://stackoverflow.com/a/43979935 - const canvas = document.querySelector("#canvas"); - canvas.getContext("webgl", {preserveDrawingBuffer: true}); - var Module = { - canvas: (function() { - canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false); - return canvas; - })(), - }; +<head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <!--This is code modified from https://github.com/emscripten-core/emscripten/blob/main/src/shell_minimal.html.--> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Hitomezashi</title> + <style> + canvas { + padding-right: 0; + margin-left: auto; + margin-right: auto; + display: block; + border: 0px none; + background-color: black; + } - // Set the input fields based on the query string so they appear sticky - const params = new URLSearchParams(window.location.search); - document.querySelector("input[name=\"x_pattern\"]").value = params.get("x_pattern"); - document.querySelector("input[name=\"y_pattern\"]").value = params.get("y_pattern"); - document.querySelector("input[name=\"gap\"]").value = params.get("gap"); - document.querySelector("input[name=\"line_thickness\"]").value = params.get("line_thickness"); - document.querySelector("input[name=\"fg_colour\"]").value = params.get("fg_colour"); - document.querySelector("input[name=\"bg_colour\"]").value = params.get("bg_colour"); + form { + display: table; + margin: auto; + } - function downloadCanvas() { - let downloadLink = document.createElement("a"); - downloadLink.setAttribute("download", "hitomezashi"); - downloadLink.setAttribute("href", canvas.toDataURL()); - downloadLink.click(); - } - document.querySelector("#downloadCanvas").addEventListener("click", downloadCanvas); - </script> - {{{ SCRIPT }}} - </body> -</html> + form>p { + display: table-row; + } + + form>p>label { + display: table-cell; + } + + form>p>label>input { + display: table-cell; + } + + button#downloadCanvas { + display: block; + margin: 0 auto; + } + + p#info { + text-align: center; + } + + p#info>a { + margin-left: 1ex; + margin-right: 1ex; + } + </style> +</head> + +<body> + <form name="hitomezashi parameters" autocomplete="off"> + <p><label>x pattern: <input name="x_pattern"></label></p> + <p><label>y pattern: <input name="y_pattern"></label></p> + <p><label>gap: <input name="gap" type="number"></label></p> + <p><label>line thickness: <input name="line_thickness" type="number"></label></p> + <p><label>foreground colour: <input name="fg_colour" type="color"></label></p> + <p><label>background colour: <input name="bg_colour" type="color"></label></p> + </form> + + <canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas> + <script type='text/javascript'> + var Module = { + canvas: (function () { + var canvas = document.getElementById('canvas'); + + canvas.addEventListener("webglcontextlost", function (e) { alert('Error: WebGL context lost. Please reload the page.'); e.preventDefault(); }, false); + + return canvas; + })(), + }; + function downloadCanvas() { + let downloadLink = document.createElement("a"); + downloadLink.setAttribute("download", "hitomezashi"); + downloadLink.setAttribute("href", canvas.toDataURL()); + downloadLink.click(); + } + document.querySelector("#downloadCanvas").addEventListener("click", downloadCanvas); + </script> + {{{ SCRIPT }}} +</body> + +</html>
\ No newline at end of file |