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 }