# HG changeset patch # User June Park # Date 1767455287 28800 # Node ID 65e5a5b89a4e78dcf5cc7c99c24d8e6b37290b2c # Parent 684edfaf93b74e39d3cf495711499195a7f39773 [Seobeo] Migrated everything to this page. diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/main.c --- a/mrjunejune/main.c Fri Jan 02 20:38:02 2026 -0800 +++ b/mrjunejune/main.c Sat Jan 03 07:48:07 2026 -0800 @@ -19,9 +19,11 @@ char *path, Dowa_Arena *arena ) { + Seobeo_Log(SEOBEO_DEBUG, "[Curr] %s\n", path); size_t html_size = 0; char *template = Seobeo_Web_LoadFile(path, &html_size); if (!template) return; + Seobeo_Log(SEOBEO_DEBUG, "[Curr] ??\n"); size_t current_offset = 0; char *cursor = template; @@ -47,6 +49,7 @@ size_t sub_file_size = 0; char *sub_content = Seobeo_Web_LoadFile(include_name, &sub_file_size); + Seobeo_Log(SEOBEO_DEBUG, "[Curr] Sub content: %s\n", sub_content); if (sub_content) { memcpy(final_body + current_offset, sub_content, sub_file_size); @@ -435,6 +438,42 @@ return resp; } +Seobeo_Request_Entry *RenderBlogList(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + Seobeo_Request_Entry *resp = NULL; + char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); + Seobeo_ServerSideRender(final_body, "/blog/index.html", arena); + Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); + return resp; +} + + +Seobeo_Request_Entry *RenderBlog(Seobeo_Request_Entry *req, Dowa_Arena *arena) +{ + Seobeo_Log(SEOBEO_DEBUG, "[CURR], Hello\n"); + Seobeo_Request_Entry *resp = NULL; + + void *blog_id_kv = Dowa_HashMap_Get_Ptr(req, ":blog_id"); + if (!blog_id_kv) + { + char *error_msg = "No Blog Id"; + Dowa_HashMap_Push_Arena(resp, "status", "404", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); + Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena); + return resp; + } + + const char *blog_id = ((Seobeo_Request_Entry *)blog_id_kv)->value; + char *html_path = Dowa_Arena_Allocate(arena, 512); + sprintf(html_path, "/blog/%s/index.html", blog_id); + + char *final_body = Dowa_Arena_Allocate(arena, 100 * 1024); // TODO: Think about the sizes + Seobeo_ServerSideRender(final_body, html_path, arena); + Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); + return resp; +} + + CREATE_REDIRECT_HANDLER(HomePage, "/") CREATE_REDIRECT_HANDLER(Resume, "/resume") CREATE_REDIRECT_HANDLER(Tools, "/tools") @@ -459,9 +498,14 @@ Seobeo_Router_Register("GET", "/tools/file_converter", GetFileConverter); Seobeo_Router_Register("GET", "/tools/file_converter/index.html", GetRedirectFileConverter); + // -- File converter --/ Seobeo_Router_Register("POST", "/api/convert/image-to-webp", ConvertImageToWebP); Seobeo_Router_Register("POST", "/api/convert/video-to-mp4", ConvertVideoToMP4); Seobeo_Router_Register("GET", "/api/download/:filename", DownloadConvertedFile); + // -- Blog --/ + Seobeo_Router_Register("GET", "/blog", RenderBlogList); + Seobeo_Router_Register("GET", "/blog/:blog_id", RenderBlog); + Seobeo_Web_Server_Start("mrjunejune/src", "6969", SEOBEO_MODE_EDGE, 3); } diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/base.css --- a/mrjunejune/src/base.css Fri Jan 02 20:38:02 2026 -0800 +++ b/mrjunejune/src/base.css Sat Jan 03 07:48:07 2026 -0800 @@ -235,10 +235,6 @@ border-radius: 8px; } -pre > code { - all: unset; -} - blockquote { border-left: 4px solid var(--accent); padding: 0 0 0 20px; diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/index.html Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,43 @@ + + + + {{/parts/base_head.html}} + + + {{/parts/header.html}} +
+

Blogs

