MLX42 in the web Tutorial

17 days

2025 06

Tutorial explaining all steps to publish your mlx42 code to the web for free.

MLX42 makes it possible to compile your C ready to deploy in a static webpage with WebAssembly by using https://emscripten.org/

Let's start with an awesome Makefile that I tailored for you:

NAME = pulga_web

CC = cc
CFLAGS = -O3 -Wall -Wextra -Werror -Iincludes -IMLX42/include

SRCS = $(wildcard srcs/*.c)

OBJS = $(SRCS:.c=.o)

MLX_NATIVE_LIB = MLX42/build/libmlx42.a
MLX_WEB_LIB = MLX42/build_web/libmlx42_web.a
WEB = web/demo.html
MLX_FLAGS = -LMLX42/build -lmlx42 -ldl -lglfw -lm -lpthread

all: $(MLX_NATIVE_LIB) $(NAME)

$(NAME): $(OBJS)
  $(CC) $(CFLAGS) $(OBJS) -o $(NAME) $(MLX_FLAGS)

$(MLX_NATIVE_LIB):
  @cd MLX42 && cmake -B build
  @cd MLX42 && cmake --build build -j4
  @-mv MLX42/build/libmlx42.a $(MLX_NATIVE_LIB)

$(MLX_WEB_LIB):
  @cd MLX42 && emcmake cmake -B build_web
  @cd MLX42 && cmake --build build_web --parallel
  @-mv MLX42/build_web/libmlx42.a $(MLX_WEB_LIB)

clean:
  rm -f $(OBJS)

fclean: clean
  rm -f $(NAME)

web: $(WEB)

$(WEB): $(SRCS) $(MLX_WEB_LIB)
  mkdir -p web
  emcc -DWEB -O3 -I include -I MLX42/include -pthread $(SRCS) \
    -o $(WEB) \
    $(MLX_WEB_LIB) \
    -s USE_GLFW=3 -s USE_WEBGL2=1 -s FULL_ES3=1 -s WASM=1 \
    -s NO_EXIT_RUNTIME=1 -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \
    -s ALLOW_MEMORY_GROWTH

re: fclean all

.PHONY: all clean fclean re $(WEB)


This Makefile comes with a cool feature which allows you to compile your project natively (for quick testing on the usual MLX window) and also for the web, when you are ready to deploy it in a website.

make # normal make, compiles your project with libmlx42
make web # web compilation with emcc , uses libml42_web

The Makefile currently produces all the files you will need for your web deployment
WEB = web/demo.html

Later I will show you how to tailor this, for now I'll demonstrate how this works with an example I prepared for you:


#include <MLX42/MLX42.h>
#include <time.h>
#include <stdlib.h>
#include <stdio.h>

#define WIDTH 800
#define HEIGHT 600

#ifdef WEB
# include <emscripten.h>
# include <emscripten/html5.h>
#endif

static mlx_t        *mlx;
static mlx_image_t  *image;

static int32_t ft_pixel(int32_t r, int32_t g, int32_t b, int32_t a)
{
    return (r << 24 | g << 16 | b << 8 | a);
}

static void ft_randomize(void* param)
{
  (void)param;
  for (uint32_t i = 0; i < image->width; ++i)
  {
    for (uint32_t y = 0; y < image->height; ++y)
    {
      uint32_t color = ft_pixel(
        rand() % 0xFF, // R
        rand() % 0xFF, // G
        rand() % 0xFF, // B
        rand() % 0xFF  // A
      );
      mlx_put_pixel(image, i, y, color);
    }
  }
}

static void ft_hook(void* param)
{
  mlx_t* mlx = param;

  if (mlx_is_key_down(mlx, MLX_KEY_ESCAPE))
    mlx_close_window(mlx);
  if (mlx_is_key_down(mlx, MLX_KEY_UP))
    image->instances[0].y -= 5;
  if (mlx_is_key_down(mlx, MLX_KEY_DOWN))
    image->instances[0].y += 5;
  if (mlx_is_key_down(mlx, MLX_KEY_LEFT))
    image->instances[0].x -= 5;
  if (mlx_is_key_down(mlx, MLX_KEY_RIGHT))
    image->instances[0].x += 5;
}

#ifdef WEB
static void emscripten_main_loop(void)
{
  mlx_loop(mlx);
}
#endif

int main(int argc, char **argv) {
  (void)argc;(void)argv;
  if (!(mlx = mlx_init(WIDTH, HEIGHT, "MLX42", true)))
  {
    puts(mlx_strerror(mlx_errno));
    return(EXIT_FAILURE);
  }
  if (!(image = mlx_new_image(mlx, 128, 128)))
  {
    mlx_close_window(mlx);
    puts(mlx_strerror(mlx_errno));
    return(EXIT_FAILURE);
  }
  if (mlx_image_to_window(mlx, image, 0, 0) == -1)
  {
    mlx_close_window(mlx);
    puts(mlx_strerror(mlx_errno));
    return(EXIT_FAILURE);
  }

  mlx_loop_hook(mlx, ft_randomize, mlx);
  mlx_loop_hook(mlx, ft_hook, mlx);

#ifdef WEB
  emscripten_set_main_loop(emscripten_main_loop, 0, true);
#else
  mlx_loop(mlx);
#endif
  mlx_terminate(mlx);

  return EXIT_SUCCESS;
}

This should be your file structure:
includes/
srcs
└── main.c
MLX42
├── build
├── build_web
├── cmake
├── CMakeLists.txt
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── docs
├── include
├── lib
├── LICENSE
├── README.md
├── SECURITY.md
├── shaders
├── src
├── tests
├── tools
└── web
web
├── demo.html
├── demo.js
└── demo.wasm
Makefile

In order to understand the main.c you can refer to the official explanation in the MLX42 web directory: https://github.com/codam-coding-college/MLX42/tree/master/web

Then you can access index.html with a webserver, I use python's easy solution:

python3 -m http.server 8080
Finally let's publish it in a website.

Upload your code to a GitHub repository (it can be private)

echo "# mlx_web_example" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/pulgamecanica/mlx_web_example.git
git push -u origin main
make fclean
git add .
git commit -m "second commit"
git push
Go To Settings
Go to Pages
Open the "Branch" dropdown
Select the "main" branch

This will automatically publish your files in the main branch to a web server. Your files will be indexed, this mean that the demo.html which was generated by emscripten will also be available. I'll explain how you can get the link... 
You might see this warning if you repo is private (this is normal)
Go to the repo's settings
Toggle the box to use the GitHub Pages website URL
Toggle it again to deactivate it, we just wanted it to pre-fill the URL
Add the suffix /web/demo.html at the end of the link so it goes directly to the page!
You can visit my example website here:

https://pulgamecanica.github.io/mlx_web_example/web/demo.html

Now we will explore how you can change the website aspect because the default one is not very nice.

First let's understand the html which was generated automatically:

Starting with the "head"
In the head there is nothing too interesting, you can see some css and the tile, not that the page is not using viewport scaling, you might want to add this for phone compatibility, depending on the project specifications.

Let's start customizing the page, change the title and move the css to a style.css file.

The new head looks like this:

<head>
  <meta charset=utf-8>
  <meta content="text/html; charset=utf-8"http-equiv=Content-Type>
  <title>Pulga Page</title>
  <link rel="stylesheet" href="style.css"> 
</head>

You will need to create the style.css and put the css there, this helps dealing with the css and making changes easier.

We'll get back to the style.css later and I will give you some cool ideas.

For now let's move to the body and understand what is happening.
We have the following elements:

- image base64 encoded link <a>
- spinner <div>
- status <div>
- controls <span>
- progress <div>
-  webgl canvas <div>
- terminal <textarea>

Start by removing the image link <a> and the controls <span>
Now your body should look like this:

<div id="background"></div>

<div id="main-container">
  <div class="status-panel">
    <div class="spinner" id="spinner"></div>
    <div class="emscripten" id="status">Downloading...</div>
    <progress hidden id="progress" max="100" value="0"></progress>
  </div>

  <div class="canvas-container">
    <canvas
      class="emscripten"
      id="canvas"
      oncontextmenu="event.preventDefault()"
      tabindex="-1"
    ></canvas>
  </div>

  <button id="toggle-terminal">Toggle Terminal</button>
  <textarea id="output" rows="8" hidden></textarea>
</div>

Also add the following to the js script:

const toggleBtn = document.getElementById("toggle-terminal");

toggleBtn.addEventListener("click", () => {
  outputElement.hidden = !outputElement.hidden;
});

And replace the style.css with the following

/* Background styling */
body, html {
  margin: 0;
  padding: 0;
  height: 100%;
  font-family: 'Press Start 2P', monospace;
  background: linear-gradient(to bottom right, #1b1c2b, #0f0f0f);
  color: #33ff33;
  overflow: hidden;
}

#background {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  background-image: radial-gradient(#3f3f3f 1px, transparent 1px);
  background-size: 16px 16px;
  opacity: 0.1;
}

/* Layout container */
#main-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 1rem;
  height: 100%;
  box-sizing: border-box;
}

