comparison mrjunejune/src/public/raylib.js @ 100:65e5a5b89a4e

[Seobeo] Migrated everything to this page.
author June Park <parkjune1995@gmail.com>
date Sat, 03 Jan 2026 07:48:07 -0800
parents
children
comparison
equal deleted inserted replaced
99:684edfaf93b7 100:65e5a5b89a4e
1 /**
2 * I Stole this from https://github.com/tsoding/c3-demo/blob/main/raylib.js and added few functions for functionality.
3 * It seems to be from raylib.com/examples
4 */
5 function make_environment(env) {
6 return new Proxy(env, {
7 get(_target, prop, _receiver) {
8 if (env[prop] !== undefined) {
9 return env[prop].bind(env);
10 }
11 return (...args) => {
12 throw new Error(`NOT IMPLEMENTED: ${prop} ${args}`);
13 };
14 },
15 });
16 }
17
18 let iota = 0;
19 const LOG_ALL = iota++; // Display all logs
20 const LOG_TRACE = iota++; // Trace logging, intended for internal use only
21 const LOG_DEBUG = iota++; // Debug logging, used for internal debugging, it should be disabled on release builds
22 const LOG_INFO = iota++; // Info logging, used for program execution info
23 const LOG_WARNING = iota++; // Warning logging, used on recoverable failures
24 const LOG_ERROR = iota++; // Error logging, used on unrecoverable failures
25 const LOG_FATAL = iota++; // Fatal logging, used to abort program: exit(EXIT_FAILURE)
26 const LOG_NONE = iota++; // Disable logging
27
28 class RaylibJs {
29 #FONT_SCALE_MAGIC = 0.65;
30
31 #reset() {
32 this.previous = undefined;
33 this.wasm = undefined;
34 this.ctx = undefined;
35 this.dt = undefined;
36 this.targetFPS = 60;
37 this.entryFunction = undefined;
38 this.prevPressedKeyState = new Set();
39 this.currentPressedKeyState = new Set();
40 this.currentMouseWheelMoveState = 0;
41 /* -1 since 0 is left click, 1 middle, 2 is right click */
42 this.currentIsMouseButtonPressed = -1;
43 this.currentMousePosition = { x: 0, y: 0 };
44 this.images = [];
45 this.quit = false;
46 }
47
48 constructor() {
49 this.#reset();
50 }
51
52 stop() {
53 this.quit = true;
54 }
55
56 async start({ wasmPath, canvasId }) {
57 if (this.wasm !== undefined) {
58 console.error("The game is already running. Please stop() it first.");
59 return;
60 }
61
62 const canvas = document.getElementById(canvasId);
63 this.ctx = canvas.getContext("2d");
64 if (this.ctx === null) {
65 throw new Error("Could not create 2d canvas context");
66 }
67
68 this.wasm = await WebAssembly.instantiateStreaming(fetch(wasmPath), {
69 env: make_environment(this),
70 });
71
72 const keyDown = (e) => {
73 this.currentPressedKeyState.add(glfwKeyMapping[e.code]);
74 };
75 const keyUp = (e) => {
76 this.currentPressedKeyState.delete(glfwKeyMapping[e.code]);
77 };
78 const wheelMove = (e) => {
79 this.currentMouseWheelMoveState = Math.sign(-e.deltaY);
80 };
81 const mouseMove = (e) => {
82 this.currentMousePosition = { x: e.clientX, y: e.clientY };
83 };
84 const mouseClick = (e) => {
85 this.currentIsMouseButtonPressed = e.button;
86 };
87 const touchStarted = () => {
88 this.currentIsMouseButtonPressed = 0;
89 };
90 const touchOrClickEnded = () => {
91 this.currentIsMouseButtonPressed = -1;
92 };
93 window.addEventListener("keydown", keyDown);
94 window.addEventListener("keyup", keyUp);
95 window.addEventListener("wheel", wheelMove);
96 window.addEventListener("mousemove", mouseMove);
97 window.addEventListener("mousedown", mouseClick);
98 window.addEventListener("mouseup", touchOrClickEnded);
99
100 /* For phones */
101 window.addEventListener("touchstart", touchStarted);
102 window.addEventListener("touchend", touchOrClickEnded);
103 window.addEventListener("touchcancel", touchOrClickEnded);
104
105 this.wasm.instance.exports.main();
106 const next = (timestamp) => {
107 if (this.quit) {
108 this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
109 window.removeEventListener("keydown", keyDown);
110 this.#reset();
111 return;
112 }
113 this.dt = (timestamp - this.previous) / 1000.0;
114 this.previous = timestamp;
115 this.entryFunction();
116 window.requestAnimationFrame(next);
117 };
118 window.requestAnimationFrame((timestamp) => {
119 this.previous = timestamp;
120 window.requestAnimationFrame(next);
121 });
122 }
123
124 InitWindow(width, height, title_ptr) {
125 this.ctx.canvas.width = width;
126 this.ctx.canvas.height = height;
127 const buffer = this.wasm.instance.exports.memory.buffer;
128 document.title = cstr_by_ptr(buffer, title_ptr);
129 }
130
131 WindowShouldClose() {
132 return false;
133 }
134
135 SetTargetFPS(fps) {
136 console.log(
137 `The game wants to run at ${fps} FPS, but in Web we gonna just ignore it.`,
138 );
139 this.targetFPS = fps;
140 }
141
142 GetScreenWidth() {
143 return this.ctx.canvas.width;
144 }
145
146 GetScreenHeight() {
147 return this.ctx.canvas.height;
148 }
149
150 GetFrameTime() {
151 // TODO: This is a stopgap solution to prevent sudden jumps in dt when the user switches to a differen tab.
152 // We need a proper handling of Target FPS here.
153 return Math.min(this.dt, 1.0 / this.targetFPS);
154 }
155
156 BeginDrawing() {}
157
158 EndDrawing() {
159 this.prevPressedKeyState.clear();
160 this.prevPressedKeyState = new Set(this.currentPressedKeyState);
161 this.currentMouseWheelMoveState = 0.0;
162 }
163
164 DrawCircleV(center_ptr, radius, color_ptr) {
165 const buffer = this.wasm.instance.exports.memory.buffer;
166 const [x, y] = new Float32Array(buffer, center_ptr, 2);
167 const [r, g, b, a] = new Uint8Array(buffer, color_ptr, 4);
168 const color = color_hex_unpacked(r, g, b, a);
169 this.ctx.beginPath();
170 this.ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
171 this.ctx.fillStyle = color;
172 this.ctx.fill();
173 }
174
175 ClearBackground(color_ptr) {
176 this.ctx.fillStyle = getColorFromMemory(
177 this.wasm.instance.exports.memory.buffer,
178 color_ptr,
179 );
180 this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
181 }
182
183 // RLAPI void DrawText(const char *text, int posX, int posY, int fontSize, Color color); // Draw text (using default font)
184 DrawText(text_ptr, posX, posY, fontSize, color_ptr) {
185 const buffer = this.wasm.instance.exports.memory.buffer;
186 const text = cstr_by_ptr(buffer, text_ptr);
187 const color = getColorFromMemory(buffer, color_ptr);
188 fontSize *= this.#FONT_SCALE_MAGIC;
189 this.ctx.fillStyle = color;
190 // TODO: since the default font is part of Raylib the css that defines it should be located in raylib.js and not in index.html
191 this.ctx.font = `${fontSize}px grixel`;
192
193 const lines = text.split("\n");
194 for (var i = 0; i < lines.length; i++) {
195 this.ctx.fillText(lines[i], posX, posY + fontSize + i * fontSize);
196 }
197 }
198
199 // RLAPI void DrawRectangle(int posX, int posY, int width, int height, Color color); // Draw a color-filled rectangle
200 DrawRectangle(posX, posY, width, height, color_ptr) {
201 const buffer = this.wasm.instance.exports.memory.buffer;
202 const color = getColorFromMemory(buffer, color_ptr);
203 this.ctx.fillStyle = color;
204 this.ctx.fillRect(posX, posY, width, height);
205 }
206
207 DrawRectangleV(position_ptr, size_ptr, color_ptr) {
208 const buffer = this.wasm.instance.exports.memory.buffer;
209 const color = getColorFromMemory(buffer, color_ptr);
210 const position = new Float32Array(buffer, position_ptr, 2);
211 const size = new Float32Array(buffer, size_ptr, 2);
212 this.ctx.fillStyle = color;
213 this.ctx.fillRect(position[0], position[1], size[0], size[1]);
214 }
215
216 IsKeyPressed(key) {
217 return (
218 !this.prevPressedKeyState.has(key) && this.currentPressedKeyState.has(key)
219 );
220 }
221 IsKeyDown(key) {
222 return this.currentPressedKeyState.has(key);
223 }
224 GetMouseWheelMove() {
225 return this.currentMouseWheelMoveState;
226 }
227 IsGestureDetected() {
228 return false;
229 }
230 IsMouseButtonPressed(key) {
231 return this.currentIsMouseButtonPressed == key;
232 }
233
234 TextFormat(...args) {
235 // TODO: Implement printf style formatting for TextFormat
236 return args[0];
237 }
238
239 TraceLog(logLevel, text_ptr, ...args) {
240 // TODO: Implement printf style formatting for TraceLog
241 const buffer = this.wasm.instance.exports.memory.buffer;
242 const text = cstr_by_ptr(buffer, text_ptr);
243 switch (logLevel) {
244 case LOG_ALL:
245 console.log(`ALL: ${text} ${args}`);
246 break;
247 case LOG_TRACE:
248 console.log(`TRACE: ${text} ${args}`);
249 break;
250 case LOG_DEBUG:
251 console.log(`DEBUG: ${text} ${args}`);
252 break;
253 case LOG_INFO:
254 console.log(`INFO: ${text} ${args}`);
255 break;
256 case LOG_WARNING:
257 console.log(`WARNING: ${text} ${args}`);
258 break;
259 case LOG_ERROR:
260 console.log(`ERROR: ${text} ${args}`);
261 break;
262 case LOG_FATAL:
263 throw new Error(`FATAL: ${text}`);
264 case LOG_NONE:
265 console.log(`NONE: ${text} ${args}`);
266 break;
267 }
268 }
269
270 GetMousePosition(result_ptr) {
271 const bcrect = this.ctx.canvas.getBoundingClientRect();
272 const x = this.currentMousePosition.x - bcrect.left;
273 const y = this.currentMousePosition.y - bcrect.top;
274
275 const buffer = this.wasm.instance.exports.memory.buffer;
276 new Float32Array(buffer, result_ptr, 2).set([x, y]);
277 }
278
279 CheckCollisionPointRec(point_ptr, rec_ptr) {
280 const buffer = this.wasm.instance.exports.memory.buffer;
281 const [x, y] = new Float32Array(buffer, point_ptr, 2);
282 const [rx, ry, rw, rh] = new Float32Array(buffer, rec_ptr, 4);
283 return x >= rx && x <= rx + rw && y >= ry && y <= ry + rh;
284 }
285
286 Fade(result_ptr, color_ptr, alpha) {
287 const buffer = this.wasm.instance.exports.memory.buffer;
288 const [r, g, b, _] = new Uint8Array(buffer, color_ptr, 4);
289 const newA = Math.max(0, Math.min(255, 255.0 * alpha));
290 new Uint8Array(buffer, result_ptr, 4).set([r, g, b, newA]);
291 }
292
293 DrawRectangleRec(rec_ptr, color_ptr) {
294 const buffer = this.wasm.instance.exports.memory.buffer;
295 const [x, y, w, h] = new Float32Array(buffer, rec_ptr, 4);
296 const color = getColorFromMemory(buffer, color_ptr);
297 this.ctx.fillStyle = color;
298 this.ctx.fillRect(x, y, w, h);
299 }
300
301 DrawRectangleLinesEx(rec_ptr, lineThick, color_ptr) {
302 const buffer = this.wasm.instance.exports.memory.buffer;
303 const [x, y, w, h] = new Float32Array(buffer, rec_ptr, 4);
304 const color = getColorFromMemory(buffer, color_ptr);
305 this.ctx.strokeStyle = color;
306 this.ctx.lineWidth = lineThick;
307 this.ctx.strokeRect(
308 x + lineThick / 2,
309 y + lineThick / 2,
310 w - lineThick,
311 h - lineThick,
312 );
313 }
314
315 MeasureText(text_ptr, fontSize) {
316 const buffer = this.wasm.instance.exports.memory.buffer;
317 const text = cstr_by_ptr(buffer, text_ptr);
318 fontSize *= this.#FONT_SCALE_MAGIC;
319 this.ctx.font = `${fontSize}px grixel`;
320 return this.ctx.measureText(text).width;
321 }
322
323 TextSubtext(text_ptr, position, length) {
324 const buffer = this.wasm.instance.exports.memory.buffer;
325 const text = cstr_by_ptr(buffer, text_ptr);
326 const subtext = text.substring(position, length);
327
328 var bytes = new Uint8Array(buffer, 0, subtext.length + 1);
329 for (var i = 0; i < subtext.length; i++) {
330 bytes[i] = subtext.charCodeAt(i);
331 }
332 bytes[subtext.length] = 0;
333
334 return bytes;
335 }
336
337 // RLAPI Texture2D LoadTexture(const char *fileName);
338 LoadTexture(result_ptr, filename_ptr) {
339 const buffer = this.wasm.instance.exports.memory.buffer;
340 const filename = cstr_by_ptr(buffer, filename_ptr);
341
342 var result = new Uint32Array(buffer, result_ptr, 5);
343 var img = new Image();
344 const isLocalhost = window.location.hostname === "localhost";
345 const baseUrl = isLocalhost
346 ? "http://localhost:6969/"
347 : `https://${window.location.hostname}/`;
348 img.src = `${baseUrl}${filename}`;
349 this.images.push(img);
350
351 result[0] = this.images.indexOf(img);
352 // TODO: get the true width and height of the image
353 result[1] = 256; // width
354 result[2] = 256; // height
355 result[3] = 1; // mipmaps
356 result[4] = 7; // format PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
357
358 return result;
359 }
360
361 // RLAPI void DrawTexture(Texture2D texture, int posX, int posY, Color tint);
362 DrawTexture(texture_ptr, posX, posY, color_ptr) {
363 const buffer = this.wasm.instance.exports.memory.buffer;
364 const [id, width, height, mipmaps, format] = new Uint32Array(
365 buffer,
366 texture_ptr,
367 5,
368 );
369 // // TODO: implement tinting for DrawTexture
370 // const tint = getColorFromMemory(buffer, color_ptr);
371
372 this.ctx.drawImage(this.images[id], posX, posY);
373 }
374
375 // TODO: codepoints are not implemented
376 LoadFontEx(
377 result_ptr,
378 fileName_ptr /*, fontSize, codepoints, codepointCount*/,
379 ) {
380 const buffer = this.wasm.instance.exports.memory.buffer;
381 const fileName = cstr_by_ptr(buffer, fileName_ptr);
382 // TODO: dynamically generate the name for the font
383 // Support more than one custom font
384 const font = new FontFace("myfont", `url(${fileName})`);
385 document.fonts.add(font);
386 font.load();
387 }
388
389 GenTextureMipmaps() {}
390 SetTextureFilter() {}
391
392 MeasureTextEx(result_ptr, font, text_ptr, fontSize, spacing) {
393 const buffer = this.wasm.instance.exports.memory.buffer;
394 const text = cstr_by_ptr(buffer, text_ptr);
395 const result = new Float32Array(buffer, result_ptr, 2);
396 this.ctx.font = fontSize + "px myfont";
397 const metrics = this.ctx.measureText(text);
398 result[0] = metrics.width;
399 result[1] = fontSize;
400 }
401
402 DrawTextEx(font, text_ptr, position_ptr, fontSize, spacing, tint_ptr) {
403 const buffer = this.wasm.instance.exports.memory.buffer;
404 const text = cstr_by_ptr(buffer, text_ptr);
405 const [posX, posY] = new Float32Array(buffer, position_ptr, 2);
406 const tint = getColorFromMemory(buffer, tint_ptr);
407 this.ctx.fillStyle = tint;
408 this.ctx.font = fontSize + "px myfont";
409 this.ctx.fillText(text, posX, posY + fontSize);
410 }
411
412 GetRandomValue(min, max) {
413 return min + Math.floor(Math.random() * (max - min + 1));
414 }
415
416 ColorFromHSV(result_ptr, hue, saturation, value) {
417 const buffer = this.wasm.instance.exports.memory.buffer;
418 const result = new Uint8Array(buffer, result_ptr, 4);
419
420 // Red channel
421 let k = (5.0 + hue / 60.0) % 6;
422 let t = 4.0 - k;
423 k = t < k ? t : k;
424 k = k < 1 ? k : 1;
425 k = k > 0 ? k : 0;
426 result[0] = Math.floor((value - value * saturation * k) * 255.0);
427
428 // Green channel
429 k = (3.0 + hue / 60.0) % 6;
430 t = 4.0 - k;
431 k = t < k ? t : k;
432 k = k < 1 ? k : 1;
433 k = k > 0 ? k : 0;
434 result[1] = Math.floor((value - value * saturation * k) * 255.0);
435
436 // Blue channel
437 k = (1.0 + hue / 60.0) % 6;
438 t = 4.0 - k;
439 k = t < k ? t : k;
440 k = k < 1 ? k : 1;
441 k = k > 0 ? k : 0;
442 result[2] = Math.floor((value - value * saturation * k) * 255.0);
443
444 result[3] = 255;
445 }
446
447 raylib_js_set_entry(entry) {
448 this.entryFunction =
449 this.wasm.instance.exports.__indirect_function_table.get(entry);
450 }
451 }
452
453 const glfwKeyMapping = {
454 Space: 32,
455 Quote: 39,
456 Comma: 44,
457 Minus: 45,
458 Period: 46,
459 Slash: 47,
460 Digit0: 48,
461 Digit1: 49,
462 Digit2: 50,
463 Digit3: 51,
464 Digit4: 52,
465 Digit5: 53,
466 Digit6: 54,
467 Digit7: 55,
468 Digit8: 56,
469 Digit9: 57,
470 Semicolon: 59,
471 Equal: 61,
472 KeyA: 65,
473 KeyB: 66,
474 KeyC: 67,
475 KeyD: 68,
476 KeyE: 69,
477 KeyF: 70,
478 KeyG: 71,
479 KeyH: 72,
480 KeyI: 73,
481 KeyJ: 74,
482 KeyK: 75,
483 KeyL: 76,
484 KeyM: 77,
485 KeyN: 78,
486 KeyO: 79,
487 KeyP: 80,
488 KeyQ: 81,
489 KeyR: 82,
490 KeyS: 83,
491 KeyT: 84,
492 KeyU: 85,
493 KeyV: 86,
494 KeyW: 87,
495 KeyX: 88,
496 KeyY: 89,
497 KeyZ: 90,
498 BracketLeft: 91,
499 Backslash: 92,
500 BracketRight: 93,
501 Backquote: 96,
502 // GLFW_KEY_WORLD_1 161 /* non-US #1 */
503 // GLFW_KEY_WORLD_2 162 /* non-US #2 */
504 Escape: 256,
505 Enter: 257,
506 Tab: 258,
507 Backspace: 259,
508 Insert: 260,
509 Delete: 261,
510 ArrowRight: 262,
511 ArrowLeft: 263,
512 ArrowDown: 264,
513 ArrowUp: 265,
514 PageUp: 266,
515 PageDown: 267,
516 Home: 268,
517 End: 269,
518 CapsLock: 280,
519 ScrollLock: 281,
520 NumLock: 282,
521 PrintScreen: 283,
522 Pause: 284,
523 F1: 290,
524 F2: 291,
525 F3: 292,
526 F4: 293,
527 F5: 294,
528 F6: 295,
529 F7: 296,
530 F8: 297,
531 F9: 298,
532 F10: 299,
533 F11: 300,
534 F12: 301,
535 F13: 302,
536 F14: 303,
537 F15: 304,
538 F16: 305,
539 F17: 306,
540 F18: 307,
541 F19: 308,
542 F20: 309,
543 F21: 310,
544 F22: 311,
545 F23: 312,
546 F24: 313,
547 F25: 314,
548 NumPad0: 320,
549 NumPad1: 321,
550 NumPad2: 322,
551 NumPad3: 323,
552 NumPad4: 324,
553 NumPad5: 325,
554 NumPad6: 326,
555 NumPad7: 327,
556 NumPad8: 328,
557 NumPad9: 329,
558 NumpadDecimal: 330,
559 NumpadDivide: 331,
560 NumpadMultiply: 332,
561 NumpadSubtract: 333,
562 NumpadAdd: 334,
563 NumpadEnter: 335,
564 NumpadEqual: 336,
565 ShiftLeft: 340,
566 ControlLeft: 341,
567 AltLeft: 342,
568 MetaLeft: 343,
569 ShiftRight: 344,
570 ControlRight: 345,
571 AltRight: 346,
572 MetaRight: 347,
573 ContextMenu: 348,
574 // GLFW_KEY_LAST GLFW_KEY_MENU
575 };
576
577 function cstrlen(mem, ptr) {
578 let len = 0;
579 while (mem[ptr] != 0) {
580 len++;
581 ptr++;
582 }
583 return len;
584 }
585
586 function cstr_by_ptr(mem_buffer, ptr) {
587 const mem = new Uint8Array(mem_buffer);
588 const len = cstrlen(mem, ptr);
589 const bytes = new Uint8Array(mem_buffer, ptr, len);
590 return new TextDecoder().decode(bytes);
591 }
592
593 function color_hex_unpacked(r, g, b, a) {
594 r = r.toString(16).padStart(2, "0");
595 g = g.toString(16).padStart(2, "0");
596 b = b.toString(16).padStart(2, "0");
597 a = a.toString(16).padStart(2, "0");
598 return "#" + r + g + b + a;
599 }
600
601 function color_hex(color) {
602 const r = ((color >> (0 * 8)) & 0xff).toString(16).padStart(2, "0");
603 const g = ((color >> (1 * 8)) & 0xff).toString(16).padStart(2, "0");
604 const b = ((color >> (2 * 8)) & 0xff).toString(16).padStart(2, "0");
605 const a = ((color >> (3 * 8)) & 0xff).toString(16).padStart(2, "0");
606 return "#" + r + g + b + a;
607 }
608
609 function getColorFromMemory(buffer, color_ptr) {
610 const [r, g, b, a] = new Uint8Array(buffer, color_ptr, 4);
611 return color_hex_unpacked(r, g, b, a);
612 }