For a project I'm working on I wanted to draw a SVG image in Emacs and update it when the user clicks on it. After digging through the documentation for a while, I found a rather nice solution.
This page contains the code for a simple SVG editor where dragging the mouse draws a line and right-clicking draws a circle.
Imports
Emacs provides a library for creating SVG images. We also need
subr
and
subr-x
for some helper functions.
(require 'subr) (require 'subr-x) (require 'svg)
Variables
All constants a defined with variables. A more serious application
should probably use
defcustom
for this.
There are also buffer-local variables to keep track of the lines and circles that have been drawn, and whether the grid should be shown.
(defvar mouse-demo-width 800 "Width of the mouse-demo SVG.") (defvar mouse-demo-height 800 "Height of the mouse-demo SVG.") (defvar mouse-demo-grid-size 40 "Size of the optional grid.") (defvar-local mouse-demo-lines nil "Lines in the current buffer.") (defvar-local mouse-demo-circles nil "Circles in the current buffer.") (defvar-local mouse-demo-grid nil "Enable/disable the grid.")
Keymap and Mode Definition
Handlers for mouse events can be defined in the keymap of a mode. On
my mouse,
mouse-1
is the left button and
mouse-3
is the right one.
See the Emacs Documentation for more details on supported mouse event types.
(defvar mouse-demo-mode-map (let ((map (make-sparse-keymap))) (define-key map [drag-mouse-1] #'mouse-demo-handle-drag) (define-key map [down-mouse-3] #'mouse-demo-handle-click) (define-key map (kbd "g") #'mouse-demo-toggle-grid) (define-key map (kbd "c") #'mouse-demo-clear) map) "Keymap for `mouse-demo-mode'.") (define-minor-mode mouse-demo-mode "Minor mode for drawing lines and circles in SVG images. \\{mouse-demo-mode-map}" :init-value nil :lighter " mouse-demo" :keymap mouse-demo-mode-map :group 'mouse-demo)
There is no key-binding for saving the image as this can be done
with the
image-save
command.
Positions Relative to an Image
Each time a drag- or click-event occurs, we want to find out the x-y position of the event relative to the image.
(posn-object posn)
returns the object at
posn
.
For images, this is a list
(image :key1 value1 :key2 value2 ...)
.
One thing to look out for is that images might be scaled when inserting them in a buffer, so we need to divide the x and y coordinates by the scale of the image.
I have also included a snap-to-grid functionality, rounding x and y to the nearest point on a grid.
(defun mouse-demo-image-xy (posn) "If POSN is on an image, return a position '(x . y)' relative to the image." (let ((obj (posn-object posn))) (when (eq 'image (car obj)) (let* ((scale (plist-get (cdr obj) :scale)) (posn-xy (posn-object-x-y posn)) (x (/ (car posn-xy) scale)) (y (/ (cdr posn-xy) scale))) (if mouse-demo-grid (cons (* mouse-demo-grid-size (round x mouse-demo-grid-size)) (* mouse-demo-grid-size (round y mouse-demo-grid-size))) (cons x y))))))
Event Handlers
Event handlers are called interactively and with
(interactive "e")
they receive the event as their argument.
(event-start event)
returns the position where the event started,
the drag-event also has a
(event-end event)
.
(defun mouse-demo-toggle-grid () "Toggle the grid." (interactive) (setq mouse-demo-grid (not mouse-demo-grid)) (mouse-demo-redraw)) (defun mouse-demo-clear () "Remove all lines and circles." (interactive) (setq mouse-demo-lines '() mouse-demo-circles '()) (mouse-demo-redraw)) (defun mouse-demo-handle-drag (event) "Draw a line from the start of EVENT to its end." (interactive "e") (when-let ((start (mouse-demo-image-xy (event-start event))) (end (mouse-demo-image-xy (event-end event)))) (push (cons start end) mouse-demo-lines) (mouse-demo-redraw))) (defun mouse-demo-handle-click (event) "Draw a circle at the position of EVENT." (interactive "e") (when-let ((xy (mouse-demo-image-xy (event-start event)))) (push xy mouse-demo-circles) (mouse-demo-redraw)))
Drawing SVGs
(defun mouse-demo-svg () "Generate a SVG with lines, circles and an optional grid." (let ((svg (svg-create mouse-demo-width mouse-demo-height))) ;; Background (svg-rectangle svg 2 2 (- mouse-demo-width 4) (- mouse-demo-height 4) :fill-color "white" :stroke-color "black" :stroke-width 4) ;; Grid (when mouse-demo-grid (dolist (x (number-sequence 0 mouse-demo-width mouse-demo-grid-size)) (svg-line svg x 0 x mouse-demo-height :stroke-color "gray" :stroke-width 1)) (dolist (y (number-sequence 0 mouse-demo-height mouse-demo-grid-size)) (svg-line svg 0 y mouse-demo-width y :stroke-color "gray" :stroke-width 1))) ;; Lines (dolist (line mouse-demo-lines) (svg-line svg (caar line) (cdar line) (cadr line) (cddr line) :stroke-color "black" :stroke-width 4 :stroke-linecap "round")) ;; Circles (dolist (circle mouse-demo-circles) (svg-circle svg (car circle) (cdr circle) 20 :fill-color "red")) (svg-image svg)))
Displaying SVGs
All that's left to do is to open a buffer in
mouse-demo-mode
and
insert the SVG we've drawn.
The
(insert "\n ")
adds some padding at the left and the top.
(defun mouse-demo-buffer () (get-buffer-create "*mouse-demo*")) (defun mouse-demo-redraw () "Redraw the buffer." (with-current-buffer (mouse-demo-buffer) (goto-char (point-min)) (erase-buffer) (insert "\n ") (insert-image (mouse-demo-svg)))) (defun mouse-demo () (interactive) (with-current-buffer (mouse-demo-buffer) (mouse-demo-mode) (mouse-demo-clear) (switch-to-buffer (current-buffer))))