Mercurial
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/infinite_canvas/web_surface_web.c Mon Aug 17 16:57:56 2026 -0700 @@ -0,0 +1,270 @@ +#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; +}