+ +
+ {{/parts/footer.html}} + + diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/multithread-in-js/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/multithread-in-js/index.html Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,20 @@ + + + + + + Multi Threading in JS + {{/parts/base_head.html}} + + + + {{/parts/header.html}} +
+
+
+ {{/parts/footer.html}} + + + diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/multithread-in-js/index.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/multithread-in-js/index.md Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,97 @@ +# Multithreading in JavaScript + +I recently discussed various programming languages and their advantages with my new coworkers at Meta. During our conversation, we debated the topic of multithreading in JavaScript. My stance was that multithreading became possible in Node.js after certain versions, while my coworker believed it wasn't feasible due to JavaScript's original design. This led me to delve deeper into the topic and provide some code examples. + +## History and Design Choices + +JavaScript is inherently a __*single-threaded*__ system. It has one call stack, one memory heap, and executes code sequentially. According to Brendan Eich, the creator of JavaScript, it was meant to be a simple language for "people who don’t know what a compiler is." Initially, JavaScript was regarded as a "cursed" language because it was primarily used for annoying pop-up ads and flashy animations. This eventually prompted Firefox to impose restrictions on such features. + +Interestingly (and off tangent), Eich mentioned that JavaScript was designed as a starting point, with the expectation that developers would eventually transition to "real" languages. This idea mirrors other ecosystems like Microsoft's BASIC leading to C++ or JavaScript serving as an entry point to Java. Despite its C-like syntax, JavaScript's connection to Java is superficial, adding to its unique identity. + +For more context, check out [this short but fascinating interview with Brendan Eich](https://www.youtube.com/watch?v=IPxQ9kEaF8c). + +Fast forward to Ryan Dahl’s creation of Node.js, which uses the **event-driven, non-blocking I/O** model with a __*single-threaded*__ event loop. Built on top of the V8 engine, Node.js compiles JavaScript into machine code at runtime. Today, it's the backbone of server-side JavaScript applications like Next.js, Remix, Express, and more. Thanks to the V8 engine (thank you C++), JavaScript is surprisingly fast for a scripting language. (And no, I won't link stupid "programming" dumb ball bouncing around doing billion nested loop). + +However, V8 doesn't natively support multithreading in JavaScript because it processes everything within a __*single-threaded*__. In browsers like Chrome, for instance, each tab operates as an independent process, which prevents interference between tabs. Traditionally, developers have relied on tools like PM2 or other process managers to launch multiple Node.js instances to handle large workloads. This workaround sufficed for many use cases. + +So, looking at the history, the design choices were against creating multithreaded programs, and many people didn’t really care about multithreading since most of the work Node.js handles on the server side tends to be lightweight and not CPU-bound. + +### So.. is Multithreading Possible Now? + +The short answer is yes. Node.js supports multithreading through the **Worker Threads** module, introduced in Node.js 10.5.0 (June 20, 2018) and stabilized in Node.js 12. I didn’t pay much attention to this feature until last year when I had to run an FFmpeg command to process videos and audios. (I really hope that code is still alive... ) + +The advent of **SharedArrayBuffer** and **Atomics** APIs in V8 enabled native multithreading capabilities (thank you C++). Node.js developers complemented these changes by adding thread-safe capabilities to core libraries, making it possible to safely share data between threads. + +## Example of Multithreading in Node.js + +Here’s a simple program to calculate all prime numbers between 0 and 1 billion using 4 CPUs. (I'm using WSL2 and am slightly ashamed of coding on Windows—a "toy OS." Then again, we're also using a "toy language," so it balances out!) + +**`main.js`:** +```javascript +const { Worker } = require('worker_threads'); + +const start = 0; +const end = 1_000_000_000; +const numberOfCpus = 4; +const rangePerWorker = Math.ceil((end - start + 1) / numberOfCpus); + +console.log(`Calculating prime numbers from ${start} to ${end} using ${numberOfCpus} workers...`); + +let completedWorkers = 0; +const primes = []; + +for (let i = 0; i < numberOfCpus; i++) { + const workerStart = start + i * rangePerWorker; + const workerEnd = Math.min(workerStart + rangePerWorker - 1, end); + + const worker = new Worker('./worker.js', { + workerData: { start: workerStart, end: workerEnd }, + }); + + worker.on('message', (workerPrimes) => { + primes.push(...workerPrimes); + completedWorkers++; + + if (completedWorkers === numberOfCpus) { + console.log('All workers completed.'); + } + }); + + worker.on('error', (err) => console.error(`Worker error: ${err}`)); + worker.on('exit', (code) => { + if (code !== 0) console.error(`Worker stopped with exit code ${code}`); + }); +} +``` + +**`worker.js`:** +```javascript +const { parentPort, workerData } = require('worker_threads'); + +const { start, end } = workerData; + +function isPrime(num) { + if (num < 2) return false; + for (let i = 2, sqrt = Math.sqrt(num); i <= sqrt; i++) { + if (num % i === 0) return false; + } + return true; +} + +const primes = []; +for (let i = start; i <= end; i++) { + if (isPrime(i)) primes.push(i); +} + +parentPort.postMessage(primes); +``` + +**Result:** + + + +As expected, all 4 cores are utilized. Interestingly, one core finished significantly earlier than the others because it was handling a smaller computational load (0 to 250 million). I noticed some random spikes at the end—probably due to system-level resource allocation quirks, but someone smart can probably figure out the actual details... + +**[Link to codes](https://github.com/MrJuneJune/blog_sample_codes/tree/main/performance_tests/node_multithreads)** diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/optimizing-data-structures/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/optimizing-data-structures/index.html Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,20 @@ + + + + + + Optimizing Data Structure for Performance + {{/parts/base_head.html}} + + + + {{/parts/header.html}} +
+
+
+ {{/parts/footer.html}} + + + diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/optimizing-data-structures/index.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/optimizing-data-structures/index.md Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,98 @@ +### Optimizing Data Structures for Performance + +In my professional experience as a full-stack engineer, I primarily work with languages like Python, JavaScript, and Ruby, which don’t provide strict control over memory management. Contract work requiring languages like C or C++ is rare, as most clients prefer "modern" languages that prioritize ease of use over low-level control. Even on the few occasions when I did write C or C++ code, my focus was not on performance optimization. However, I recently observed some C++ code that changed my perspective. + +Let’s consider creating a simple entity for a video game: + +```c++ +struct Entity { + Vector3* position; + Vector3* velocity; + int* id; + + Entity(); + ~Entity(); + + void update(); +}; +``` + +At first glance, this structure seems perfectly reasonable, especially if additional attributes are added later. However, if performance is a concern, this approach has significant drawbacks. While the pointers themselves are stored contiguously in memory, the data they point to may be dynamically allocated in disparate locations on the heap, leading to poor cache locality and slower performance. + +For example, in a 64-bit computer, the `Entity` struct might look like this in memory: + +``` +| position (8 bytes) | velocity (8 bytes) | id (8 bytes) | +``` + +However, the data the pointers reference could be scattered across the heap: + +- `position` might point to memory at `0x1000`. +- `velocity` might point to memory at `0x2000`. +- `id` might point to memory at `0x3000`. + +Since these memory addresses are not contiguous or guaranteed to be close, accessing them involves additional overhead. To address this, we can allocate a centralized memory block to improve performance and cache locality. + +Here’s an optimized version of the `Entity` struct: + +```c++ +struct Entity { + char* memory_block = nullptr; + Vector3* position = nullptr; + Vector3* velocity = nullptr; + int* id = nullptr; + + Entity(); + ~Entity(); + + void update(); +}; +``` + +In this version, we allocate a single memory block for the entire `Entity` and assign pointers to the relevant sections within that block. Here’s an example of how this can be done: + +```c++ +Entity::Entity() { + memory_block = new char[sizeof(Vector3) * 2 + sizeof(int)]; + + position = new (memory_block) Vector3; + velocity = new (memory_block + sizeof(Vector3)) Vector3; + id = new (memory_block + sizeof(Vector3) * 2) int(10); +} +``` + +Now, all attributes reside within a single, contiguous memory block. This improves cache locality and reduces the number of dynamic allocations, leading to better performance when constructing and accessing `Entity` objects. + +### Performance Comparison + +I ran some simple tests to compare the performance of the two approaches. The first test involved updating 5,000,000 entities. The results are as follows: + +``` +Updated Entities: 5,000,000 + +Heap-Allocated Entity +Time taken: 409.119 ms +Memory Block Entity +Time taken: 358.371 ms + +Second run (reversed): +Memory Block Entity +Time taken: 353.688 ms +Heap-Allocated Entity +Time taken: 423.250 ms +``` + +The second test measured the time taken to initialize and delete 100,000 entities: + +``` +Number of Entities: 100,000 + +Heap-Allocated Entity +Time taken: 554.954 ms +Memory Block Entity +Time taken: 481.258 ms +``` + +### Observations + +The results show a consistent ~20% performance improvement when using a centralized memory block. This difference is significant, especially in performance-critical applications like video games. In the future, I plan to share how I’ve applied similar optimization techniques to improve performance in JavaScript for network requests. These small changes can add up, making a big difference in real-world applications. diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/optimizing-grass-rendering/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/optimizing-grass-rendering/index.html Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,20 @@ + + + + + + Optimizing Random Placement with Colour Noise + {{/parts/base_head.html}} + + + + {{/parts/header.html}} +
+
+
+ {{/parts/footer.html}} + + + diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/optimizing-grass-rendering/index.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/optimizing-grass-rendering/index.md Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,161 @@ +# Optimizing Random Placement with Colour Noise + +### Introduction + +When it comes to placing objects like grass in a scene, achieving a balance between randomness and natural-looking distribution is a challenge. Too much randomness, like white noise, results in unnatural clustering or gaps. On the other hand, a completely uniform pattern can look artificial. Nature strikes a balance, often governed by constraints such as nutrient availability or environmental factors. + +In this blog, I’ll walk you through how I tackled this problem by comparing white noise and blue noise for object placement. I’ll also explore an optimization technique inspired by Casey Muratori’s insights to improve performance, making it feasible for rendering grass in real-time applications. + +### Understanding White Noise vs. Blue Noise + +White noise is completely random: each point is placed without consideration of others. While this randomness is simple to implement, it results in visually chaotic patterns with undesirable clumping. + +Blue noise introduces spatial constraints, ensuring a minimum distance between points. This results in a more natural, evenly spaced pattern—perfect for simulating real-world distributions like grass or tree placement. + +To visualize this difference, I implemented functions to generate both white and blue noise patterns and compared their results. + + +### Implementation Highlights + +#### White Noise + +The **whiteNoiseRandom** function generates points completely randomly within a bounding box: + +```cpp +void whiteNoiseRandom(Vector2& top_left_corner, Vector2& bottom_right_corner, const int& sides, const float& inner_radius, Vector2 positions[], int& i) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution random_float(0, 1); + + Vector2 start_point = { + (random_float(gen) * (bottom_right_corner.x - top_left_corner.x)) + top_left_corner.x , + (random_float(gen) * (bottom_right_corner.y - top_left_corner.y)) + top_left_corner.y + }; + positions[i] = { start_point.x, start_point.y }; +} +``` + +This is computationally the most efficient as there is no constraints on where it can be placed which mean it will often lead to clumping together. + +
+ +Above image is okay, but lacks realism. We can do better. + + +#### Blue Noise + +Blue noise requires additional checks to maintain a minimum distance between points, implemented as follows: + +```cpp +void blueNoiseRandom(Vector2& top_left_corner, Vector2& bottom_right_corner, const int& sides, const float& inner_radius, Vector2 positions[], int& i) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution random_float(0, 1); + + int attempts = 0; + const int max_attempts = sides * sides; + + while (attempts < max_attempts) { + Vector2 start_point = { random_float(gen) * sides, random_float(gen) * sides }; + if (x >= top_left_corner.x && x < bottom_right_corner.y && y >= top_left_corner.y && y < bottom_right_corner.y) { + bool added = true; + for (int j = 0; j < i; j++) { + float dx = x - positions[j].x; + float dy = y - positions[j].y; + if (std::sqrt(dx * dx + dy * dy) < inner_radius) { + added = false; + break; + } + } + if (added) { + positions[i] = { start_point.x, start_point.y }; + break; + } + } + attempts++; + } +} +``` + +As you can see from below image, this approach improves visual quality... + +
+ +...but at the cost of computational complexity. As the number of points increases, available space becomes limited, requiring more attempts to place a point. This can take several seconds, which isn’t ideal. + + + +** It takes around 8 seconds for 800 points in 1000 x 800 with 30 radius which means it should be able to add 100 more points in theory ** + + +### Optimizing Blue Noise Generation + + +While researching approaches, I came across Casey Muratori’s work on Witness, where he introduced a local sampling optimization. Instead of evaluating the entire grid for each point, checks are limited to a smaller region around the current point. This reduces redundant calculations, especially in dense scenes. Here’s my implementation: + +```cpp +void blueNoiseCircle(Vector2& top_left_corner, Vector2& bottom_right_corner, const int& sides, const float& inner_radius, const float& outer_radius, + Vector2& starting_point, Vector2 positions[], int& i){ + float center_x = starting_point.x; + float center_y = starting_point.y; + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution angle_dist(0, 2 * M_PI); + std::uniform_real_distribution radius_dist(0, 1); + + bool added = false; + int attempts = 0; + const int max_attempts = outer_radius*outer_radius*PI - inner_radius*inner_radius*PI; + + while (attempts <= max_attempts) { + float theta = angle_dist(gen); + float random_scale = radius_dist(gen); + float radius = std::sqrt(random_scale * (outer_radius * outer_radius - + inner_radius * inner_radius) + + inner_radius * inner_radius); + float x = center_x + radius * std::cos(theta); + float y = center_y + radius * std::sin(theta); + + added = true; + if (x > 0 && x < sides && y > 0 && y < sides) { + for (int j = 0; j < i; j++) { + Vector2 current_point = positions[j]; + float dx = x - current_point.x; + float dy = y - current_point.y; + if (std::sqrt(dx * dx + dy * dy) < inner_radius) { + added = false; + break; + } + } + if (added) { + positions[i] = { x, y }; + break; + } + } + + attempts++; + } + if (!added) { + blueNoiseRandom(top_left_corner, bottom_right_corner, sides, inner_radius, positions, i); + } +} +``` + + + +** It only took 2 seconds or so. It slows down at the end since it probably needed to look for one or two points using random number ** + + +
+ + +### Conclusion + +If you're working on object placement for games, simulations, or any graphics project, understanding the balance between randomness and natural constraints is crucial. Blue noise offers a compelling way to achieve this balance, and with optimizations, it becomes practical for real-time rendering. + +You can find the full source code for my implementation on [GitHub](https://github.com/MrJuneJune/blog_sample_codes/tree/main/performance_tests/noises). diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/thoughts-on-ide/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/thoughts-on-ide/index.html Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,20 @@ + + + + + + My thoughts on IDE + {{/parts/base_head.html}} + + + + {{/parts/header.html}} +
+
+
+ {{/parts/footer.html}} + + + diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/thoughts-on-ide/index.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/thoughts-on-ide/index.md Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,22 @@ +# Thoughts on IDE + +IDEs are crucial tools for developers. They are like a knife to a chef, a stethoscope to a doctor, or a pair of nice shoes to a basketball player. These tools significantly improve quality of life and directly impact how efficiently someone can work. That being said, they are not necessities. A chef can still cook with a dull knife, a doctor can diagnose with some degree of accuracy without a stethoscope, and LeBron will still dunk on you in office shoes. + +
+ +However, I’ve seen many engineers throughout my career who are unable to code effectively unless they have a specific setup—whether it’s VS Code, IntelliJ, or some other IDE. It’s disheartening to see people afraid to use a terminal just because it’s not “user-friendly.” Most of these modern Electron-based editors are simply wrapping terminal commands into clickable buttons. It’s bizarre to see engineers struggle with tasks like resolving merge conflicts unless they have buttons labeled 'Accept All,' 'Accept Incoming,' or 'Accept Local.' Some can’t even SSH into a remote server without the help of VS Code. + +I fully support tools that help developers write better code and become better engineers. But if someone doesn’t understand what’s happening under the hood and asks questions like, “Why can’t I run Python in my VS Code?”, then we’ve overcomplicated the process. This makes things harder for beginners to actually learn what is going on and frustrating for those who already know what they’re doing since it takes forever to figure out where to set simple configurations. Losing context about what’s happening behind the scenes leads to a noticeable degradation in the quality of engineers, who can no longer perform simple tasks in the terminal. + +
+ +*Why is VS Code taking up 1GB of RAM for opening a text file? The folder doesn't have 1GB worth of the files.* + +Productivity on some of these modern IDEs is also questionable. They are often slow. It can take a second or two to open a file, even after the folder is loaded. Many IDEs run countless linters, syntax checkers, and multiple LSP servers simultaneously, all because people install plugins for every library they use. These state-of-the-art applications, developed by billion-dollar companies, should leverage the power of a 4.15GHz CPU to feel like a sharp chef’s knife. Instead, they often feel like clunky infomercial gadgets that press a button to chop vegetables. + +
+*It takes up 2~3 seconds to open a directory that has maybe 300 files. I don't even have any plugins.* + +Personally, I prefer using Neovim or vanilla Vim. Not because they are objectively better than VS Code (they also slow down sometimes), but because they don’t force me to deal with random dependency issues and unnecessary overhead. Many features, like peeking at definitions, work out of the box, and configuration is straightforward when needed. + +This is maybe just a rant, but if you’re starting out as an engineer, I recommend learning to use the terminal first. Then, try out various IDEs, starting from terminal base application, to see what they improve and what they make worse. Becoming proficient in your tools—whatever they may be—will help you code more effectively in an environment you’re comfortable with. diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/thoughts-on-tdd/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/thoughts-on-tdd/index.html Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,20 @@ + + + + + + My thoughts on TDD + {{/parts/base_head.html}} + + + + {{/parts/header.html}} +
+
+
+ {{/parts/footer.html}} + + + diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/thoughts-on-tdd/index.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/thoughts-on-tdd/index.md Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,234 @@ +# Thoughts on TDD + +Is testing important? Ask yourself that question. If you had to think about it for more than a few seconds, you’re either an inexperienced programmer or someone who has never had to release a product to a large group of users. Testing is not just important—it’s essential. It ensures that the software you release is less buggy and more stable because it allows you to catch issues before your customers do. That’s the "why" behind testing that everyone would agree to. + +The real debates arise around the *how*—specifically, approaches to testing, including methodologies like Test-Driven Development (TDD). Over my career, I’ve worked at multiple companies, all of which practiced TDD in some form. This often involved writing unit tests, integration tests, and end-to-end (e2e) tests. However, despite the commonality of TDD, every company seemed to implement it in its own convoluted way, making the code harder to write, debug, and maintain. I want to talk about this practice and my problem with it. + +## Unit Tests + +> "Unit testing is the process where you test the smallest functional unit of code. Software testing helps ensure code quality, and it's an integral part of software development. It's a software development best practice to write software as small, functional units then write a unit test for each code unit." +> +> — Definition from AWS + +You might agree with the above definition, disagree, or be unsure about what qualifies as a "unit," but most people are likely to agree with it overall. Now, imagine you’re a 21-year-old physics graduate with three months of self-taught coding experience (and a serious League of Legends addiction). Somehow, you land your first job as a software engineer and are tasked with writing a serializer for a `GET` API in Django—and... you guessed it! testing it! + +Here's what such a serializer might look like: + +```python +from rest_framework import serializers + +class FooSerializer(serializers.Serializer): + name = serializers.CharField(max_length=100) + unit_price = serializers.FloatField() + quantity_on_hand = serializers.IntegerField(default=0) + + def create(self, validated_data): + return Foo.objects.create(**validated_data) + + def update(self, instance, validated_data): + instance.name = validated_data.get('name', instance.name) + instance.unit_price = validated_data.get('unit_price', instance.unit_price) + instance.quantity_on_hand = validated_data.get('quantity_on_hand', instance.quantity_on_hand) + instance.save() + return instance +``` + +If you’re unfamiliar with Django, the `create` and `update` methods save or update records in the database. It’s normal to serialize an object like this into a response for use in an API endpoint, often with a tool like `JSONRenderer().render(foo.data)`. + +Now, we understand detailed implementation as much as senior INSERT_LIBRARY engineer, the question is: *How do you write a unit test for this?* + + +There are two main reasons why this serializer is hard to test as a true "unit": + +**Dependency on Framework Classes** + +The serializer inherits from Django REST Framework’s `Serializer` class, which comes with built-in behaviors for things like validation and field handling. If you test whether `name` exceeds 100 characters or if `unit_price` is a float, you’re essentially testing whether Django itself works, which is redundant. But you still need to create a test given a *correct* vallue and *incorrect* value because you are testing more for business logic rather than the code. So are we creating unit test for the function ? or the library? + +**Database Dependency** + +The `create` and `update` methods interact with the database directly. Testing them requires either mocking the database (which introduces complexity). If you try to write a test that checks whether the `create` method works, you might end up mocking the `Foo` model, overriding its methods, and verifying that the mock functions were called with the correct arguments. While this might make sense for complex logic, it often feels like overkill for a simple serializer. + +Here’s an example of how you might write such a *unit* test: + +```python +from unittest.mock import patch +from rest_framework.exceptions import ValidationError + +CORRECT_DATA = { + 'name': 'Test Item', + 'unit_price': 10.99, + 'quantity_on_hand': 5 +} +BAD_DATA = { + 'name': 'Test Item' * 100, + 'unit_price': "yo", + 'quantity_on_hand': 5 +} + +def test_serializer_create(): + serializer = FooSerializer(data=CORRECT_DATA) + + # This is where you are testing the frameworks.... + assert serializer.is_valid() + + with patch('app.models.Foo.objects.create') as mock_create: + serializer.save() + mock_create.assert_called_once_with( + name='Test Item', + unit_price=10.99, + quantity_on_hand=5 + ) + +# And you need to create 3 more! (2 update using CORRECT_DATA, and BAD_DATA and 1 with create only) +``` + +This test ensures that the `create` method is called with the right data. But if the serializer logic gets more complex, the mocking can become cumbersome and annoying. Think about below case, where `FooSerializers` needs to have a redis singleton because it needs to store certain data inside of redis for faster accesibility for few minutes. + +```python +class FooSerializer(serializers.Serializer): + ... + def create(self, validated_data): + self.bar(**validated_data, { ttl: 1000 }) + ... + + @cache_property + def bar(self): + return self.get_bar() + + def get_bar(self): + return Bar() +``` + +In this case, the `get_bar` method is introduced to facilitate testing of the singleton behavior. This allows you to override the `get_bar` method during tests to avoid interacting with the actual Redis singleton. However, this adds another layer of complexity to your tests. + +```python +def test_serializer_create(): + serializer = FooSerializer(data=CORRECT_DATA) + + assert serializer.is_valid() + + # Mocking the database interaction + with patch('app.models.Foo.objects.create') as mock_create: + # Mocking the singleton method + with patch.object(serializer, 'get_bar', return_value=MockBar()) as mock_bar: + serializer.save() + mock_bar.assert_called_once_with( + name='Test Item', + unit_price=10.99, + quantity_on_hand=5 + + ) + mock_create.assert_called_once_with( + name='Test Item', + unit_price=10.99, + quantity_on_hand=5 + ) +``` + +You can imagine how the unit test would look as more cascading effects are introduced. With all these 76 lines of code (19 for each case, accounting for 4 tests: 2 with correct data values and 2 with incorrect data values), it essentially tests... + +1. Whether the `create`, `update`, and `Redis` functions are called. +2. Whether the data has the correct units. + +
+ + +### Different Approach: Unit Testing in Rails + +In frameworks like Ruby on Rails, a different paradigm is used for handling tests, one that might directly conflict with the traditional definition of unit tests. Instead of relying heavily on mocking and isolating functions, frameworks like Rails encourage spinning up a test database or any external dependencies that are *essential*. Here’s how the same serializer would look in Rails using Active Record: + +```ruby +class Foo < ApplicationRecord + validates :name, presence: true, length: { maximum: 100 } + validates :unit_price, numericality: true +end +``` + +And here’s how a test might look: + +```ruby +require 'test_helper' + +class FooTest < ActiveSupport::TestCase + test "validates name length and prevents invalid record from saving" do + foo = Foo.new(name: "a" * 101, unit_price: 10.99, quantity_on_hand: 5) + + assert_not foo.valid?, "Foo should be invalid when name exceeds 100 characters" + assert_equal ["is too long (maximum is 100 characters)"], foo.errors[:name] + assert_not foo.save, "Foo with an invalid name should not be saved to the database" + assert_nil Foo.find_by(name: "a" * 101), "No Foo record with an invalid name should exist in the database" + end + + test "saves valid record to the database" do + foo = Foo.new(name: "Valid Item", unit_price: 10.99, quantity_on_hand: 5) + + assert foo.valid?, "Foo should be valid with correct attributes" + assert foo.save, "Foo with valid attributes should be saved to the database" + + # Look for saved foo and bar + saved_foo = Foo.find_by(name: "Valid Item") + saved_bar = Bar.find_by(name: "Valid Item") + + assert_not_nil saved_foo, "Foo record should exist in the database" + assert_equal 10.99, saved_foo.unit_price, "Foo's unit_price should match the saved value" + assert_equal 5, saved_foo.quantity_on_hand, "Foo's quantity_on_hand should match the saved value" + assert_equal 10.99, saved_bar.unit_price, "Bar's unit_price should match the saved value" + end +end +``` + +In this example, you’re not mocking anything. Instead, you’re leveraging the test database to directly validate business logic. This approach often feels cleaner and less convoluted than mocking, especially for frameworks with tightly integrated ORM layers like Active Record. If your application also involves a Redis instance or similar components, you would create those as part of your test setup and verify their existence. This approach, however, goes strictly against AWS's definition of unit tests. At my previous job, I remember having a heated argument with an engineer over this—he was a strong believer in AWS's strict definition, while I preferred a looser interpretation based on my experience with Rails. + +**(And no, starting up a Docker container for a Redis instance or database is not SOOO slow that shouldn't be done. This is a widely accepted practice in many engineering companies, including Shopify and Airbnb.)** + +If you ask me, I’d argue that Rails' approach is slightly better because you avoid the overhead of creating numerous mock objects. For example, in Jest or Python, mocking can get out of hand—especially if you need to mock React hooks that query or mutate global states instead of initializing the state directly within the test. It’s overwhelming to see ten different mock values inserted into a single object, and now imagine having to do that for every single function you write. But now, we will have conflict with... + +## Integration Tests + +> "A type of software test that verifies how different components of your application and services interact and work together as a system, ensuring data flows correctly between them and that the overall functionality is as expected." +> +> — Definition from AWS + +We’ve essentially already done this in our Ruby on Rails testing, so let’s revisit our `FooSerializer`. Integration tests ensure that the serializer works as expected. Here's an example of such a test: + +```python +def test_serializer_create(): + serializer = FooSerializer(data=CORRECT_DATA) + + assert serializer.is_valid() + serializer.save() + assert Foo.objects.filter(name=CORRECT_DATA.name).exists() +``` + +At this point, we’re essentially rewriting the same tests but with less mocking. So, what’s the real value of this? There has to be a scenario where unit tests are useful, and integration tests aren’t—or vice versa. In this particular case, though, it’s hard to distinguish that line. As applications grow more complex, these distinctions may become clearer. However, for most scenarios like this one, if tests weren’t written at all, it would be difficult to identify what’s missing or redundant. + +In this specific case, many would agree that writing both unit tests and integration tests offers little value. It’s almost redundant and doesn’t justify the time and effort required. + +## E2E Tests + +I couldn't find a definition so I am going to just ask gemini for it. + +> End-to-end (E2E) testing is a software testing method that verifies how a software product works from start to finish. It's also known as system testing or broad stack testing +> +> — Gemini version that doesn't create controversial images. + +Finally, let’s talk about end-to-end (E2E) tests. The serializer function we wrote will likely live inside some API endpoint, and I don’t feel like writing codes for it so I hope you guys can imagine it. In most cases, E2E tests are run through a virtual machine (VM) or Docker container that replicates a smaller version of the real server and database, seeded with factory data. These environments are spun up during CI/CD pipelines, where tests simulate real server requests. + +E2E testing is extremely useful when your server is self-contained. However, if your application relies on third-party services, things can get tricky. For example, let’s consider a case where your application depends on an AWS service to check content safety. You can’t easily mimic that service locally, so you’d need to test against their live servers, which may incur costs for every request. As a result, you’ll likely want to separate those tests from your main CI/CD pipeline and use a dedicated staging environment to avoid mixing with production accounts. + +That’s a *good* case. A *bad* case is when the third-party service doesn’t allow any testing at all. For instance, Google’s SSO or similar services might block testing to prevent potential DDoS attacks (Because it is a DDoS attack). In these scenarios, your E2E tests often become glorified integration tests, where you simulate the service instead of interacting with the real thing. + +In short, TDD in its purest sense—using unit, integration, and E2E tests together—is often impractical or unachievable. I haven’t even touched on the time required to run E2E tests, the context-switching needed to write or debug them, flakiness, and other challenges. Still, we write tests because they are critical to delivering reliable software. After all, we’re not open-source engineers releasing products that randomly break because of dependencies like `is_number` in npm, right? Right? + + +## My Thoughts + +We should primarily focus on integration and E2E tests that validate real user activity. It’s not always necessary to test the entire flow, especially when certain parts—like SSO—can only be mocked and not tested fully. E2E tests are often slow, flaky (the worst!), and sometimes impossible to implement comprehensively. + +Unit tests should be reserved for complex functions that require isolation. My general approach to testing is like a binary search: + +- If a unit test fails, the problem is always within the business logic of that specific function. +- If an integration test fails, the issue is likely due to mismatched input/output between components. +- If an E2E test fails, it could indicate a third-party service outage, a race condition, or a broader system issue. + +For simple components like our `FooSerializer`, writing three separate tests (unit, integration, and E2E) is overkill at best and a waste of time at worst. Instead, focus on testing critical user flows and isolating tests to specific areas when necessary. This strikes a balance between test coverage and productivity, avoiding the trap of excessive and redundant testing. diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/wasm-bunny/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/wasm-bunny/index.html Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,40 @@ + + + + + + WASM using c3 + {{/parts/base_head.html}} + + + + {{/parts/header.html}} +
+
+
+ +
+
+ {{/parts/footer.html}} + + + + + diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/wasm-bunny/index.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/wasm-bunny/index.md Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,81 @@ +# First Steps with C3 and Raylib on WebAssembly + +Hi! This is my first blog post on this website, and hopefully, it’s the first of many, as I have some free time at the moment. + +Today, I spent a few hours experimenting with the programming language [C3](https://c3-lang.org/) and linking it with a static file from [Raylib](https://www.raylib.com/), a popular library for creating web applications. My goal was to build something interactive and deploy it to the web with minimal hassle. + +I’ve set up a repository to link C3 with Raylib and stress-test it using a bunny-rendering benchmark, inspired by this [bunnymark benchmark](https://old.reddit.com/r/raylib/comments/15jy1x3/raylib_bunnymark_benchmark_with_100k_bunnies/). + +Here are some key things to keep in mind when compiling C3 and Raylib to WebAssembly (WASM): + +## Limited WASM Support in C3: + +C3 has limited support for compiling to [WASM](https://c3-lang.org/faq/#platform-support). The standard library isn't usable in WASM, so any standard functions need to be rewritten or excluded. For example, I attempted to randomly assign colors to a rabbit, but there was no compile-time error, so I had to debug why WASM wasn’t loading properly. In hindsight, I should have known, given that the `--link-libc=no` flag was used in the examples. + +Example command: +``` +c3c compile --reloc=none --target wasm32 -g0 --link-libc=no --no-entry main.c3 raylib.c3 +``` + +## Custom Raylib JavaScript File: + +Raylib APIs need to be called through a custom `raylib.js` file. As shown in the command above, there’s no static Raylib file (`raylib.a`) linked directly to WASM. Instead, Raylib APIs are accessed through JavaScript using [WebAssembly.instantiateStreaming](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/instantiateStreaming_static). For example, `raylib::init_window` would be called in JavaScript like this: + +```javascript +function InitWindow(width, height, title_ptr) { + this.ctx.canvas.width = width; + this.ctx.canvas.height = height; + const buffer = this.wasm.instance.exports.memory.buffer; + document.title = cstr_by_ptr(buffer, title_ptr); +} + +const raylibObject = { + raylib: { InitWindow: InitWindow }, +}; + +WebAssembly.instantiateStreaming(fetch("main.wasm"), raylibObject).then( + (obj) => obj.instance.exports.exported_func(), +); +``` + +To actaully link it together, I was lucky to find raylib.js [this repo](https://github.com/tsoding/c3-demo/blob/main/raylib.js) (Thanks tsoding!!). I only needed to add a few functions for handling clicks. The file had `RaylibJs` class which need to be turned into object from above example and used `Proxy` class to do that. I actaully never seen that API being used outside of this. + +```javascript +function make_environment(env) { + return new Proxy(env, { + get(_target, prop, _receiver) { + if (env[prop] !== undefined) { + return env[prop].bind(env); + } + return (...args) => { + throw new Error(`NOT IMPLEMENTED: ${prop} ${args}`); + }; + }, + }); +} + +class RaylibJs { + // will have all raylib functions + ... + + IsMouseButtonPressed(key) { + return this.currentIsMouseButtonPressed == key; + } + + // entrypoints to fetch wasm and start it. + start(wasmPath, canvasId) { + ... + + this.wasm = await WebAssembly.instantiateStreaming(fetch(wasmPath), { + env: make_environment(this), + }); + + // Call the main functions from wasm object + this.wasm.instance.exports.main(); + } + } +``` + +Overall, this was an interesting project to spend a few hours on, and maybe in the future, I’ll explore compiling the C3 standard library to WASM. + +Below are the results! diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/wsl2-ssh/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/wsl2-ssh/index.html Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,20 @@ + + + + + + WSL2 Cloudtop Setup + {{/parts/base_head.html}} + + + + {{/parts/header.html}} +
+
+
+ {{/parts/footer.html}} + + + diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/blog/wsl2-ssh/index.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/blog/wsl2-ssh/index.md Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,139 @@ + +Sorry for going MIA. I hadn’t had any programming-related ideas worth sharing lately, and nothing I wrote in my free time felt ready. But recently, something sparked my interest. + +If you’ve worked at a FAANG company, chances are you’ve used *cloudtop*—essentially SSH’ing into a remote dev server because your laptop can’t realistically run a local version of Facebook or Gmail. Personally, I’ve grown to like the workflow. It gives me a consistent development environment no matter where I go, and it’s fast enough that I don’t run into many issues while coding. + +Also, I should probably use that $2K computer I bought... which has mostly been collecting dust. + +## What am I trying to achieve? + +I have a domain lying around from a startup idea where I planned to post free educational content and maybe get some coffee money out of it—because, yeah, I’m broke. Like most of my ideas, I never got around to finishing it. But now, I’m putting it to use for this setup. + +Here’s the plan: + +1. Point a DNS record to my public IP address. +2. Forward traffic from my router (public IP) to my Windows machine (internal IP). +3. Use Windows `netsh` to proxy ports from Windows to my WSL2 instance. +4. Set up an SSH server inside WSL2 (I'm using Debian). + +You might wonder why I’m using WSL2 instead of just working directly from Windows. It’s simple: I’m a creature of habit. I can’t get used to PowerShell or all the Windows-specific shortcuts. My WSL2 instance runs Debian, but this tutorial should work on most Linux distros with minor adjustments. + +The list above flows from DNS all the way to the WSL2 SSH server. Let’s now go step-by-step and verify each part. + +## Setting up SSH in WSL2 + +We’ll use `openssh-server`. Yes, you could write your own SSH server, but let’s not kid ourselves—just use something battle-tested. + +```bash +sudo apt update +sudo apt install openssh-server +``` + +If you’re not using Debian, substitute with your distro’s package manager. + +Now check if the SSH service is running: + +```bash +sudo systemctl status ssh +``` + +If it's not running: + +```bash +sudo systemctl start ssh +``` + +To start SSH automatically on boot: + +```bash +sudo systemctl enable ssh +``` + +Now SSH is running on your WSL2 instance. But you can’t access it from outside without configuring Windows to forward traffic to WSL2. + +**warning**: You should be careful when you are setting up SSH. Try to use PublicKeyAuthentication over password as you will get many robot visitor trying to login as a root ^_^ + +## Setting up Portproxy on Windows + +I am picking port number 2222 on my window machine, but you can pick whatever port number that is avaialble. To have your Windows host forward to port 22(default SSH port number) on your WSL2 instance (replace the IP with your actual WSL2 IP): + +If you don't know your WSL2 IP address, you can easily see this by using ipconfig in powershell. + +```powershell +netsh interface portproxy add v4tov4 listenport=2222 listenaddress=0.0.0.0 connectport=22 connectaddress= +``` + +To view current portproxy rules: + +```powershell +netsh interface portproxy show all +``` + +If you need to remove it (only do this if you know that port is not being used) + +```powershell +netsh interface portproxy delete v4tov4 listenport=2222 listenaddress=0.0.0.0 +``` + +You should now be able to SSH into your WSL2 instance from your Windows machine: + +```bash +ssh -p 2222 @localhost +``` + +Or from another device on the same network: + +```bash +ssh -p 2222 @ +``` + +## Setting Up Port Forwarding on Your Router + +To make it publicly accessible, forward port 2222 on your router to the internal IP of your Windows machine. + +If you're using Xfinity, you can do this from the Xfinity app. Otherwise, you can often log into your router at `http://10.0.0.1` or `http://192.168.0.1`. Default credentials are often: + +- **Username**: `admin` +- **Password**: `password` (varies by model/provider, so check your router label or ISP’s documentation) + +## Pointing a Domain to Your Public IP + +Once your SSH server is reachable via your public IP, buy a domain from Namecheap or another registrar and set up an A record pointing to that IP. + +However, most residential IPs are dynamic, so they can change. To deal with this, use a dynamic DNS (DDNS) solution. + +I use **Cloudflare** with a small PowerShell script that updates the A record periodically: + +```powershell +$zoneId = "" +$recordId = "" +$apiToken = "" +$recordName = "" +$currentIP = Invoke-RestMethod -Uri "https://api.ipify.org" + +$headers = @{ + "Authorization" = "Bearer $apiToken" + "Content-Type" = "application/json" +} + +$body = @{ + type = "A" + name = $recordName + content = $currentIP + ttl = 120 + proxied = $false +} | ConvertTo-Json + +Invoke-RestMethod -Method PUT ` + -Uri "https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records/$recordId" ` + -Headers $headers ` + -Body $body +``` + +You can schedule this with Windows Task Scheduler to run every 10–15 minutes. + +## That’s It + +TADA! You now have your own *Cloudtop* setup—using your personal machine. It only costs electricity (which might be more than a free AWS tier, lol), but it’s yours. + +In the next post, I’ll show you how to use the exact same steps to host a full-featured server, including self-hosting your own GitHub-like instance. diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/index.html --- a/mrjunejune/src/index.html Fri Jan 02 20:38:02 2026 -0800 +++ b/mrjunejune/src/index.html Sat Jan 03 07:48:07 2026 -0800 @@ -43,15 +43,15 @@ -

