This demo is borrowed from Alexey Kutepov, aka tsoding. They built a graphics library called olive.c. The following demo is Dots3D example from olive.c. Since this is a dynamic module, all the pixel manipulation math would be happening in C and on the Emacs Lisp side we will take care of creating and displaying the canvas and just calling the dynamic module function in a timer to update the canvas. While the original demo didn’t have any mouse interactivity, we can include it in ours quite easily.
After doing the mandatory int plugin_is_GPL_compatible we define some constants in the dynamic module:
#define WIDTH 960 #define HEIGHT 720 #define BACKGROUND_COLOR 0xFF181818 #define GRID_COUNT 10 #define GRID_PAD (0.5f/GRID_COUNT) #define GRID_SIZE ((GRID_COUNT - 1)*GRID_PAD) #define CIRCLE_RADIUS 5 #define Z_START 0.25f
We’ve just inherited them from the original demo’s code. Now we need one helper to draw the actual circles/points in this grid/space. We can just convert into C the code we wrote earlier for drawing circles:
void draw_circle(uint32_t *pixels, int cx, int cy, int r, uint32_t color) { int r2 = r * r; for (int y = -r; y <= r; ++y) { for (int x = -r; x <= r; ++x) { if (x*x + y*y <= r2) { int px = cx + x; int py = cy + y; if (px >= 0 && px < WIDTH && py >= 0 && py < HEIGHT) pixels[py * WIDTH + px] = color; } } } }
Now we need to write the actual module function that will be called from Emacs. Firstly, what should this function’s signature be? Since it’s a module function, it has to be like this:
static emacs_value render(emacs_env* env, ptrdiff_t nargs, emacs_value args[], void* data)
But what should be its arguments when called? Well, firstly it needs the canvas where it will render to. It will also need as arguments the angles from which to render the whole grid, because the grid must be rotating, so the angles will be changing continuously. So let’s get those arguments, and get access to the canvas’ pixel buffer as well:
static emacs_value render(emacs_env* env, ptrdiff_t nargs, emacs_value args[], void* data) { emacs_value canvas = args[0]; float angle_x = env->extract_float(env, args[1]); float angle_y = env->extract_float(env, args[2]); uint32_t* pixels = env->canvas_data(env, canvas); if (!pixels) return Qnil; }
Now the background must be painted, for which we’ll just loop through all the pixels and set them to BACKGROUND_COLOR. We’ll also get some float valuees for the upcoming math. We clearly need some camera math to make it move as well, and since we are in 3D we’ll need a 3-level nested for loop. So here’s what the final function looks like:
static emacs_value render(emacs_env* env, ptrdiff_t nargs, emacs_value args[], void* data) { emacs_value canvas = args[0]; float angle_x = env->extract_float(env, args[1]); float angle_y = env->extract_float(env, args[2]); uint32_t* pixels = env->canvas_data(env, canvas); if (!pixels) return Qnil; for(int i = 0; i < WIDTH * HEIGHT; ++i) pixels[i] = BACKGROUND_COLOR; float cos_x = cosf(angle_x), sin_x = sinf(angle_x); float cos_y = cosf(angle_y), sin_y = sinf(angle_y); float camera_distance = 0.8f; float focal_length = 800.0f; for (int ix = 0; ix < GRID_COUNT; ++ix) { for (int iy = 0; iy < GRID_COUNT; ++iy) { for (int iz = 0; iz < GRID_COUNT; ++iz) { float x = ix*GRID_PAD - GRID_SIZE/2.0f; float y = iy*GRID_PAD - GRID_SIZE/2.0f; float z = Z_START + iz*GRID_PAD; float px = x; float py = y; float pz = z - (Z_START + GRID_SIZE/2.0f); float x1 = px * cos_y + pz * sin_y; float z1 = -px * sin_y + pz * cos_y; float y1 = py; float y2 = y1 * cos_x - z1 * sin_x; float z2 = y1 * sin_x + z1 * cos_x; float x2 = x1; float z_cam = z2 + camera_distance; float screen_x = (x2 / z_cam) * focal_length + WIDTH / 2.0f; float screen_y = (y2 / z_cam) * focal_length + HEIGHT / 2.0f; uint32_t r = ix*255/GRID_COUNT; uint32_t g = iy*255/GRID_COUNT; uint32_t b = iz*255/GRID_COUNT; uint32_t color = 0xFF000000 | (r << 16) | (g << 8) | b; draw_circle(pixels, (int)screen_x, (int)screen_y, CIRCLE_RADIUS, color); } } } return Qnil; }
After this we can just initialize the module:
int emacs_module_init(struct emacs_runtime *rt) { if ((size_t)rt->size < sizeof (*rt)) return 1; emacs_env* env = rt->get_environment(rt); if ((size_t)env->size < sizeof (*env)) return 2; Qnil = env->make_global_ref(env, env->intern(env, "nil")); env->funcall(env, env->intern(env, "defalias"), 2, (emacs_value[]){ env->intern(env, "dots3d-render"), env->make_function(env, 3, 3, render, "Render dots3d", 0) }); return 0; }
This now needs to be compiled into a shared object, do not forget to add emacs-module.h wherever gcc looks for includes:
gcc -O2 -I%ssrc dots3d.c -o /tmp/dots3d.so -fPIC -shared -lm
Now we can use this from Emacs Lisp, some preliminary stuff:
(module-load "/tmp/dots3d.so") (declare-function dots3d-render "ext:dots3d.c") (switch-to-buffer (get-buffer-create "*dots3d*")) (defvar dots3d-canvas) (defvar dots3d-frame 0) (defvar dots3d-time 0.0) (defvar dots3d-last-time 0.0) (defvar dots3d-angle-x 0.0) (defvar dots3d-angle-y 0.0) (defvar dots3d-auto-rotate t) (setq dots3d-time (float-time)) (setq dots3d-last-time (float-time))
We setup the main canvas:
(setq dots3d-canvas '(image :type canvas :data-width 960 :data-height 720 :margin (20 . 20) :scale 1 :id dots3d))
We need to disable the mode-line and cursor:
(setq-local cursor-type nil
mode-line-position nil
mode-line-modified nil
mode-line-mule-info nil
mode-line-remote nil)
And we display the canvas:
(insert (propertize "#" 'display dots3d-canvas))
Now the main function that takes care of interacting with the space. We basically use track-mouse to update the canvas by calling the render function with new angles, every time we drag on the canvas. To be noted, we need to stop the auto-rotate while we are dragging.
(defun dots3d-start-drag (event) (interactive "e") (setq dots3d-auto-rotate nil) (let* ((start-pos (posn-object-x-y (event-start event))) (last-x (car start-pos)) (last-y (cdr start-pos))) (when (and last-x last-y) (track-mouse (let (evt pos mx my) (while (progn (setq evt (read-event)) (mouse-movement-p evt)) (setq pos (posn-object-x-y (event-start evt))) (when (and (car pos) (cdr pos)) (setq mx (car pos) my (cdr pos)) (setq dots3d-angle-y (+ dots3d-angle-y (* (- mx last-x) 0.01))) (setq dots3d-angle-x (+ dots3d-angle-x (* (- my last-y) 0.01))) (setq last-x mx last-y my) (dots3d-render dots3d-canvas dots3d-angle-x dots3d-angle-y) (canvas-refresh dots3d-canvas)))))) (setq dots3d-auto-rotate t))) (local-set-key [down-mouse-1] 'dots3d-start-drag)
And the final render function in Emacs Lisp can be extremely simple, you just call the render function with slightly adjusted angles and we call it in a timer so that it keeps rotating:
(defun dots3d-update () (let* ((time (float-time)) (dt (- time dots3d-last-time))) (switch-to-buffer (get-buffer-create "*dots3d*")) (setq dots3d-last-time time) (when dots3d-auto-rotate (setq dots3d-angle-y (+ dots3d-angle-y (* dt 0.5)))) (dots3d-render dots3d-canvas dots3d-angle-x dots3d-angle-y) (canvas-refresh dots3d-canvas))) (run-with-timer nil (/ 1 60.0) 'dots3d-update)
And after evaluation, you should see as below and be able to move the grid by dragging it: