GNU Emacs에 새로 추가된 Canvas 기능, API, 그리고 Emacs Lisp 및 동적 모듈을 이용한 활용법을 소개합니다.
Divya 작성
2026년 8월 23일
Canvas 패치가 마침내 업스트림 GNU Emacs에 병합되었습니다. 제 Mastodon을 팔로우해 왔다면, 이것이 약 8개월에 걸친 여정이었다는 것을 아실 겁니다. 하지만 마침내 코어 Emacs에, 2025년 초 Emacs Reader 프로젝트를 시작했을 때 제가 절실히 원했던 기능이 들어왔습니다. 즉, 이미지 데이터를 문자열에 넣어서 Emacs를 막히게 하지 않고 Emacs 안에서 이미지를 갱신하고 조작하는 방법입니다. 이 한계를 극복하기 위해 여러 해킹을 거쳤으며, 어떤 것은 다른 것보다 성공적이었습니다. 하지만 멋진 Daniel, 즉 minad가 동적 모듈 API를 통해 Emacs 내부의 픽셀 버퍼를 노출하자는 아이디어를 제안했고, 이제 그 아이디어는 Emacs의 기능이 되었습니다. 이는 Emacs 32 릴리스 주기에 포함될 예정입니다. 이에 관한 더 자세한 역사는 나중에 쓰겠습니다. 지금은 이 글에서 새 기능과 API, 그리고 이를 사용해 할 수 있는 일을 소개합니다. 초보자도 이해할 수 있도록 최대한 독립적으로 설명하겠습니다. 하지만 먼저 이 기능에 관해 사람들이 가질 수 있는 몇 가지 오해를 바로잡아야 합니다.
아니요. 웹 프레임워크나 기술과는 아무 관련이 없습니다. Emacs 버퍼의 표면에 사용자가 임의로 그릴 수 있게 해 주기 때문에 이를 “canvas”라고 부릅니다. 따라서 높은 수준에서는 HTML5와 Emacs canvas가 같은 일을 하지만, 구현이나 작동 방식에는 전혀 관계가 없습니다. HTML5 Canvas에는 그리기 함수 등을 갖춘 더 풍부한 API도 있습니다. 현재 Emacs의 canvas API는 기존 이미지 API와 통합되며 동적 모듈 API를 통해 기반 픽셀 버퍼에 접근할 수 있게 할 뿐입니다. 동적 모듈을 통한 Canvas 사용을 참고하세요.
좋은 질문입니다! Emacs 버퍼 수준에서 canvas는 SVG를 포함한 다른 Emacs 이미지와 똑같이 보이고 동작합니다. SVG 및 다른 이미지와 다른 점은 다음과 같습니다.
* canvas의 `:data`는 단순한 ARGB32 픽셀 데이터(문자열 또는 벡터)이며, 특정 압축 형식이 아닙니다.
* 저수준에서 Emacs Lisp 이미지 객체에 접근하여 조작할 수 있습니다.
* 전체 재표시를 호출하지 않고 이미지를 갱신할 수 있습니다.
3. “그래도 Emacs의 재표시 엔진에 제한되는 것 아닌가요?”
완전히 그렇지는 않습니다. canvas를 갱신할 때마다 호출하는 canvas-refresh 함수가 있는데, 이 함수는 Emacs의 redisplay를 사용하거나 호출하지 않습니다. 해당 canvas와 관련된 버퍼의 특정 글리프만 갱신하는 자체 다시 그리기 경로를 갖고 있습니다. 따라서 변경된 모든 글리프를 갱신하려는 전체 재표시는 비용이 더 들지만, canvas-refresh는 필요한 것만 갱신합니다. Emacs 25부터 이중 버퍼링을 지원하므로 프레임에서 이를 활성화했다면 버퍼를 전환하려고 redisplay를 호출해야 하지만, 그것이 canvas를 느리게 하지는 않습니다. 확신이 들지 않는다면, Emacs 세션의 나머지 부분에 지연을 일으키지 않고 25, 30, 60FPS에서 부드럽게 실행되는 아래 시연을 보세요. 또는 Emacs가 조금도 버벅이지 않고 2K60 FPS 및 720p 30FPS 동영상을 나란히 표시하는 이것을 보세요!
단순히 필요하지 않기 때문입니다 :D! canvas는 픽셀 버퍼를 노출할 뿐이고, Emacs는 그것이 변경됨에 따라 단순히 재표시합니다. 원한다면 Emacs 밖에서 픽셀을 하드웨어 가속으로 처리한 다음 canvas의 픽셀 버퍼에 넣을 수 있습니다. 그러면 픽셀 계산, 텍스처 연산 등 가장 필요한 곳에서 하드웨어 가속을 얻을 수 있습니다. 이상적으로는 Emacs 자체에 GPU 기반 재표시가 도입되면 훌륭하고 분명 도움이 되겠지만, 현재 Emacs에서도 canvas를 통해 HW 가속의 많은 이점을 얻을 수 있습니다.
그 질문에 답하기 전에 “GNU Emacs에서 이미지란 무엇인가?”를 물어야 합니다. 누구나 수십 년 동안 Emacs가 다양한 형식의 멋진 이미지를 표시하는 모습을 보았습니다. Emacs는 어떻게 이를 수행할까요? Emacs 버퍼 수준에는 이름에서 알 수 있듯 버퍼 내 문자 위치나 문자열을 위한 속성 목록(일명 plist)인 텍스트 속성이 있습니다(오버레이 속성도 참고하세요). 텍스트가 가질 수 있는 많은 속성 중에는 display 속성이라는 특별한 것이 있습니다. 이 특별한 속성이 텍스트의 표시 방식을 담당합니다. 바로 이것을 이용해 이미지를 표시합니다! create-image를 사용해 정확히 plist인 이미지 객체를 만든 다음, 이 이미지 객체(이미지 명세라고 합니다)를 어떤 텍스트나 오버레이의 display 속성으로 설정합니다. 오버레이는 “텍스트” 없이 일반 텍스트 속성과 같은 속성을 가지며, 특정 시작과 끝을 가진 특정 버퍼 소속 객체입니다.
이것이 GNU Emacs에서 이미지가 동작하는 API의 간략한 설명입니다. 새 canvas 기능은 이 API와 완전히 호환됩니다. 요약하면, Emacs 버퍼 수준에서 canvas는 _그저 또 하나의 이미지 유형_입니다! 실제로 최신 GNU Emacs를 소스에서 컴파일하고 (image-type-available-p 'canvas)를 평가하면 t가 반환되어야 합니다.
그러면 즉시 이런 질문이 나옵니다. Emacs가 이미 이미지를 지원했고 canvas가 같은 API를 따른다면, 이것의 요점은 무엇일까요? XBM, PPM, PNG, GIF, SVG 등이 있는데 왜 새 이미지 유형이 필요할까요? 짧은 답은 두 가지입니다.
SVG는 충분히 단순한 한 대체로 훌륭한 일을 할 수 있지만, 높은 갱신 빈도에서 복잡한 그래픽을 다루게 되는 순간 Emacs가 막히기 시작합니다. 2025년에 제가 원한 것은 정확히 이를 우회하는 방법, 즉 문자열이나 Emacs Lisp 객체를 가비지 컬렉터에 통과시킬 필요 없이 픽셀을 조작하여 제자리에서 갱신할 수 있도록 Emacs가 표시하는 이미지에 픽셀 수준으로 접근하는 방법이었습니다. 그리고 그것이 바로 canvas가 제공하는 것입니다.
canvas는 다음 두 수준에서 보고 사용할 수 있습니다.
이는 픽셀 버퍼를 직접 조작하는 저수준 효율성을 희생하지 않으면서 기존 Emacs 이미지 API와 완전히 통합할 수 있게 해 줍니다. 이제 이 두 방식으로 canvas를 각각 또는 동시에 사용하는 방법을 보이겠습니다.
일반 Emacs 이미지처럼 create-image를 사용하거나 이미지 명세 plist를 직접 만들어 이미지를 생성할 수 있습니다. canvas의 경우 후자 방식은 다음과 같습니다.
(setq test-canvas `(image :type canvas :id test :data-width 10 :data-height 10 :data nil))
이것이 canvas 이미지 명세의 모습입니다. 이것과 다른 이미지의 유일한 차이점은(명백한 :type canvas 외에) canvas 이미지에는 :id 속성이 _필요하다_는 것입니다. 이는 각 canvas를 고유하게 식별하는 데 사용됩니다.
대신 (setq test-canvas (create-image nil 'canvas t))로 canvas를 만들었다면 거의 똑같이 보일 것입니다.
(image :type canvas :data nil :scale default :id g138)
물론 두 경우 모두 data를 제공하지 않았으므로 기술적으로 유효한 canvas 이미지는 아니지만, 형태는 이와 같습니다. 좀 더 흥미롭게 만들어 봅시다. canvas가 받는 “data”는 ARGB32 픽셀 배열이라는 점을 기억하세요. Emacs Lisp에서는 유니바이트 문자열이나 벡터를 통해 이를 수행할 수 있습니다. 후자가 보이기 조금 더 쉬우므로 계속 그것을 사용하겠습니다.
먼저 픽셀 배열을 만듭니다. make-vector를 사용합니다.
;; A red square of 10x0 size (setq rect-vec (make-vector (* 10 10) #xFFFF0000))
이는 각 요소가 #xFFFF0000(빨간색) 32비트 ARGB 픽셀인 10x10 크기의 벡터를 만듭니다. 말 그대로 빨간색을 담은 픽셀 단위 배열 벡터입니다. 이는 canvas에 유효한 데이터이므로 이제 직접 만들 수 있습니다.
(setq rect-canvas `(image :type canvas :id rect :data-width 10 :data-height 10 :data ,rect-vec))
이제 간단히 표시할 수 있습니다.
(insert (propertize "#" 'display rect-canvas))
위 내용을 평가하면 버퍼에 작은 빨간 정사각형이 보일 것입니다! 짜잔, 작은 canvas가 생겼습니다!
SVG로 작은 직사각형을 훨씬 더 빨리 만들 수 있으므로 이것이 그다지 유용해 보이지 않을 수 있습니다. canvas의 진정한 가치는 전체 재표시 없이 canvas를 갱신할 수 있는 canvas-refresh 함수 사용에서 나옵니다. 이 함수는 특정 canvas를 덮는 글리프만 건드리고 즉시 갱신합니다. 모든 변경 사항을 갱신하려는 전체 redisplay 호출보다 이는 훨씬 저렴합니다.
또한 canvas-refresh에는 RELOAD-DATA라는 추가 선택 인수가 있습니다. 이것이 non-nil이면 canvas에서 새로 갱신된 데이터(:data 속성 또는 :file 변경)를 다시 불러옵니다. 이 함수를 올바르게 사용하는 방법은 뒤에서 보이겠습니다.
서론에서 말했듯 canvas는 Emacs Lisp 및/또는 동적 모듈을 통해 사용할 수 있습니다. Emacs의 동적 모듈과 그 효과적인 사용법을 더 알아보려면, 이에 관한 제 이전 글을 참고하세요. Emacs 32는 동적 모듈에서 canvas를 다루는 간단한 API를 제공합니다. 아이디어는 간단합니다. canvas를 생성한 뒤 canavs_data 함수를 호출하기만 하면 canvas와 연결된 픽셀 버퍼를 제공합니다. 다시 말하지만 이 버퍼는 ARGB32 픽셀 데이터에만 유효합니다.
static emacs_value Fcanvas_update(emacs_env* env, ptrdiff_t nargs, emacs_value args[], void* data) { emacs_value canvas = env->args[0]; uint32_t *canvas_pixel = env->canvas_data(env, canvas); // accessing canvas' pixel buffer if (canvas_pixel) { // do some pixel manipulation to the canvas pixel } }
env->funcall(env, env->intern(env, "canvas-refresh"), (emacs_value[])[rect_canvas, Qnil]);
위 동적 모듈 함수는 정상적인 동적 모듈에 넣고 컴파일한 뒤 module-load로 불러오면, 색상을 변경하거나(또는 픽셀 조작 코드가 수행하는 작업을 통해) canvas를 반복해서 갱신하게 됩니다. 직접 조작할 수 있는 canvas의 저수준 표현에 접근함으로써 Emacs Lisp의 한계를 넘어설 수 있습니다. 또한 canvas-refresh가 Emacs의 전체 redisplay에 따른 병목을 피하므로 전혀 느려지지 않습니다! 동적 모듈을 통해 canvas를 사용할 때는 Emacs Lisp 이미지 명세의 :data를 사용하지 않으므로 canvas-refresh의 RELOAD-DATA 인수는 nil이어야 합니다.
여기에서는 Emacs Lisp와 동적 모듈 모두에서 canvas로 수행할 수 있는 몇 가지 멋진 그래픽 애니메이션을 보여 줍니다. 이전 시연은 Emacs 안팎에서 제가 했던 모든 실험을 모은 Tushar의 블로그 글을 참고하세요. 기본 시연(멋진 모드 줄 시연 포함)이 있는 Minad의 emacs-canvas-patch도 보세요. 아래 시연 중 처음 두 개는 순수 Emacs Lisp이고, 마지막 하나는 동적 모듈을 통한 C입니다.
canvas를 만들고 설정하는 것으로 시작합시다. 배경은 짙은 보라색(#xFF8B00FF)으로 하겠습니다.
(setq W 250 H 250 BG #xFF8B00FF) (setq ball-canvas-vec (make-vector (* W H) BG)) (setq ball-canvas `(image :type canvas :id rect :data-width ,W :data-height ,H :data ,ball-canvas-vec)) (insert (propertize "#" 'display ball-canvas))
이것은 이전 절에서 한 것과 정확히 같고, 단지 보라색일 뿐입니다! 이제 흥미로운 부분입니다. 이 250x250 픽셀 canvas 안에서 공이 튀게 만들고 싶습니다. 먼저 공을 준비합니다. 공에는 반지름, 초기 위치, 초기 속도(그리고 색상!)가 있습니다.
(setq ball-radius 10) (setq bx (/ W 2) by (/ H 2)) ; start from the center (setq dx 4 dy 3) ; initial velocity (setq ball-color #xFFFFFF00) ; yellow
이제 실제로 공을 그려야 합니다. 이를 위해 해석기하학을 조금 떠올려 보세요! 공을 그리는 방식은 한 점을 원의 중심으로 삼고, 특정 경계 상자 안에 원을 만들 수 있는지 확인하는 것입니다. ball-radius가 15이면 지름은 30이므로, 원을 넣으려면 적어도 30x30 정사각형이 필요합니다. 이제 실제 원을 그리기 위해 원의 방정식을 사용하겠습니다.
이를 Emacs Lisp로 옮기면 다음과 같습니다.
(defun draw-ball (cx cy color) (let ((r2 (* ball-radius ball-radius))) ; Radius squared ;; the nested loop checks for the circle within the square (dotimes (y-off (1+ (* 2 ball-radius))) (dotimes (x-off (1+ (* 2 ball-radius))) (let* ((px (+ cx (- x-off ball-radius))) (py (+ cy (- y-off ball-radius))) (dx-local (- px cx)) ; Horizontal distance from center (dy-local (- py cy)) ; Vertical distance from center (dist2 (+ (* dx-local dx-local) (* dy-local dy-local)))) ; squared distance using pythagorean theorem ;; check if pixel is inside the circle AND inside the canvas bounds (when (and (>= px 0) (< px W) (>= py 0) (< py H) (<= dist2 r2)) (aset ball-canvas-vec (+ (* py W) px) color)))))))
위 함수는 이해될 것입니다. 공을 그리는 가장 효율적인 방법은 아니지만 이 시연에는 비교적 충분히 단순합니다. 이제 공이 canvas 안에 머무르게 하는 물리를 확인하기만 하면 됩니다.
(defun ball-bounce () (setq bx (+ bx dx) by (+ by dy)) ;; compare with ball-radius so the edge of the circle bounces, not the center. (when (<= bx ball-radius) (setq dx (abs dx)) (setq bx ball-radius)) (when (>= bx (- W ball-radius)) (setq dx (- (abs dx))) (setq bx (- W ball-radius))) (when (<= by ball-radius) (setq dy (abs dy)) (setq by ball-radius)) (when (>= by (- H ball-radius)) (setq dy (- (abs dy))) (setq by (- H ball-radius))))
이제 반복문으로 실행하면 됩니다!
(setq ball-frame 0) (setq max-ball-frames 2000) (defun ball-loop () (if (>= ball-frame max-ball-frames) (cancel-timer ball-timer) (setq ball-frame (1+ ball-frame)) (draw-ball bx by BG) ; make old ball invisible (turn it into background color) (ball-bounce) (draw-ball bx by ball-color) (canvas-refresh ball-canvas t))) (setq ball-timer (run-with-timer 0 0.02 'ball-loop))
모두 평가하면 보라색 정사각형 안에서 노란 공이 튀는 것을 볼 수 있습니다. 2000프레임 뒤 자동으로 멈추지만 무한히 계속하게 할 수도 있습니다! 다음과 같이 보일 것입니다.
명백히 여기서 모든 수학을 자세히 다룰 수는 없습니다. 관심 있다면 고전인 Goldstein, Poole & Sako의 Classical Mechanics 또는 Landau & Lifshitz의 _Course of Theoretical Physics, Vol. I_를 독자에게 권합니다. 하지만 독자 여러분은 아마 Lisper일 것이므로, 더 나은 책은 Sussman & Wisdom의 _Structure and Interpretation of Classical Mechanics_일 것입니다.
처음 고전역학을 공부할 때 저를 매료시킨 것 중 하나는 이중 진자였습니다. 이의 기반 수학인 오일러-라그랑주 방정식과 일반적인 변분법 또한 제게 매우 흥미로웠습니다. 그렇지 않다고 생각한다면 브라키스토크론 곡선의 역사와 수학을 살펴보길 권합니다. 이 모든 것으로 보아, 이를 canvas를 사용한 시각화로 만들고 싶다는 것은 분명합니다!
혼돈계를 설명하는 라그랑지언의 두 방정식은 다음과 같습니다.
이제 평소처럼 검은색 배경의 canvas를 만들고 이전처럼 표시해 봅시다.
(setq W 250 H 250 BG #xFF101010) (setq canvas-vec (make-vector (* W H) BG)) (setq canvas `(image :type canvas :id dp :data-width ,W :data-height ,H :data ,canvas-vec)) (insert (propertize "#" 'display canvas))
중력, 막대 길이, 시간 간격이라는 상수를 설정합니다.
(setq g 9.8 L 60.0 dt 0.05)
각도 및 , 각속도 및 의 초기 조건과 고정된 앵커 지점을 설정합니다.
(setq th1 2.5 th2 2.5 w1 0.0 w2 0.0) (setq pivot-x 125 pivot-y 80)
도우미 몇 가지가 필요합니다. 앞서 만든 공용 함수와, 선/막대를 그리는 함수 및 벡터에 픽셀을 설정하는 함수가 필요합니다.
(defun set-px (x y color) (when (and (>= x 0) (< x W) (>= y 0) (< y H)) (aset canvas-vec (+ (* (truncate y) W) (truncate x)) color)))
(defun draw-line (x0 y0 x1 y1 color) (let* ((x0 (truncate x0)) (y0 (truncate y0)) (x1 (truncate x1)) (y1 (truncate y1)) (dx (abs (- x1 x0))) (dy (abs (- y1 y0))) (sx (if (< x0 x1) 1 -1)) (sy (if (< y0 y1) 1 -1)) (err (- dx dy)) e2) (while (not (and (= x0 x1) (= y0 y1))) (set-px x0 y0 color) (setq e2 (* 2 err)) (when (> e2 (- dy)) (setq err (- err dy)) (setq x0 (+ x0 sx))) (when (< e2 dx) (setq err (+ err dx)) (setq y0 (+ y0 sy)))) (set-px x0 y0 color)))
(defun draw-circ (cx cy r color) (let ((r2 (* r r))) (dotimes (y (* 2 r)) (dotimes (x (* 2 r)) (let* ((px (+ (- cx r) x)) (py (+ (- cy r) y)) (dx (- px cx)) (dy (- py cy))) (when (<= (+ (* dx dx) (* dy dy)) r2) (set-px px py color)))))))
이제 진자의 움직임을 처리하는 주 절차입니다.
(defun pendulum-physics () (let* ((dth (- th1 th2)) (den (- 3.0 (cos (* 2.0 dth)))) (num1 (+ (* (- g) 3.0 (sin th1)) (* (- g) (sin (- th1 (* 2.0 th2)))) (* (- 2.0) (sin dth) (+ (* w2 w2 L) (* w1 w1 L (cos dth)))))) (a1 (/ num1 (* L den))) (num2 (* 2.0 (sin dth) (+ (* 2.0 w1 w1 L) (* 2.0 g (cos th1)) (* w2 w2 L (cos dth))))) (a2 (/ num2 (* L den)))) (setq w1 (+ w1 (* a1 dt)) w2 (+ w2 (* a2 dt))) (setq th1 (+ th1 (* w1 dt)) th2 (+ th2 (* w2 dt)))))
위 코드가 수학을 따르는지 확인하는 일은 독자의 연습문제로 남겨 둡니다! 특히 처음에 소개한 두 방정식과 위 절차를 비교해 보세요. 이제 주 게임 루프입니다.
(defun pendulum-loop () (dotimes (i 12) (pendulum-physics)) (fillarray canvas-vec BG) (let* ((x1 (+ pivot-x (* L (sin th1)))) (y1 (+ pivot-y (* L (cos th1)))) (x2 (+ x1 (* L (sin th2)))) (y2 (+ y1 (* L (cos th2))))) (draw-line pivot-x pivot-y x1 y1 #xFF888888) (draw-line x1 y1 x2 y2 #xFF888888) (draw-circ x1 y1 8 #xFF00FF00) (draw-circ x2 y2 8 #xFFFF0000) (set-px pivot-x pivot-y #xFFFFFFFF)) (canvas-refresh canvas t)) (setq my-timer (run-with-timer 0 0.05 'pendulum-loop))
평가하면 대략 다음과 같이 보일 것입니다.
마우스로 진자를 끌어 초기 위치를 설정할 수 있게 하여 여기서 상호작용을 활성화할 수 있으며, Emacs의 track-mouse 기능으로 충분히 가능합니다. 너무 복잡해질 것이므로 여기에는 포함하지 않겠습니다.
이 시연은 tsoding으로도 알려진 Alexey Kutepov에게서 가져왔습니다. 그는 olive.c라는 그래픽 라이브러리를 만들었습니다. 다음 시연은 olive.c의 Dots3D 예제입니다. 이는 동적 모듈이므로 모든 픽셀 조작 수학은 C에서 일어나며, Emacs Lisp 쪽에서는 canvas를 만들고 표시한 뒤 타이머에서 동적 모듈 함수를 호출해 canvas를 갱신하기만 합니다. 원래 시연에는 마우스 상호작용이 없었지만, 우리 버전에는 아주 쉽게 넣을 수 있습니다.
필수적인 int plugin_is_GPL_compatible를 작성한 후 동적 모듈에서 몇 가지 상수를 정의합니다.
#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
원래 시연 코드에서 그대로 가져온 것입니다. 이제 이 격자/공간에서 실제 원/점을 그리는 도우미 하나가 필요합니다. 앞서 원을 그리기 위해 작성한 코드를 C로 변환하면 됩니다.
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 (xx + 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; } } } }
이제 Emacs에서 호출할 실제 모듈 함수를 작성해야 합니다. 우선 이 함수의 시그니처는 무엇이어야 할까요? 모듈 함수이므로 다음과 같아야 합니다.
static emacs_value render(emacs_env* env, ptrdiff_t nargs, emacs_value args[], void* data)
그렇다면 호출할 때 인수는 무엇이어야 할까요? 우선 렌더링할 canvas가 필요합니다. 격자는 회전해야 하므로 전체 격자를 렌더링할 각도도 인수로 필요하며, 이 각도는 계속 변합니다. 따라서 그 인수를 받고 canvas의 픽셀 버퍼에도 접근해 봅시다.
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;
}
이제 배경을 칠해야 하므로 모든 픽셀을 순회하며 BACKGROUND_COLOR로 설정합니다. 이어질 수학을 위한 float 값도 몇 개 가져옵니다. 움직임을 위해 카메라 수학이 필요하고, 3차원이므로 3단계 중첩 for 반복문이 필요합니다. 최종 함수는 다음과 같습니다.
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; // How far the camera is from the center (Lower = closer) float focal_length = 800.0f; // The "zoom" multiplier (Higher = more zoomed in)
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);
// 1. Rotate around Y axis (Yaw) float x1 = px * cos_y + pz * sin_y;
float z1 = -px * sin_y + pz * cos_y;
float y1 = py;
// 2. Rotate around X axis (Pitch) float y2 = y1 * cos_x - z1 * sin_x;
float z2 = y1 * sin_x + z1 * cos_x;
float x2 = x1;
// 3. Apply camera distance float z_cam = z2 + camera_distance;
// 4. Perspective projection using focal length 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;
}
이후 모듈을 초기화할 수 있습니다.
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;
}
이제 공유 객체로 컴파일해야 합니다. gcc가 인클루드를 찾는 위치에 emacs-module.h를 추가하는 것을 잊지 마세요.
gcc -O2 -I%ssrc dots3d.c -o /tmp/dots3d.so -fPIC -shared -lm
이제 Emacs Lisp에서 이를 사용할 수 있습니다. 먼저 몇 가지 준비 작업을 합니다.
(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))
주 canvas를 설정합니다.
(setq dots3d-canvas '(image :type canvas :data-width 960 :data-height 720 :margin (20 . 20) :scale 1 :id dots3d))
모드 줄과 커서를 비활성화해야 합니다.
(setq-local cursor-type nil mode-line-position nil mode-line-modified nil mode-line-mule-info nil mode-line-remote nil)
그리고 canvas를 표시합니다.
(insert (propertize "#" 'display dots3d-canvas))
이제 공간과의 상호작용을 처리하는 주 함수입니다. 기본적으로 canvas를 끌 때마다 새 각도로 render 함수를 호출하여 canvas를 갱신하는 데 track-mouse를 사용합니다. 끄는 동안 자동 회전을 멈춰야 한다는 점에 유의하세요.
(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)
Emacs Lisp의 최종 렌더 함수는 매우 간단할 수 있습니다. 약간 조정된 각도로 render 함수를 호출하고, 계속 회전하도록 타이머에서 호출하기만 합니다.
(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)
평가한 뒤에는 아래와 같은 모습이 보이고, 격자를 끌어 움직일 수 있습니다.
분명하듯 이 기능은 Emacs 안에서 그래픽 기능의 새로운 세계를 열었습니다. 이런 가능성은 몇 곳에서 표면만 조금 훑었을 뿐입니다. 해커들이 이것을 이용해 어떤 애플리케이션을 만들지 매우 기대됩니다. Emacs에서 canvas 위에 구축할 수 있는 몇 가지 가능한 애플리케이션은 다음과 같습니다(그중 일부는 우리가 직접 작업할 수도 있습니다).
그 외에도 더 있습니다… Emacs 내부의 그래픽 애플리케이션과 그것들을 조합 가능하게 만드는 방법에 관한 토론을 열었으니, 자유롭게 그곳에 기여해 주세요.
https://github.com/minad/emacs-canvas-patchhttps://github.com/minad/emacs-shader-demo/https://elpa.nongnu.org/nongnu/doom-game.htmlhttps://github.com/mochar/glmakie-elhttps://codeberg.org/MonadicSheep/pale/https://tusharhero.codeberg.page/emacs-pale-canvas-and-stuff-demos.html마지막 빌드: 2026-08-23
커밋: ebfb81d