/* Canvas focus */
.canvas-container {
  flex-grow: 1;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 1rem;
}

canvas {
  border: 4px solid #33ff33;
  box-shadow: 0 0 12px #33ff33;
  width: 90vw;
  height: 60vh;
  max-width: 1024px;
  background: black;
}

/* Status elements */
.status-panel {
  text-align: center;
  margin-bottom: 1rem;
}

.spinner {
  width: 24px;
  height: 24px;
  border: 4px solid #33ff33;
  border-top: 4px solid transparent;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: auto;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

progress {
  width: 60%;
  height: 10px;
  margin-top: 0.5rem;
}

/* Terminal output */
#output {
  width: 90%;
  max-width: 1000px;
  background: black;
  color: #33ff33;
  border: 2px solid #33ff33;
  padding: 0.5rem;
  resize: none;
  font-size: 12px;
  font-family: 'Courier New', monospace;
  margin-top: 1rem;
}

/* Toggle button */
#toggle-terminal {
  background-color: black;
  border: 2px solid #33ff33;
  color: #33ff33;
  font-family: 'Press Start 2P', monospace;
  font-size: 10px;
  padding: 0.5rem 1rem;
  margin-top: 1rem;
  cursor: pointer;
}

#toggle-terminal:hover {
  background-color: #111;
  box-shadow: 0 0 6px #33ff33;
}

/* Responsive tweaks */
@media (max-width: 768px) {
  canvas {
    width: 95vw;
    height: 50vh;
  }

  #output {
    font-size: 10px;
  }
}

Finally this is how it should look like:
Finally, in order to avoid emcc from overwriting your modified html file, you should definately change in the makefile the following line:

WEB = web/demo.html

TO

WEB = web/demo.js  

This way emcc will know to only generate the html

Also if your project loads anything, you will need to preload it like so:

emcc -DWEB -O3
    ... ... ...
    -s ALLOW_MEMORY_GROWTH \
    -s ALLOW_MEMORY_GROWTH \
    --preload-file <your_assets_directory>


Hope you liked it! :D