| /* | |
| * Copyright (C) 2026 Behdad Esfahbod | |
| * | |
| * This is part of HarfBuzz, a text shaping library. | |
| * | |
| * Permission is hereby granted, without written agreement and without | |
| * license or royalty fees, to use, copy, modify, and distribute this | |
| * software and its documentation for any purpose, provided that the | |
| * above copyright notice and the following two paragraphs appear in | |
| * all copies of this software. | |
| * | |
| * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR | |
| * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES | |
| * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN | |
| * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH | |
| * DAMAGE. | |
| * | |
| * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, | |
| * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND | |
| * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS | |
| * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO | |
| * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. | |
| * | |
| * Author(s): Behdad Esfahbod | |
| */ | |
| /* | |
| * Web (Emscripten) build of the hb-gpu demo. | |
| * | |
| * Reuses the desktop demo code with two changes: | |
| * 1. emscripten_set_main_loop replaces the blocking while loop | |
| * 2. HB_GPU_ATLAS_2D is defined (WebGL2 lacks texture buffers) | |
| * | |
| * Build: | |
| * source emsdk/emsdk_env.sh | |
| * ./util/gpu/web/build.sh | |
| * | |
| * Serve: | |
| * python3 -m http.server -d util/gpu/web/out | |
| */ | |
| #define HB_NO_PRAGMA_GCC_DIAGNOSTIC | |
| #include <emscripten.h> | |
| #include <emscripten/html5.h> | |
| /* Pull in the demo code — all headers are self-contained */ | |
| #include "../demo-common.h" | |
| #include "../demo-buffer.h" | |
| #include "../demo-font.h" | |
| #include "../demo-renderer.h" | |
| #include "../demo-view.h" | |
| #include "../default-text-combined.hh" | |
| #include "../default-text-en.hh" | |
| #include "../default-font.hh" | |
| static demo_renderer_t *renderer; | |
| static demo_view_t *vu; | |
| static demo_buffer_t *buffer; | |
| static demo_font_t *current_demo_font; | |
| static hb_blob_t *current_blob; | |
| static hb_face_t *current_face; | |
| static hb_font_t *current_font; | |
| static char *current_text; | |
| static bool custom_text; | |
| static void | |
| rebuild_buffer (const char *text) | |
| { | |
| if (!text) | |
| text = default_text_en; | |
| if (text != current_text) | |
| { | |
| /* strdup first -- text may alias current_text. */ | |
| char *new_text = strdup (text); | |
| free (current_text); | |
| current_text = new_text; | |
| } | |
| demo_font_clear_cache (current_demo_font); | |
| demo_atlas_clear (renderer->get_atlas ()); | |
| demo_buffer_clear (buffer); | |
| demo_point_t top_left = {0, 0}; | |
| demo_buffer_move_to (buffer, &top_left); | |
| if (current_text) | |
| demo_buffer_add_text (buffer, current_text, current_demo_font, 1); | |
| demo_view_reset (vu); | |
| /* Don't call demo_view_display here: it may run from a | |
| * microtask (e.g. the font-fetch .then() callback) where the | |
| * canvas back-buffer won't swap to the front. Leave | |
| * needs_redraw=true (set by demo_view_reset) so the next | |
| * main_loop_iter renders inside a proper rAF tick. */ | |
| } | |
| extern "C" { | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_load_font (const char *data, int len) | |
| { | |
| hb_blob_t *blob = hb_blob_create (data, len, | |
| HB_MEMORY_MODE_DUPLICATE, | |
| NULL, NULL); | |
| hb_face_t *face = hb_face_create (blob, 0); | |
| hb_font_t *font = hb_font_create (face); | |
| hb_blob_destroy (current_blob); | |
| hb_face_destroy (current_face); | |
| hb_font_destroy (current_font); | |
| current_blob = blob; | |
| current_face = face; | |
| current_font = font; | |
| /* Flush old font state */ | |
| demo_font_destroy (current_demo_font); | |
| current_demo_font = demo_font_create (font, renderer->get_atlas (), false); | |
| rebuild_buffer (custom_text ? current_text : default_text_en); | |
| demo_font_print_stats (current_demo_font); | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_set_text (const char *utf8) | |
| { | |
| custom_text = true; | |
| rebuild_buffer (utf8); | |
| } | |
| /* Apply a comma-separated list of variation settings | |
| * ("wght=400,wdth=100") to the current font, then rebuild | |
| * the buffer so the new axis values take effect. Cheap | |
| * to call repeatedly: suitable for live slider updates. */ | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_set_variations (const char *settings) | |
| { | |
| if (!current_font) return; | |
| /* Caps the axis count: no font ships close to this. */ | |
| hb_variation_t vars[32]; | |
| unsigned n = 0; | |
| const char *p = settings; | |
| while (p && *p && n < (sizeof vars / sizeof vars[0])) | |
| { | |
| const char *end = strchr (p, ','); | |
| int len = end ? (int) (end - p) : (int) strlen (p); | |
| if (hb_variation_from_string (p, len, &vars[n])) | |
| n++; | |
| p = end ? end + 1 : nullptr; | |
| } | |
| hb_font_set_variations (current_font, vars, n); | |
| rebuild_buffer (custom_text ? current_text : default_text_en); | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_set_features (const char *settings) | |
| { | |
| hb_feature_t feats[64]; | |
| unsigned n = 0; | |
| const char *p = settings; | |
| while (p && *p && n < 64) | |
| { | |
| const char *end = strchr (p, ','); | |
| int len = end ? (int) (end - p) : (int) strlen (p); | |
| if (hb_feature_from_string (p, len, &feats[n])) | |
| n++; | |
| p = end ? end + 1 : nullptr; | |
| } | |
| demo_buffer_set_features (feats, n); | |
| rebuild_buffer (custom_text ? current_text : default_text_en); | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_set_palette (unsigned palette_index) | |
| { | |
| if (!current_demo_font) return; | |
| demo_font_set_palette (current_demo_font, palette_index); | |
| rebuild_buffer (custom_text ? current_text : default_text_en); | |
| } | |
| EMSCRIPTEN_KEEPALIVE const char * | |
| web_get_text () | |
| { | |
| return current_text; | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_toggle_animation () | |
| { | |
| demo_view_key_func (vu, 32 /* GLFW_KEY_SPACE */, 0, 1, 0); | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_set_dark (int dark) | |
| { | |
| /* Toggle dark mode via the key handler if the current | |
| * state doesn't match the request. We can't check | |
| * vu->dark_mode directly (opaque type), so track it | |
| * locally. */ | |
| static bool is_dark = false; | |
| if ((!is_dark) == (!dark)) return; | |
| is_dark = !is_dark; | |
| demo_view_key_func (vu, 66 /* GLFW_KEY_B */, 0, 1, 0); | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_reset () | |
| { | |
| demo_view_reset (vu); | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_request_redraw () | |
| { | |
| demo_view_request_redraw (vu); | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_cancel_gesture () | |
| { | |
| demo_view_cancel_gesture (vu); | |
| } | |
| EMSCRIPTEN_KEEPALIVE void | |
| web_pinch (float pan_dx, float pan_dy, | |
| float zoom_factor, float angle_delta, | |
| float cx, float cy, int w, int h) | |
| { | |
| demo_view_pinch (vu, pan_dx, pan_dy, zoom_factor, angle_delta, cx, cy, w, h); | |
| } | |
| } /* extern "C" */ | |
| static void | |
| main_loop_iter () | |
| { | |
| glfwPollEvents (); | |
| if (demo_view_should_redraw (vu)) | |
| demo_view_display (vu, buffer); | |
| } | |
| static void | |
| framebuffer_size_func (GLFWwindow *window, int width, int height) | |
| { demo_view_reshape_func (vu, width, height); } | |
| static void | |
| key_func (GLFWwindow *window, int key, int scancode, int action, int mods) | |
| { demo_view_key_func (vu, key, scancode, action, mods); } | |
| static void | |
| char_func (GLFWwindow *window, unsigned int codepoint) | |
| { demo_view_char_func (vu, codepoint); } | |
| static void | |
| mouse_func (GLFWwindow *window, int button, int action, int mods) | |
| { demo_view_mouse_func (vu, button, action, mods); } | |
| static void | |
| scroll_func (GLFWwindow *window, double xoffset, double yoffset) | |
| { demo_view_scroll_func (vu, xoffset, yoffset); } | |
| static void | |
| cursor_func (GLFWwindow *window, double x, double y) | |
| { demo_view_motion_func (vu, x, y); } | |
| static const char *arg_text = nullptr; | |
| static const char *arg_font = nullptr; | |
| int | |
| main (int argc, char **argv) | |
| { | |
| /* Accept --text=... and --font=... so the JS shell can forward | |
| * URL parameters. Everything else is ignored. */ | |
| for (int i = 1; i < argc; i++) | |
| { | |
| if (!strncmp (argv[i], "--text=", 7)) | |
| arg_text = argv[i] + 7; | |
| else if (!strncmp (argv[i], "--font=", 7)) | |
| arg_font = argv[i] + 7; | |
| } | |
| if (!glfwInit ()) | |
| { | |
| fprintf (stderr, "Failed to initialize GLFW\n"); | |
| return 1; | |
| } | |
| glfwWindowHint (GLFW_CONTEXT_VERSION_MAJOR, 3); | |
| glfwWindowHint (GLFW_CONTEXT_VERSION_MINOR, 0); | |
| glfwWindowHint (GLFW_CLIENT_API, GLFW_OPENGL_ES_API); | |
| /* Query actual canvas size from the browser. */ | |
| int canvas_w = 700, canvas_h = 700; | |
| emscripten_get_canvas_element_size ("#canvas", &canvas_w, &canvas_h); | |
| GLFWwindow *window = glfwCreateWindow (canvas_w, canvas_h, "HarfBuzz GPU Demo", NULL, NULL); | |
| if (!window) | |
| { | |
| glfwTerminate (); | |
| fprintf (stderr, "Failed to create GLFW window\n"); | |
| return 1; | |
| } | |
| glfwMakeContextCurrent (window); | |
| glfwSetFramebufferSizeCallback (window, framebuffer_size_func); | |
| glfwSetKeyCallback (window, key_func); | |
| glfwSetCharCallback (window, char_func); | |
| glfwSetMouseButtonCallback (window, mouse_func); | |
| glfwSetScrollCallback (window, scroll_func); | |
| glfwSetCursorPosCallback (window, cursor_func); | |
| { | |
| int fb_width, fb_height; | |
| glfwGetFramebufferSize (window, &fb_width, &fb_height); | |
| glViewport (0, 0, fb_width, fb_height); | |
| } | |
| renderer = demo_renderer_create_gl (window, false); | |
| vu = demo_view_create (renderer, window); | |
| current_blob = hb_blob_create ((const char *) default_font, | |
| sizeof (default_font), | |
| HB_MEMORY_MODE_READONLY, | |
| NULL, NULL); | |
| current_face = hb_face_create (current_blob, 0); | |
| current_font = hb_font_create (current_face); | |
| current_demo_font = demo_font_create (current_font, renderer->get_atlas (), false); | |
| current_text = strdup (arg_text ? arg_text : default_text_combined); | |
| if (arg_text) | |
| custom_text = true; | |
| buffer = demo_buffer_create (); | |
| demo_point_t top_left = {0, 0}; | |
| demo_buffer_move_to (buffer, &top_left); | |
| /* When ?font= is pending, skip initial glyph upload -- the | |
| * async font load will replace the font anyway, and we'd | |
| * upload defaults-into-atlas just to throw them away. Buffer | |
| * stays empty until web_load_font's rebuild_buffer runs. */ | |
| if (!arg_font) | |
| demo_buffer_add_text (buffer, current_text, current_demo_font, 1); | |
| demo_font_print_stats (current_demo_font); | |
| demo_view_setup (vu); | |
| emscripten_set_main_loop (main_loop_iter, 0, 1); | |
| /* Not reached */ | |
| return 0; | |
| } |