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;
+}