Mercurial
view infinite_canvas/web_surface_web.c @ 276:b55c22cff335
Add interactive infinite canvas prototype
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 16:57:56 -0700 |
| parents | |
| children | 8d560f50ed4c |
line wrap: on
line source
#include "infinite_canvas/web_surface.h" #include <emscripten/emscripten.h> #include <string.h> EM_JS(void, Canvas_Web_Begin_Frame, (), { document.querySelectorAll("[data-canvas-web-surface]").forEach((surface) => { surface.dataset.canvasActive = "false"; }); }); EM_JS(void, Canvas_Web_Create_Or_Update, ( uint32 entity_id, const char *p_url, float x, float y, float width, float height), { const canvas = Module.canvas; const surfaceId = `canvas-web-surface-${entity_id}`; let surface = document.getElementById(surfaceId); if (!surface) { surface = document.createElement("div"); surface.id = surfaceId; surface.dataset.canvasWebSurface = "true"; surface.style.position = "fixed"; surface.style.zIndex = "4"; surface.style.overflow = "hidden"; surface.style.borderRadius = "8px"; surface.style.background = "#fff"; const frame = document.createElement("iframe"); frame.title = `Canvas web content ${entity_id}`; frame.referrerPolicy = "strict-origin-when-cross-origin"; frame.sandbox = "allow-downloads allow-forms allow-modals allow-popups allow-same-origin allow-scripts"; frame.style.width = "100%"; frame.style.height = "100%"; frame.style.border = "0"; frame.style.display = "block"; surface.appendChild(frame); document.body.appendChild(surface); } surface.dataset.canvasActive = "true"; const frame = surface.firstElementChild; const url = UTF8ToString(p_url); if (frame.dataset.url !== url) { frame.dataset.url = url; frame.src = url; } const canvasBounds = canvas.getBoundingClientRect(); const scaleX = canvasBounds.width / Math.max(1, canvas.width); const scaleY = canvasBounds.height / Math.max(1, canvas.height); surface.style.left = `${canvasBounds.left + x * scaleX}px`; surface.style.top = `${canvasBounds.top + y * scaleY}px`; surface.style.width = `${Math.max(1, width * scaleX)}px`; surface.style.height = `${Math.max(1, height * scaleY)}px`; }); EM_JS(void, Canvas_Image_Create_Or_Update, ( uint32 entity_id, const char *p_source, float x, float y, float width, float height), { const canvas = Module.canvas; const surfaceId = `canvas-web-surface-${entity_id}`; let surface = document.getElementById(surfaceId); if (!surface) { surface = document.createElement("div"); surface.id = surfaceId; surface.dataset.canvasWebSurface = "true"; surface.style.position = "fixed"; surface.style.zIndex = "4"; surface.style.overflow = "hidden"; surface.style.borderRadius = "8px"; surface.style.pointerEvents = "none"; const image = document.createElement("img"); image.alt = "Canvas image"; image.style.width = "100%"; image.style.height = "100%"; image.style.objectFit = "contain"; image.style.display = "block"; surface.appendChild(image); document.body.appendChild(surface); } surface.dataset.canvasActive = "true"; const image = surface.firstElementChild; const source = UTF8ToString(p_source); if (image.dataset.url !== source) { if (image.dataset.blobUrl) URL.revokeObjectURL(image.dataset.blobUrl); image.dataset.url = source; delete image.dataset.blobUrl; if (/^(https?:|data:|blob:)/i.test(source)) { image.src = source; } else { try { const bytes = FS.readFile(source); const extension = source.split(".").pop().toLowerCase(); const mime = extension === "svg" ? "image/svg+xml" : extension === "png" ? "image/png" : extension === "webp" ? "image/webp" : extension === "gif" ? "image/gif" : "image/jpeg"; const blobUrl = URL.createObjectURL(new Blob([bytes], {type: mime})); image.dataset.blobUrl = blobUrl; image.src = blobUrl; } catch (error) { image.src = source; } } } const canvasBounds = canvas.getBoundingClientRect(); const scaleX = canvasBounds.width / Math.max(1, canvas.width); const scaleY = canvasBounds.height / Math.max(1, canvas.height); surface.style.left = `${canvasBounds.left + x * scaleX}px`; surface.style.top = `${canvasBounds.top + y * scaleY}px`; surface.style.width = `${Math.max(1, width * scaleX)}px`; surface.style.height = `${Math.max(1, height * scaleY)}px`; }); EM_JS(void, Canvas_Web_End_Frame, (), { document.querySelectorAll("[data-canvas-web-surface]").forEach((surface) => { if (surface.dataset.canvasActive !== "true") { const content = surface.firstElementChild; if (content && content.dataset.blobUrl) { URL.revokeObjectURL(content.dataset.blobUrl); } surface.remove(); } }); }); EM_JS(void, Canvas_Web_Destroy, (), { document.querySelectorAll("[data-canvas-web-surface]").forEach((surface) => { const content = surface.firstElementChild; if (content && content.dataset.blobUrl) { URL.revokeObjectURL(content.dataset.blobUrl); } surface.remove(); }); }); static Rectangle Canvas_Web_Content_Bounds( const Canvas_Camera *p_camera, const Canvas_Entity *p_entity) { float frame_inset = CANVAS_WEB_FRAME_INSET / p_camera->zoom; float resize_handle = CANVAS_WEB_RESIZE_HANDLE / p_camera->zoom; Vector2 position = Canvas_Camera_World_To_Screen( p_camera, (Vector2){ p_entity->position.x + frame_inset, p_entity->position.y + frame_inset, }); return (Rectangle){ position.x, position.y, (p_entity->size.x - frame_inset - resize_handle) * p_camera->zoom, (p_entity->size.y - frame_inset - resize_handle) * p_camera->zoom, }; } static boolean Canvas_Web_Is_Visible( const Canvas_Camera *p_camera, Rectangle bounds) { return p_camera->zoom >= 0.30f && bounds.x + bounds.width > 0.0f && bounds.y + bounds.height > 0.0f && bounds.x < (float)p_camera->viewport_width && bounds.y < (float)p_camera->viewport_height; } int32 Canvas_Web_Surface_Execute_Subprocess(int argc, char **p_argv) { (void)argc; (void)p_argv; return -1; } boolean Canvas_Web_Surface_Init( Canvas_Web_Surface *p_surface, Dowa_Arena *p_arena) { memset(p_surface, 0, sizeof(*p_surface)); p_surface->p_arena = p_arena; p_surface->initialized = TRUE; return TRUE; } boolean Canvas_Web_Surface_Update_Input( Canvas_Web_Surface *p_surface, const Canvas_Camera *p_camera, const Canvas_Scene *p_scene) { (void)p_surface; (void)p_camera; (void)p_scene; return FALSE; } void Canvas_Web_Surface_Sync( Canvas_Web_Surface *p_surface, const Canvas_Camera *p_camera, const Canvas_Scene *p_scene) { if (!p_surface->initialized) return; Canvas_Web_Begin_Frame(); uint32 visible_views = 0; for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities) && visible_views < CANVAS_MAX_WEB_VIEWS; index++) { const Canvas_Entity *p_entity = &p_scene->p_entities[index]; if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT && p_entity->type != CANVAS_ENTITY_IMAGE) { continue; } Rectangle bounds = Canvas_Web_Content_Bounds(p_camera, p_entity); if (!Canvas_Web_Is_Visible(p_camera, bounds)) continue; if (p_entity->type == CANVAS_ENTITY_IMAGE) { Canvas_Image_Create_Or_Update( p_entity->id, p_entity->text, bounds.x, bounds.y, bounds.width, bounds.height); } else { Canvas_Web_Create_Or_Update( p_entity->id, p_entity->text, bounds.x, bounds.y, bounds.width, bounds.height); } visible_views++; } Canvas_Web_End_Frame(); } void Canvas_Web_Surface_Draw( const Canvas_Web_Surface *p_surface, const Canvas_Camera *p_camera, const Canvas_Scene *p_scene) { (void)p_surface; (void)p_camera; (void)p_scene; } boolean Canvas_Web_Surface_Is_Focused(const Canvas_Web_Surface *p_surface) { (void)p_surface; return FALSE; } void Canvas_Web_Surface_Shutdown(Canvas_Web_Surface *p_surface) { if (!p_surface->initialized) return; Canvas_Web_Destroy(); p_surface->initialized = FALSE; }