Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 274:c9be578316a6 | 276:b55c22cff335 |
|---|---|
| 1 #include "infinite_canvas/web_surface.h" | |
| 2 | |
| 3 #include <emscripten/emscripten.h> | |
| 4 #include <string.h> | |
| 5 | |
| 6 EM_JS(void, Canvas_Web_Begin_Frame, (), { | |
| 7 document.querySelectorAll("[data-canvas-web-surface]").forEach((surface) => { | |
| 8 surface.dataset.canvasActive = "false"; | |
| 9 }); | |
| 10 }); | |
| 11 | |
| 12 EM_JS(void, Canvas_Web_Create_Or_Update, ( | |
| 13 uint32 entity_id, | |
| 14 const char *p_url, | |
| 15 float x, | |
| 16 float y, | |
| 17 float width, | |
| 18 float height), { | |
| 19 const canvas = Module.canvas; | |
| 20 const surfaceId = `canvas-web-surface-${entity_id}`; | |
| 21 let surface = document.getElementById(surfaceId); | |
| 22 if (!surface) { | |
| 23 surface = document.createElement("div"); | |
| 24 surface.id = surfaceId; | |
| 25 surface.dataset.canvasWebSurface = "true"; | |
| 26 surface.style.position = "fixed"; | |
| 27 surface.style.zIndex = "4"; | |
| 28 surface.style.overflow = "hidden"; | |
| 29 surface.style.borderRadius = "8px"; | |
| 30 surface.style.background = "#fff"; | |
| 31 | |
| 32 const frame = document.createElement("iframe"); | |
| 33 frame.title = `Canvas web content ${entity_id}`; | |
| 34 frame.referrerPolicy = "strict-origin-when-cross-origin"; | |
| 35 frame.sandbox = "allow-downloads allow-forms allow-modals allow-popups allow-same-origin allow-scripts"; | |
| 36 frame.style.width = "100%"; | |
| 37 frame.style.height = "100%"; | |
| 38 frame.style.border = "0"; | |
| 39 frame.style.display = "block"; | |
| 40 surface.appendChild(frame); | |
| 41 document.body.appendChild(surface); | |
| 42 } | |
| 43 | |
| 44 surface.dataset.canvasActive = "true"; | |
| 45 const frame = surface.firstElementChild; | |
| 46 const url = UTF8ToString(p_url); | |
| 47 if (frame.dataset.url !== url) { | |
| 48 frame.dataset.url = url; | |
| 49 frame.src = url; | |
| 50 } | |
| 51 | |
| 52 const canvasBounds = canvas.getBoundingClientRect(); | |
| 53 const scaleX = canvasBounds.width / Math.max(1, canvas.width); | |
| 54 const scaleY = canvasBounds.height / Math.max(1, canvas.height); | |
| 55 surface.style.left = `${canvasBounds.left + x * scaleX}px`; | |
| 56 surface.style.top = `${canvasBounds.top + y * scaleY}px`; | |
| 57 surface.style.width = `${Math.max(1, width * scaleX)}px`; | |
| 58 surface.style.height = `${Math.max(1, height * scaleY)}px`; | |
| 59 }); | |
| 60 | |
| 61 EM_JS(void, Canvas_Image_Create_Or_Update, ( | |
| 62 uint32 entity_id, | |
| 63 const char *p_source, | |
| 64 float x, | |
| 65 float y, | |
| 66 float width, | |
| 67 float height), { | |
| 68 const canvas = Module.canvas; | |
| 69 const surfaceId = `canvas-web-surface-${entity_id}`; | |
| 70 let surface = document.getElementById(surfaceId); | |
| 71 if (!surface) { | |
| 72 surface = document.createElement("div"); | |
| 73 surface.id = surfaceId; | |
| 74 surface.dataset.canvasWebSurface = "true"; | |
| 75 surface.style.position = "fixed"; | |
| 76 surface.style.zIndex = "4"; | |
| 77 surface.style.overflow = "hidden"; | |
| 78 surface.style.borderRadius = "8px"; | |
| 79 surface.style.pointerEvents = "none"; | |
| 80 | |
| 81 const image = document.createElement("img"); | |
| 82 image.alt = "Canvas image"; | |
| 83 image.style.width = "100%"; | |
| 84 image.style.height = "100%"; | |
| 85 image.style.objectFit = "contain"; | |
| 86 image.style.display = "block"; | |
| 87 surface.appendChild(image); | |
| 88 document.body.appendChild(surface); | |
| 89 } | |
| 90 | |
| 91 surface.dataset.canvasActive = "true"; | |
| 92 const image = surface.firstElementChild; | |
| 93 const source = UTF8ToString(p_source); | |
| 94 if (image.dataset.url !== source) { | |
| 95 if (image.dataset.blobUrl) URL.revokeObjectURL(image.dataset.blobUrl); | |
| 96 image.dataset.url = source; | |
| 97 delete image.dataset.blobUrl; | |
| 98 if (/^(https?:|data:|blob:)/i.test(source)) { | |
| 99 image.src = source; | |
| 100 } else { | |
| 101 try { | |
| 102 const bytes = FS.readFile(source); | |
| 103 const extension = source.split(".").pop().toLowerCase(); | |
| 104 const mime = extension === "svg" ? "image/svg+xml" : | |
| 105 extension === "png" ? "image/png" : | |
| 106 extension === "webp" ? "image/webp" : | |
| 107 extension === "gif" ? "image/gif" : "image/jpeg"; | |
| 108 const blobUrl = URL.createObjectURL(new Blob([bytes], {type: mime})); | |
| 109 image.dataset.blobUrl = blobUrl; | |
| 110 image.src = blobUrl; | |
| 111 } catch (error) { | |
| 112 image.src = source; | |
| 113 } | |
| 114 } | |
| 115 } | |
| 116 | |
| 117 const canvasBounds = canvas.getBoundingClientRect(); | |
| 118 const scaleX = canvasBounds.width / Math.max(1, canvas.width); | |
| 119 const scaleY = canvasBounds.height / Math.max(1, canvas.height); | |
| 120 surface.style.left = `${canvasBounds.left + x * scaleX}px`; | |
| 121 surface.style.top = `${canvasBounds.top + y * scaleY}px`; | |
| 122 surface.style.width = `${Math.max(1, width * scaleX)}px`; | |
| 123 surface.style.height = `${Math.max(1, height * scaleY)}px`; | |
| 124 }); | |
| 125 | |
| 126 EM_JS(void, Canvas_Web_End_Frame, (), { | |
| 127 document.querySelectorAll("[data-canvas-web-surface]").forEach((surface) => { | |
| 128 if (surface.dataset.canvasActive !== "true") { | |
| 129 const content = surface.firstElementChild; | |
| 130 if (content && content.dataset.blobUrl) { | |
| 131 URL.revokeObjectURL(content.dataset.blobUrl); | |
| 132 } | |
| 133 surface.remove(); | |
| 134 } | |
| 135 }); | |
| 136 }); | |
| 137 | |
| 138 EM_JS(void, Canvas_Web_Destroy, (), { | |
| 139 document.querySelectorAll("[data-canvas-web-surface]").forEach((surface) => { | |
| 140 const content = surface.firstElementChild; | |
| 141 if (content && content.dataset.blobUrl) { | |
| 142 URL.revokeObjectURL(content.dataset.blobUrl); | |
| 143 } | |
| 144 surface.remove(); | |
| 145 }); | |
| 146 }); | |
| 147 | |
| 148 static Rectangle Canvas_Web_Content_Bounds( | |
| 149 const Canvas_Camera *p_camera, | |
| 150 const Canvas_Entity *p_entity) | |
| 151 { | |
| 152 float frame_inset = CANVAS_WEB_FRAME_INSET / p_camera->zoom; | |
| 153 float resize_handle = CANVAS_WEB_RESIZE_HANDLE / p_camera->zoom; | |
| 154 Vector2 position = Canvas_Camera_World_To_Screen( | |
| 155 p_camera, | |
| 156 (Vector2){ | |
| 157 p_entity->position.x + frame_inset, | |
| 158 p_entity->position.y + frame_inset, | |
| 159 }); | |
| 160 return (Rectangle){ | |
| 161 position.x, | |
| 162 position.y, | |
| 163 (p_entity->size.x - frame_inset - resize_handle) * p_camera->zoom, | |
| 164 (p_entity->size.y - frame_inset - resize_handle) * p_camera->zoom, | |
| 165 }; | |
| 166 } | |
| 167 | |
| 168 static boolean Canvas_Web_Is_Visible( | |
| 169 const Canvas_Camera *p_camera, | |
| 170 Rectangle bounds) | |
| 171 { | |
| 172 return p_camera->zoom >= 0.30f && | |
| 173 bounds.x + bounds.width > 0.0f && | |
| 174 bounds.y + bounds.height > 0.0f && | |
| 175 bounds.x < (float)p_camera->viewport_width && | |
| 176 bounds.y < (float)p_camera->viewport_height; | |
| 177 } | |
| 178 | |
| 179 int32 Canvas_Web_Surface_Execute_Subprocess(int argc, char **p_argv) | |
| 180 { | |
| 181 (void)argc; | |
| 182 (void)p_argv; | |
| 183 return -1; | |
| 184 } | |
| 185 | |
| 186 boolean Canvas_Web_Surface_Init( | |
| 187 Canvas_Web_Surface *p_surface, | |
| 188 Dowa_Arena *p_arena) | |
| 189 { | |
| 190 memset(p_surface, 0, sizeof(*p_surface)); | |
| 191 p_surface->p_arena = p_arena; | |
| 192 p_surface->initialized = TRUE; | |
| 193 return TRUE; | |
| 194 } | |
| 195 | |
| 196 boolean Canvas_Web_Surface_Update_Input( | |
| 197 Canvas_Web_Surface *p_surface, | |
| 198 const Canvas_Camera *p_camera, | |
| 199 const Canvas_Scene *p_scene) | |
| 200 { | |
| 201 (void)p_surface; | |
| 202 (void)p_camera; | |
| 203 (void)p_scene; | |
| 204 return FALSE; | |
| 205 } | |
| 206 | |
| 207 void Canvas_Web_Surface_Sync( | |
| 208 Canvas_Web_Surface *p_surface, | |
| 209 const Canvas_Camera *p_camera, | |
| 210 const Canvas_Scene *p_scene) | |
| 211 { | |
| 212 if (!p_surface->initialized) return; | |
| 213 | |
| 214 Canvas_Web_Begin_Frame(); | |
| 215 uint32 visible_views = 0; | |
| 216 for (size_t index = 0; | |
| 217 index < Dowa_Array_Length(p_scene->p_entities) && | |
| 218 visible_views < CANVAS_MAX_WEB_VIEWS; | |
| 219 index++) { | |
| 220 const Canvas_Entity *p_entity = &p_scene->p_entities[index]; | |
| 221 if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT && | |
| 222 p_entity->type != CANVAS_ENTITY_IMAGE) { | |
| 223 continue; | |
| 224 } | |
| 225 Rectangle bounds = Canvas_Web_Content_Bounds(p_camera, p_entity); | |
| 226 if (!Canvas_Web_Is_Visible(p_camera, bounds)) continue; | |
| 227 if (p_entity->type == CANVAS_ENTITY_IMAGE) { | |
| 228 Canvas_Image_Create_Or_Update( | |
| 229 p_entity->id, | |
| 230 p_entity->text, | |
| 231 bounds.x, | |
| 232 bounds.y, | |
| 233 bounds.width, | |
| 234 bounds.height); | |
| 235 } else { | |
| 236 Canvas_Web_Create_Or_Update( | |
| 237 p_entity->id, | |
| 238 p_entity->text, | |
| 239 bounds.x, | |
| 240 bounds.y, | |
| 241 bounds.width, | |
| 242 bounds.height); | |
| 243 } | |
| 244 visible_views++; | |
| 245 } | |
| 246 Canvas_Web_End_Frame(); | |
| 247 } | |
| 248 | |
| 249 void Canvas_Web_Surface_Draw( | |
| 250 const Canvas_Web_Surface *p_surface, | |
| 251 const Canvas_Camera *p_camera, | |
| 252 const Canvas_Scene *p_scene) | |
| 253 { | |
| 254 (void)p_surface; | |
| 255 (void)p_camera; | |
| 256 (void)p_scene; | |
| 257 } | |
| 258 | |
| 259 boolean Canvas_Web_Surface_Is_Focused(const Canvas_Web_Surface *p_surface) | |
| 260 { | |
| 261 (void)p_surface; | |
| 262 return FALSE; | |
| 263 } | |
| 264 | |
| 265 void Canvas_Web_Surface_Shutdown(Canvas_Web_Surface *p_surface) | |
| 266 { | |
| 267 if (!p_surface->initialized) return; | |
| 268 Canvas_Web_Destroy(); | |
| 269 p_surface->initialized = FALSE; | |
| 270 } |