During my free time, I like to write codes mostly in C, Python, and Typescript. All in mono repo styles using mercurial and bazel. (I know that is mentally ill...)

-

Feel free to check it out. My bad code..

+

During my free time, I like to write codes mostly in C, Python, and Typescript; all in mono repo styles using bazel. (I know that is mentally ill...)

+

Feel free to check it out my bad code!

Links

{{/parts/footer.html}} diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/public/bunny.wasm Binary file mrjunejune/src/public/bunny.wasm has changed diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/public/node-multitasking.mp4 Binary file mrjunejune/src/public/node-multitasking.mp4 has changed diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/public/raylib.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/public/raylib.js Sat Jan 03 07:48:07 2026 -0800 @@ -0,0 +1,612 @@ +/** + * I Stole this from https://github.com/tsoding/c3-demo/blob/main/raylib.js and added few functions for functionality. + * It seems to be from raylib.com/examples + */ +function make_environment(env) { + return new Proxy(env, { + get(_target, prop, _receiver) { + if (env[prop] !== undefined) { + return env[prop].bind(env); + } + return (...args) => { + throw new Error(`NOT IMPLEMENTED: ${prop} ${args}`); + }; + }, + }); +} + +let iota = 0; +const LOG_ALL = iota++; // Display all logs +const LOG_TRACE = iota++; // Trace logging, intended for internal use only +const LOG_DEBUG = iota++; // Debug logging, used for internal debugging, it should be disabled on release builds +const LOG_INFO = iota++; // Info logging, used for program execution info +const LOG_WARNING = iota++; // Warning logging, used on recoverable failures +const LOG_ERROR = iota++; // Error logging, used on unrecoverable failures +const LOG_FATAL = iota++; // Fatal logging, used to abort program: exit(EXIT_FAILURE) +const LOG_NONE = iota++; // Disable logging + +class RaylibJs { + #FONT_SCALE_MAGIC = 0.65; + + #reset() { + this.previous = undefined; + this.wasm = undefined; + this.ctx = undefined; + this.dt = undefined; + this.targetFPS = 60; + this.entryFunction = undefined; + this.prevPressedKeyState = new Set(); + this.currentPressedKeyState = new Set(); + this.currentMouseWheelMoveState = 0; + /* -1 since 0 is left click, 1 middle, 2 is right click */ + this.currentIsMouseButtonPressed = -1; + this.currentMousePosition = { x: 0, y: 0 }; + this.images = []; + this.quit = false; + } + + constructor() { + this.#reset(); + } + + stop() { + this.quit = true; + } + + async start({ wasmPath, canvasId }) { + if (this.wasm !== undefined) { + console.error("The game is already running. Please stop() it first."); + return; + } + + const canvas = document.getElementById(canvasId); + this.ctx = canvas.getContext("2d"); + if (this.ctx === null) { + throw new Error("Could not create 2d canvas context"); + } + + this.wasm = await WebAssembly.instantiateStreaming(fetch(wasmPath), { + env: make_environment(this), + }); + + const keyDown = (e) => { + this.currentPressedKeyState.add(glfwKeyMapping[e.code]); + }; + const keyUp = (e) => { + this.currentPressedKeyState.delete(glfwKeyMapping[e.code]); + }; + const wheelMove = (e) => { + this.currentMouseWheelMoveState = Math.sign(-e.deltaY); + }; + const mouseMove = (e) => { + this.currentMousePosition = { x: e.clientX, y: e.clientY }; + }; + const mouseClick = (e) => { + this.currentIsMouseButtonPressed = e.button; + }; + const touchStarted = () => { + this.currentIsMouseButtonPressed = 0; + }; + const touchOrClickEnded = () => { + this.currentIsMouseButtonPressed = -1; + }; + window.addEventListener("keydown", keyDown); + window.addEventListener("keyup", keyUp); + window.addEventListener("wheel", wheelMove); + window.addEventListener("mousemove", mouseMove); + window.addEventListener("mousedown", mouseClick); + window.addEventListener("mouseup", touchOrClickEnded); + + /* For phones */ + window.addEventListener("touchstart", touchStarted); + window.addEventListener("touchend", touchOrClickEnded); + window.addEventListener("touchcancel", touchOrClickEnded); + + this.wasm.instance.exports.main(); + const next = (timestamp) => { + if (this.quit) { + this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); + window.removeEventListener("keydown", keyDown); + this.#reset(); + return; + } + this.dt = (timestamp - this.previous) / 1000.0; + this.previous = timestamp; + this.entryFunction(); + window.requestAnimationFrame(next); + }; + window.requestAnimationFrame((timestamp) => { + this.previous = timestamp; + window.requestAnimationFrame(next); + }); + } + + InitWindow(width, height, title_ptr) { + this.ctx.canvas.width = width; + this.ctx.canvas.height = height; + const buffer = this.wasm.instance.exports.memory.buffer; + document.title = cstr_by_ptr(buffer, title_ptr); + } + + WindowShouldClose() { + return false; + } + + SetTargetFPS(fps) { + console.log( + `The game wants to run at ${fps} FPS, but in Web we gonna just ignore it.`, + ); + this.targetFPS = fps; + } + + GetScreenWidth() { + return this.ctx.canvas.width; + } + + GetScreenHeight() { + return this.ctx.canvas.height; + } + + GetFrameTime() { + // TODO: This is a stopgap solution to prevent sudden jumps in dt when the user switches to a differen tab. + // We need a proper handling of Target FPS here. + return Math.min(this.dt, 1.0 / this.targetFPS); + } + + BeginDrawing() {} + + EndDrawing() { + this.prevPressedKeyState.clear(); + this.prevPressedKeyState = new Set(this.currentPressedKeyState); + this.currentMouseWheelMoveState = 0.0; + } + + DrawCircleV(center_ptr, radius, color_ptr) { + const buffer = this.wasm.instance.exports.memory.buffer; + const [x, y] = new Float32Array(buffer, center_ptr, 2); + const [r, g, b, a] = new Uint8Array(buffer, color_ptr, 4); + const color = color_hex_unpacked(r, g, b, a); + this.ctx.beginPath(); + this.ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + this.ctx.fillStyle = color; + this.ctx.fill(); + } + + ClearBackground(color_ptr) { + this.ctx.fillStyle = getColorFromMemory( + this.wasm.instance.exports.memory.buffer, + color_ptr, + ); + this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); + } + + // RLAPI void DrawText(const char *text, int posX, int posY, int fontSize, Color color); // Draw text (using default font) + DrawText(text_ptr, posX, posY, fontSize, color_ptr) { + const buffer = this.wasm.instance.exports.memory.buffer; + const text = cstr_by_ptr(buffer, text_ptr); + const color = getColorFromMemory(buffer, color_ptr); + fontSize *= this.#FONT_SCALE_MAGIC; + this.ctx.fillStyle = color; + // 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 + this.ctx.font = `${fontSize}px grixel`; + + const lines = text.split("\n"); + for (var i = 0; i < lines.length; i++) { + this.ctx.fillText(lines[i], posX, posY + fontSize + i * fontSize); + } + } + + // RLAPI void DrawRectangle(int posX, int posY, int width, int height, Color color); // Draw a color-filled rectangle + DrawRectangle(posX, posY, width, height, color_ptr) { + const buffer = this.wasm.instance.exports.memory.buffer; + const color = getColorFromMemory(buffer, color_ptr); + this.ctx.fillStyle = color; + this.ctx.fillRect(posX, posY, width, height); + } + + DrawRectangleV(position_ptr, size_ptr, color_ptr) { + const buffer = this.wasm.instance.exports.memory.buffer; + const color = getColorFromMemory(buffer, color_ptr); + const position = new Float32Array(buffer, position_ptr, 2); + const size = new Float32Array(buffer, size_ptr, 2); + this.ctx.fillStyle = color; + this.ctx.fillRect(position[0], position[1], size[0], size[1]); + } + + IsKeyPressed(key) { + return ( + !this.prevPressedKeyState.has(key) && this.currentPressedKeyState.has(key) + ); + } + IsKeyDown(key) { + return this.currentPressedKeyState.has(key); + } + GetMouseWheelMove() { + return this.currentMouseWheelMoveState; + } + IsGestureDetected() { + return false; + } + IsMouseButtonPressed(key) { + return this.currentIsMouseButtonPressed == key; + } + + TextFormat(...args) { + // TODO: Implement printf style formatting for TextFormat + return args[0]; + } + + TraceLog(logLevel, text_ptr, ...args) { + // TODO: Implement printf style formatting for TraceLog + const buffer = this.wasm.instance.exports.memory.buffer; + const text = cstr_by_ptr(buffer, text_ptr); + switch (logLevel) { + case LOG_ALL: + console.log(`ALL: ${text} ${args}`); + break; + case LOG_TRACE: + console.log(`TRACE: ${text} ${args}`); + break; + case LOG_DEBUG: + console.log(`DEBUG: ${text} ${args}`); + break; + case LOG_INFO: + console.log(`INFO: ${text} ${args}`); + break; + case LOG_WARNING: + console.log(`WARNING: ${text} ${args}`); + break; + case LOG_ERROR: + console.log(`ERROR: ${text} ${args}`); + break; + case LOG_FATAL: + throw new Error(`FATAL: ${text}`); + case LOG_NONE: + console.log(`NONE: ${text} ${args}`); + break; + } + } + + GetMousePosition(result_ptr) { + const bcrect = this.ctx.canvas.getBoundingClientRect(); + const x = this.currentMousePosition.x - bcrect.left; + const y = this.currentMousePosition.y - bcrect.top; + + const buffer = this.wasm.instance.exports.memory.buffer; + new Float32Array(buffer, result_ptr, 2).set([x, y]); + } + + CheckCollisionPointRec(point_ptr, rec_ptr) { + const buffer = this.wasm.instance.exports.memory.buffer; + const [x, y] = new Float32Array(buffer, point_ptr, 2); + const [rx, ry, rw, rh] = new Float32Array(buffer, rec_ptr, 4); + return x >= rx && x <= rx + rw && y >= ry && y <= ry + rh; + } + + Fade(result_ptr, color_ptr, alpha) { + const buffer = this.wasm.instance.exports.memory.buffer; + const [r, g, b, _] = new Uint8Array(buffer, color_ptr, 4); + const newA = Math.max(0, Math.min(255, 255.0 * alpha)); + new Uint8Array(buffer, result_ptr, 4).set([r, g, b, newA]); + } + + DrawRectangleRec(rec_ptr, color_ptr) { + const buffer = this.wasm.instance.exports.memory.buffer; + const [x, y, w, h] = new Float32Array(buffer, rec_ptr, 4); + const color = getColorFromMemory(buffer, color_ptr); + this.ctx.fillStyle = color; + this.ctx.fillRect(x, y, w, h); + } + + DrawRectangleLinesEx(rec_ptr, lineThick, color_ptr) { + const buffer = this.wasm.instance.exports.memory.buffer; + const [x, y, w, h] = new Float32Array(buffer, rec_ptr, 4); + const color = getColorFromMemory(buffer, color_ptr); + this.ctx.strokeStyle = color; + this.ctx.lineWidth = lineThick; + this.ctx.strokeRect( + x + lineThick / 2, + y + lineThick / 2, + w - lineThick, + h - lineThick, + ); + } + + MeasureText(text_ptr, fontSize) { + const buffer = this.wasm.instance.exports.memory.buffer; + const text = cstr_by_ptr(buffer, text_ptr); + fontSize *= this.#FONT_SCALE_MAGIC; + this.ctx.font = `${fontSize}px grixel`; + return this.ctx.measureText(text).width; + } + + TextSubtext(text_ptr, position, length) { + const buffer = this.wasm.instance.exports.memory.buffer; + const text = cstr_by_ptr(buffer, text_ptr); + const subtext = text.substring(position, length); + + var bytes = new Uint8Array(buffer, 0, subtext.length + 1); + for (var i = 0; i < subtext.length; i++) { + bytes[i] = subtext.charCodeAt(i); + } + bytes[subtext.length] = 0; + + return bytes; + } + + // RLAPI Texture2D LoadTexture(const char *fileName); + LoadTexture(result_ptr, filename_ptr) { + const buffer = this.wasm.instance.exports.memory.buffer; + const filename = cstr_by_ptr(buffer, filename_ptr); + + var result = new Uint32Array(buffer, result_ptr, 5); + var img = new Image(); + const isLocalhost = window.location.hostname === "localhost"; + const baseUrl = isLocalhost + ? "http://localhost:6969/" + : `https://${window.location.hostname}/`; + img.src = `${baseUrl}${filename}`; + this.images.push(img); + + result[0] = this.images.indexOf(img); + // TODO: get the true width and height of the image + result[1] = 256; // width + result[2] = 256; // height + result[3] = 1; // mipmaps + result[4] = 7; // format PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 + + return result; + } + + // RLAPI void DrawTexture(Texture2D texture, int posX, int posY, Color tint); + DrawTexture(texture_ptr, posX, posY, color_ptr) { + const buffer = this.wasm.instance.exports.memory.buffer; + const [id, width, height, mipmaps, format] = new Uint32Array( + buffer, + texture_ptr, + 5, + ); + // // TODO: implement tinting for DrawTexture + // const tint = getColorFromMemory(buffer, color_ptr); + + this.ctx.drawImage(this.images[id], posX, posY); + } + + // TODO: codepoints are not implemented + LoadFontEx( + result_ptr, + fileName_ptr /*, fontSize, codepoints, codepointCount*/, + ) { + const buffer = this.wasm.instance.exports.memory.buffer; + const fileName = cstr_by_ptr(buffer, fileName_ptr); + // TODO: dynamically generate the name for the font + // Support more than one custom font + const font = new FontFace("myfont", `url(${fileName})`); + document.fonts.add(font); + font.load(); + } + + GenTextureMipmaps() {} + SetTextureFilter() {} + + MeasureTextEx(result_ptr, font, text_ptr, fontSize, spacing) { + const buffer = this.wasm.instance.exports.memory.buffer; + const text = cstr_by_ptr(buffer, text_ptr); + const result = new Float32Array(buffer, result_ptr, 2); + this.ctx.font = fontSize + "px myfont"; + const metrics = this.ctx.measureText(text); + result[0] = metrics.width; + result[1] = fontSize; + } + + DrawTextEx(font, text_ptr, position_ptr, fontSize, spacing, tint_ptr) { + const buffer = this.wasm.instance.exports.memory.buffer; + const text = cstr_by_ptr(buffer, text_ptr); + const [posX, posY] = new Float32Array(buffer, position_ptr, 2); + const tint = getColorFromMemory(buffer, tint_ptr); + this.ctx.fillStyle = tint; + this.ctx.font = fontSize + "px myfont"; + this.ctx.fillText(text, posX, posY + fontSize); + } + + GetRandomValue(min, max) { + return min + Math.floor(Math.random() * (max - min + 1)); + } + + ColorFromHSV(result_ptr, hue, saturation, value) { + const buffer = this.wasm.instance.exports.memory.buffer; + const result = new Uint8Array(buffer, result_ptr, 4); + + // Red channel + let k = (5.0 + hue / 60.0) % 6; + let t = 4.0 - k; + k = t < k ? t : k; + k = k < 1 ? k : 1; + k = k > 0 ? k : 0; + result[0] = Math.floor((value - value * saturation * k) * 255.0); + + // Green channel + k = (3.0 + hue / 60.0) % 6; + t = 4.0 - k; + k = t < k ? t : k; + k = k < 1 ? k : 1; + k = k > 0 ? k : 0; + result[1] = Math.floor((value - value * saturation * k) * 255.0); + + // Blue channel + k = (1.0 + hue / 60.0) % 6; + t = 4.0 - k; + k = t < k ? t : k; + k = k < 1 ? k : 1; + k = k > 0 ? k : 0; + result[2] = Math.floor((value - value * saturation * k) * 255.0); + + result[3] = 255; + } + + raylib_js_set_entry(entry) { + this.entryFunction = + this.wasm.instance.exports.__indirect_function_table.get(entry); + } +} + +const glfwKeyMapping = { + Space: 32, + Quote: 39, + Comma: 44, + Minus: 45, + Period: 46, + Slash: 47, + Digit0: 48, + Digit1: 49, + Digit2: 50, + Digit3: 51, + Digit4: 52, + Digit5: 53, + Digit6: 54, + Digit7: 55, + Digit8: 56, + Digit9: 57, + Semicolon: 59, + Equal: 61, + KeyA: 65, + KeyB: 66, + KeyC: 67, + KeyD: 68, + KeyE: 69, + KeyF: 70, + KeyG: 71, + KeyH: 72, + KeyI: 73, + KeyJ: 74, + KeyK: 75, + KeyL: 76, + KeyM: 77, + KeyN: 78, + KeyO: 79, + KeyP: 80, + KeyQ: 81, + KeyR: 82, + KeyS: 83, + KeyT: 84, + KeyU: 85, + KeyV: 86, + KeyW: 87, + KeyX: 88, + KeyY: 89, + KeyZ: 90, + BracketLeft: 91, + Backslash: 92, + BracketRight: 93, + Backquote: 96, + // GLFW_KEY_WORLD_1 161 /* non-US #1 */ + // GLFW_KEY_WORLD_2 162 /* non-US #2 */ + Escape: 256, + Enter: 257, + Tab: 258, + Backspace: 259, + Insert: 260, + Delete: 261, + ArrowRight: 262, + ArrowLeft: 263, + ArrowDown: 264, + ArrowUp: 265, + PageUp: 266, + PageDown: 267, + Home: 268, + End: 269, + CapsLock: 280, + ScrollLock: 281, + NumLock: 282, + PrintScreen: 283, + Pause: 284, + F1: 290, + F2: 291, + F3: 292, + F4: 293, + F5: 294, + F6: 295, + F7: 296, + F8: 297, + F9: 298, + F10: 299, + F11: 300, + F12: 301, + F13: 302, + F14: 303, + F15: 304, + F16: 305, + F17: 306, + F18: 307, + F19: 308, + F20: 309, + F21: 310, + F22: 311, + F23: 312, + F24: 313, + F25: 314, + NumPad0: 320, + NumPad1: 321, + NumPad2: 322, + NumPad3: 323, + NumPad4: 324, + NumPad5: 325, + NumPad6: 326, + NumPad7: 327, + NumPad8: 328, + NumPad9: 329, + NumpadDecimal: 330, + NumpadDivide: 331, + NumpadMultiply: 332, + NumpadSubtract: 333, + NumpadAdd: 334, + NumpadEnter: 335, + NumpadEqual: 336, + ShiftLeft: 340, + ControlLeft: 341, + AltLeft: 342, + MetaLeft: 343, + ShiftRight: 344, + ControlRight: 345, + AltRight: 346, + MetaRight: 347, + ContextMenu: 348, + // GLFW_KEY_LAST GLFW_KEY_MENU +}; + +function cstrlen(mem, ptr) { + let len = 0; + while (mem[ptr] != 0) { + len++; + ptr++; + } + return len; +} + +function cstr_by_ptr(mem_buffer, ptr) { + const mem = new Uint8Array(mem_buffer); + const len = cstrlen(mem, ptr); + const bytes = new Uint8Array(mem_buffer, ptr, len); + return new TextDecoder().decode(bytes); +} + +function color_hex_unpacked(r, g, b, a) { + r = r.toString(16).padStart(2, "0"); + g = g.toString(16).padStart(2, "0"); + b = b.toString(16).padStart(2, "0"); + a = a.toString(16).padStart(2, "0"); + return "#" + r + g + b + a; +} + +function color_hex(color) { + const r = ((color >> (0 * 8)) & 0xff).toString(16).padStart(2, "0"); + const g = ((color >> (1 * 8)) & 0xff).toString(16).padStart(2, "0"); + const b = ((color >> (2 * 8)) & 0xff).toString(16).padStart(2, "0"); + const a = ((color >> (3 * 8)) & 0xff).toString(16).padStart(2, "0"); + return "#" + r + g + b + a; +} + +function getColorFromMemory(buffer, color_ptr) { + const [r, g, b, a] = new Uint8Array(buffer, color_ptr, 4); + return color_hex_unpacked(r, g, b, a); +} diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/public/vscode-slow.gif Binary file mrjunejune/src/public/vscode-slow.gif has changed diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/public/vscode.png Binary file mrjunejune/src/public/vscode.png has changed diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/tools/markdown_to_html/index.css --- a/mrjunejune/src/tools/markdown_to_html/index.css Fri Jan 02 20:38:02 2026 -0800 +++ b/mrjunejune/src/tools/markdown_to_html/index.css Sat Jan 03 07:48:07 2026 -0800 @@ -38,7 +38,7 @@ textarea { width: 100%; - height: 500px; + height: 700px; padding: 15px; background: rgb(var(--gray-light)); color: var(--darkgray); @@ -57,7 +57,6 @@ .title { display: grid; - place-items: center; grid-template-columns: 1fr 1fr; margin-bottom: 10px; } @@ -77,29 +76,29 @@ background: rgb(var(--gray-light)); } -#output h1, #output h2, #output h3, #output h4, #output h5, #output h6 { +h1, h2, h3, h4, h5, h6 { margin: 20px 0 10px 0; color: var(--darkgray); } -#output h1 { font-size: 2em; } -#output h2 { font-size: 1.5em; } -#output h3 { font-size: 1.3em; } +h1 { font-size: 2em; } +h2 { font-size: 1.5em; } +h3 { font-size: 1.3em; } -#output p { +p { margin: 10px 0; } -#output ul, #output ol { +ul, ol { margin: 10px 0; padding-left: 30px; } -#output li { +li { margin: 5px 0; } -#output code { +code { background: rgb(var(--gray-light)); padding: 2px 6px; border-radius: 3px; @@ -107,22 +106,23 @@ font-size: 0.9em; } -#output pre { +pre { background: #282c34; color: #abb2bf; padding: 15px; border-radius: 4px; overflow-x: auto; margin: 10px 0; + text-wrap: auto; } -#output pre code { +pre code { background: none; color: inherit; padding: 0; } -#output blockquote { +blockquote { border-left: 4px solid rgb(var(--gray-light)); padding-left: 15px; margin: 10px 0; @@ -130,35 +130,35 @@ font-style: italic; } -#output a { +a { color: var(--accent); text-decoration: none; } -#output a:hover { +a:hover { text-decoration: underline; } -#output hr { +hr { border: none; border-top: 2px solid rgb(var(--gray-light)); margin: 20px 0; } -#output img { +img { max-width: 100%; height: auto; } -#output strong { +strong { font-weight: bold; } -#output em { +em { font-style: italic; } -#output del { +del { text-decoration: line-through; } @@ -197,7 +197,7 @@ } textarea { - height: 300px; + height: 400px; font-size: 16px; } @@ -222,7 +222,7 @@ font-size: 1rem; } - #output h1 { font-size: 1.75em; } - #output h2 { font-size: 1.5em; } - #output h3 { font-size: 1.25em; } + h1 { font-size: 1.75em; } + h2 { font-size: 1.5em; } + h3 { font-size: 1.25em; } } diff -r 684edfaf93b7 -r 65e5a5b89a4e mrjunejune/src/wabbit_alpha.png Binary file mrjunejune/src/wabbit_alpha.png has changed diff -r 684edfaf93b7 -r 65e5a5b89a4e seobeo/s_web.c --- a/seobeo/s_web.c Fri Jan 02 20:38:02 2026 -0800 +++ b/seobeo/s_web.c Sat Jan 03 07:48:07 2026 -0800 @@ -222,6 +222,7 @@ else if (strstr(file_path, ".svg")) mime = "image/svg+xml"; else if (strstr(file_path, ".ico")) mime = "image/x-icon"; else if (strstr(file_path, ".json")) mime = "application/json"; + else if (strstr(file_path, ".wasm")) mime = "application/wasm"; Seobeo_Log(SEOBEO_DEBUG, "File path: %s\nBody Size: %zu\n", file_path, body_size);