Categories: sharing » drawing

RSS - Atom - Subscribe via email

Feline feelings

| drawing, cat

Feel free to use this under the Creative Commons Attribution License.

Text from sketch

Feline feelings sachachua.com/2025-03-26-01

  • happy
    • playful
    • content
    • interested
    • proud
    • accepted
    • powerful
    • peaceful
    • trusting
    • optimistic
  • surprised
    • startled
    • confused
    • amazed
    • excited
  • bad
    • tired
    • busy
    • stressed
    • bored
  • fearful
    • scared
    • anxious
    • weak
    • rejected
    • insecure
    • threatened
  • angry
    • let down
    • humiliated
    • bitter
    • mad
    • aggressive
    • frustrated
    • distant
    • critical
  • disgusted
    • disapproving
    • disappointed
    • awful
    • repelled
  • sad
    • lonely
    • vulnerable
    • despair
    • guilty
    • depressed
    • hurt

Feelings wheel by Geoffrey Roberts

I want to draw more expressively, and experimenting with distinguishing between emotions seems like a good start. I followed up on our idea of drawing cats after Stick figure out feelings. It was a lot of fun drawing various kitties based on Geoffrey Roberts' emotion wheel. It turns out I'm still sometimes iffy on what a cat looks like in different poses, but maybe enough of the cat-ness has come through in these little doodles. =)

Related posts:

You might also like:

View org source for this post

Stick figure out feelings

Posted: - Modified: | drawing

Feel free to use or remix these stick figures under the Creative Commons Attribution License.

Text and links from sketch

Stick figure out feelings https://sachachua.com/2025-03-22-02

Feelings wheel by Geoffrey Roberts, stick figures by Sacha Chua and the kiddo

  • happy
    • playful
    • content
    • interested
    • proud
    • accepted
    • powerful
    • peaceful
    • trusting
    • optimistic
  • surprised
    • startled
    • confused
    • amazed
    • excited
  • bad
    • tired
    • busy
    • stressed
    • bored
  • fearful
    • scared
    • anxious
    • weak
    • rejected
    • insecure
    • threatened
  • angry
    • let down
    • humiliated
    • bitter
    • mad
    • aggressive
    • frustrated
    • distant
    • critical
  • disgusted
    • disapproving
    • disappointed
    • awful
    • repelled
  • sad
    • lonely
    • vulnerable
    • despair
    • guilty
    • depressed
    • hurt

I had fun drawing stick figures based on Geoffrey Roberts' emotion wheel while waiting for A+. After she finished her class, she sat with me, suggesting some ways to improve the expressions and even adding her own flair.

We imagined another sketch showing cats expressing the different emotions. A+ already has a title for that sketch: "Feline feelings." It'll be a good challenge, figuring out how to draw cats clearly enough to show those emotions. Related: this sketch of bird stickers on r/Supernote.

Other variations: drawing other emotion wheels, like the Plutchik wheel or the Junto wheel. Some wheels vary emotional intensity, which will be a nice exercise.

A challenge: working on the outer ring of emotions. How do I distinguish "disillusioned" from "perplexed"? What about "free" from "joyful"?

I found my copy of Bikablo Emotions, and I'm looking forward to picking up more tips from it. I remember flipping through it for my post on sketchnotes: Building my visual vocabulary (2013). So many things to explore… =)

[2025-03-26 Wed]: Follow-up: Feline feelings

View org source for this post

Playing with sketching again

| art, drawing

After our first informal field trip to the Art Gallery of Ontario, I got my own 13" iPad Air so that I can play with digital painting beside A+. Using the same apps might make it easier for her to pick up ideas from me and for me to pick up ideas from her. We mostly draw in Procreate, and I'm starting to get the hang of its brushes and features.

It's been nice doing moments from daily life again. It's been a while since I got to play with colour this easily.

A+ and I are both interested in piano, singing, and drawing, so we're experimenting with the Simply family subscription (CAD 46.49+tax/month). A+ likes to draw in the evening as a way of postponing bedtime. I could probably find lots of free drawing tutorials like the ones that Simply Draw has, but it's nice that it's already set up with the video in a corner and it pauses at the appropriate steps, so A+ can independently do it. She's starting to see shapes and shade a bit better now, although she doesn't yet have the patience to blend things slowly. I'm developing that patience, yay me. I wish I could zoom in on the reference image, though.

Here are some drawings I made following the Simply Draw tutorials.

One of the things I like about digital drawing and painting is that I can sneak up on blending by using different opacity settings and colours instead of either accurately controlling the pressure in my hand or switching between pencil and eraser. I also love the way I can use layers to build up an image gradually, how I can erase or undo, and how I can just use whatever colour I want without having to hunt for the right colour pencils or put things away afterwards. I haven't really played around with drawing with art supplies, although the watercolour tutorials that cross my feed seem fascinating. Maybe someday.

There's a glimmer here of how this might become a relaxing thing to do, different from untangling a thought or condensing a book into a sketchnote. I'm slowly getting to the point where, when I notice I'm starting to get anxious or when I'm tempted to nag A+ about procrastination, I can tell myself that I'm going on an art/music break instead and that usually keeps me busy enough until the urge passes. I think this might be useful for our sanity, especially if A+ picks up the idea too. When I'm on a music break, she often gets inspired to kick me off the piano and do her own music lessons, so that's a win. Art is something we can do side by side, and I can always make a drawing more elaborate since A+ likes to stay at roughly the same stage as I am.

I remember enjoying art enough as a kid to have fun at a summer camp where we did things like sand art and papier mache. I think I worked on an illustration of a sparrow that made it into a book of poetry or something like that. By the time we got to drafting classes in high school, I was feeling a bit more meh about art. I got back into art again with the Colors app on the Nintendo DS and then ended up getting into drawing and sketchnoting. I'd like to play around more with colour, and maybe I'll do more doodles and more drawing just because. I like drawing nature, and I'd like to get better at drawing characters too. I'll put the sketches on my blog and in my online sketches, and it'll fun to see how I grow over time.

View org source for this post

Sketching practice: Beaver, goose, squirrel, sparrow, flower, sheepdog and sheep

| drawing, art

A+'s class is working through a variety of assignments while reading through The Wild Robot. They've done chapter 1-11 so far. One of the assignments is to visualize things from the book, like sketching 6 things Roz has seen in nature so far. I figured I'd practise drawing too.

References:

A+ thought that Roz encountered a beaver, but I think she might have mixed it up with the otters. It was fun to draw a beaver anyway. I'm getting the hang of blocking out the shapes with a highlighter and then going over it with the pen.

The sheepdog wasn't from the story. It's from another reflection that I've been noodling on about how A+'s teacher often tries to herd 17 kids to be on the same literal page during virtual class. It's a hard job.

Learning about sheepdogs sent me on this fun tangent

A tangent on herding dogs: heelers (Heelers! Like Bluey!) nip at the heels; headers stare down the animals with a strong eye; some breeds use both methods and also run along the backs of the sheep; some are moderate to loose-eyed; some use barks; some are tending dogs who fence the sheep in. Fascinating. This Reddit thread is interesting too. And sheepdog training tips sound surprisingly relevant, like the importance of figuring out what distance the dog is ready to work at (which is not always the same as the distance the dog thinks they are ready to work at). Sometimes I'm the shepherd, sometimes I'm the sheepdog, sometimes I'm the sheep I want to herd.

As for A+ and art, she still gets very frustrated. "I can't do it!" she wails. But she's starting to be able to say things like "I see there's a circle here." I think it might be helpful for me to borrow a bunch of drawing books that emphasize sketching on top of basic shapes, instead of those drawing videos that just tell you the lines and curves to draw. Maybe Ed Emberley's drawing books. It might also be interesting to look through some digital art tutorials and tips, like this thread on the Procreate forum (oooh, monsters with eyes). Getting even more tempted to get an iPad for myself so that we can learn side by side. I've tried drawing on Android tablets/phablets before and Medibang Paint was pretty nice, but one of my goals is making it easier to bounce ideas and discoveries off each other.

Could be fun.

View org source for this post

Hyperlinking SVGs

| drawing, supernote, emacs
Text and links from sketch

Hyperlinking SVGs - 2025-01-17-01

I like drawing my notes. I can jump around, draw connections, doodle for fun.

A sketch can only fit so much, though. (even if I write really small)

Idea: Links: They can be signposts for other trails.

Process:

I want to make maps for myself and other people.

This is easy to do because:

  • SVGs are XML, a text format
  • Emacs has code for XML and SVG manipulation, display
  • You can use Emacs to build a simple user interface.
  • Ideas
  • TO-DO: update sketch viewer
    • prioritize SVG
    • display Org

SuperNote also has its own hyperlinks, but:

  • typing long URLS on on-screen keyboards is not fun
  • I can't figure out how to convert those links to SVG
  • Rects are more compact

Preprocessing the image

This isn't the focus of this blog post, but I thought I'd include the code anyway in case someone might find it useful.

The fastest way to get a single file off the Supernote is to enable Browse & Access by swiping down from the top. It's the icon that looks like a two-way arrow between waves.

2025-01-21_10-28-16.png
Figure 1: Browse and Access

I have some Emacs Lisp code for downloading the latest exported file using the Supernote's web server.

my-supernote-get-exported-files
(defvar my-supernote-ip-address "192.168.1.221")
(defun my-supernote-get-exported-files ()
  (condition-case nil
      (let ((data (plz 'get (format "http://%s:8089/EXPORT" my-supernote-ip-address)))
            (list))
        (when (string-match "const json = '\\(.*\\)'" data)
          (sort
           (alist-get 'fileList (json-parse-string (match-string 1 data) :object-type 'alist :array-type 'list))
           :key (lambda (o) (alist-get 'date o))
           :lessp 'string<
           :reverse t)))
    (error nil)))

my-supernote-download-latest-exported-file: Save exported file in downloads dir.
(defun my-sketch-insert-latest-doodle ()
  (interactive)
  (let* ((file (my-latest-sketch)))
    (insert
     (format
      "#+begin_right-doodle
#+ATTR_HTML: :title
%s
#+end_right-doodle"
      (org-link-make-string (concat "file:" file))))))

(defun my-supernote-download-latest-exported-file ()
  "Save exported file in downloads dir."
  (interactive)
  (let* ((info (car (my-supernote-get-exported-files)))
         (dest-dir my-download-dir)
         (new-file (and info (expand-file-name (file-name-nondirectory (alist-get 'name info)) dest-dir)))
         renamed)
    (when info
      (copy-file
       (plz 'get (format "http://%s:8089%s" my-supernote-ip-address
                         (alist-get 'uri info))
         :as 'file)
       new-file
       t)
      new-file)))

Once I've downloaded the file, I process it:

  1. my-image-recognize: use Google Cloud Vision to recognize the text, rename it based on the ID
  2. my-sketch-rename: rename the file based on the ID if I've written one on the sketch
  3. my-sketch-convert-pdf: convert to SVG, copying over the links from the previous SVG if one exists
  4. my-sketch-clean: remove any images or templates
  5. my-sketch-color-to-hex: change the hex values for easier replacement and tinkering
  6. my-sketch-add-bg: add a plain white background rectangle
  7. my-sketch-change-fill-to-style: make the attributes more consistent
  8. my-sketch-recolor: change the highlight colour from gray to light yellow
  9. my-image-store: store it in either my private-sketches directory or my sketches directory, depending on the tags in the filename; leave untitled sketches in the same directory

my-supernote-process-sketch
(defun my-supernote-process-sketch (file)
  (interactive "FFile: ")
  (my-image-recognize file)
  (setq file (my-sketch-rename file))
  (pcase (file-name-extension file)
    ((or "svg" "pdf")
     (setq file
           (my-image-store
            (my-sketch-svg-prepare file))))
    ((or "png" "jpg" "jpeg")
     (setq file
           (my-image-store
            (my-image-autorotate
             (my-image-autocrop
              (my-sketch-recolor-png
               file)))))))
  file)

my-sketch-svg-prepare: Clean up SVG for publishing.
(defvar my-debug-buffer (get-buffer-create "*temp*"))
(defun my-sketch-convert-pdf (pdf-file)
  "Returns the SVG filename."
  (interactive "FPDF: ")
  (if-let ((links (and (file-exists-p (concat (file-name-sans-extension pdf-file) ".svg"))
                       (dom-by-tag
                        (car (xml-parse-file (concat (file-name-sans-extension pdf-file) ".svg")))
                        'a))))
      ;; copy links over
      (let ((temp-file (concat (make-temp-name "svg-conversion") ".svg"))
            new-file)
        (unwind-protect
            (progn
              (call-process "pdftocairo" nil my-debug-buffer nil "-svg" (expand-file-name pdf-file)
                            temp-file)
              (setq new-file (car (xml-parse-file temp-file)))
              (dolist (link links)
                (dom-append-child new-file link))
              (with-temp-file (file-exists-p (concat (file-name-sans-extension pdf-file) ".svg"))
                (svg-print new-file)))
          (error
           (delete-file temp-file))))
    (delete-file (concat (file-name-sans-extension pdf-file) ".svg"))
    (call-process "pdftocairo" nil my-debug-buffer nil "-svg" (expand-file-name pdf-file)
                  (expand-file-name (concat (file-name-sans-extension pdf-file) ".svg"))))
  (concat (file-name-sans-extension pdf-file) ".svg"))

(defun my-sketch-change-fill-to-style (dom)
  "Inkscape handles these better when we split paths."
  (dolist (path (dom-by-tag dom 'path))
    (when (dom-attr path 'fill)
      (dom-set-attribute
       path 'style
       (if (dom-attr path 'style)
           (concat (dom-attr path 'style) ";fill:" (dom-attr path 'fill))
         (concat "fill:" (dom-attr path 'fill))))
      (dom-remove-attribute path 'fill)))
  dom)

(defun my-sketch-recolor (dom color-map &optional selector)
  "Colors are specified as ((\"#input\" . \"#output\") ...)."
  (if (symbolp color-map)
      (setq color-map
            (assoc-default color-map my-sketch-color-map)))
  (let ((map-re (regexp-opt (mapcar 'car color-map))))
    (dolist (path (if selector (dom-search dom selector)
                    (dom-by-tag dom 'path)))
      (dolist (attr '(style fill))
        (when (and (dom-attr path attr)
                   (string-match map-re (dom-attr path attr)))
          (dom-set-attribute
           path attr
           (replace-regexp-in-string
            map-re
            (lambda (match)
              (assoc-default match color-map))
            (or (dom-attr path attr) "")))))))
  dom)

(defun my-sketch-add-bg (dom)
  ;; add background rectangle
  (unless (dom-search dom (lambda (elem) (and (dom-attr elem 'class) (string-match "\\<background\\>" (dom-attr elem 'class)))))
    (let* ((view-box (mapcar 'string-to-number (split-string (dom-attr dom 'viewBox))))
           (bg-node (dom-node 'rect `((x . 0)
                                      (y . 0)
                                      (class . "background")
                                      (width . ,(elt view-box 2))
                                      (height . ,(elt view-box 3))
                                      (fill . "#ffffff")))))
      (if (dom-by-id dom "surface1")
          (push bg-node (cddr (car (dom-by-id dom "surface1"))))
        (push bg-node (cddr (car dom))))))
  dom)

(defun my-sketch-clean (dom)
  "Remove USE and IMAGE tags."
  (dolist (use (dom-by-tag dom 'use))
    (dom-remove-node dom use))
  (dolist (use (dom-by-tag dom 'image))
    (dom-remove-node dom use))
  dom)

(defun my-sketch-rotate (dom)
  (let* ((old-width (dom-attr dom 'width))
         (old-height (dom-attr dom 'height))
         (view-box (mapcar 'string-to-number (split-string (dom-attr dom 'viewBox))))
         (rotate (format "rotate(90) translate(0 %s)" (- (elt view-box 3)))))
    (dom-set-attribute dom 'width old-height)
    (dom-set-attribute dom 'height old-width)
    (dom-set-attribute dom 'viewBox (format "0 0 %d %d" (elt view-box 3) (elt view-box 2)))
    (dolist (g (dom-by-tag dom 'g))
      (dom-set-attribute g 'transform rotate)))
  dom)

(defun my-sketch-mix-blend-mode-darken (dom &optional selector)
  (dolist (p (if (functionp selector) (dom-search dom selector) (or selector (dom-by-tag dom 'path))))
    (when (and (dom-attr p 'style)
               (not (string-match "mix-blend-mode" (dom-attr p 'style))))
      (dom-set-attribute
       p 'style
       (replace-regexp-in-string ";;\\|^;" ""
                                 (concat
                                  (or (dom-attr p 'style) "")
                                  ";mix-blend-mode:darken")))))
  dom)

(defun my-sketch-color-to-hex (dom &optional selector)
  (dolist (p (if (functionp selector) (dom-search dom selector)
               (or selector (dom-search dom
                                        (lambda (p) (or (dom-attr p 'style)
                                                        (dom-attr p 'fill)))))))
    (dolist (attr '(style fill))
      (when (dom-attr p attr)
        (dom-set-attribute
         p attr
         (replace-regexp-in-string
          "rgb(\\([0-9\\.]+\\)%, *\\([0-9\\.%]+\\)%, *\\([0-9\\.]+\\)%)"
          (lambda (s)
            (color-rgb-to-hex
             (* 0.01 (string-to-number (match-string 1 s)))
             (* 0.01 (string-to-number (match-string 2 s)))
             (* 0.01 (string-to-number (match-string 3 s)))
             2))
          (dom-attr p attr))))))
  dom)

;; default for now, but will support more colour schemes someday
(defvar my-sketch-color-map
  '((blue
     ("#9d9d9d" . "#2b64a9")
     ("#9c9c9c" . "#2b64a9")
     ("#c9c9c9" . "#b3e3f1")
     ("#c8c8c8" . "#b3e3f1")
     ("#cacaca" . "#b3e3f1")
     ("#a6d2ff" . "#ffffff"))
    (t
     ("#9d9d9d" . "#888888")
     ("#9c9c9c" . "#888888")
     ("#cacaca" . "#f6f396")
     ("#c8c8c8" . "#f6f396")
     ("#a6d2ff" . "#ffffff")
     ("#c9c9c9" . "#f6f396"))))

(cl-defun my-sketch-svg-prepare (file &key color-map color-scheme new-file)
  "Clean up SVG for publishing."
  (when (string= (file-name-extension file) "pdf")
    (setq file (my-sketch-convert-pdf file)))
  (let ((dom (xml-parse-file file)))
    (setq dom (my-sketch-clean dom))
    (setq dom (my-sketch-color-to-hex dom))
    (setq dom (my-sketch-add-bg dom))
    (setq dom (my-sketch-change-fill-to-style dom))
    (setq dom (my-sketch-recolor dom
                                 (or color-map
                                     color-scheme
                                     t)))
    (with-temp-file (or new-file file) (svg-print (car dom)))
    (or new-file file)))

Editing and linking text

I've started keeping the text of the sketch in the same directory so that I can someday have full-text search for images. I have a keyboard shortcut for jumping to the text file. I like to open it in Org Mode.

my-org-sketch-open-text-file
(defun my-org-sketch-open-text-file (sketch)
  (interactive (list (my-complete-sketch-filename)))
  (find-file (concat (file-name-sans-extension sketch) ".txt"))
  (with-current-buffer (find-file-noselect sketch)
    (display-buffer-in-side-window
     (current-buffer)
     '((window-width . 0.5)
       (side . right)))))

The raw text from Google Cloud Vision is reasonably accurate but jumbled. I can move lines around with M-S-up and M-S-down in Org (org-shiftmetaup and org-shiftmetadown), which drag lines around. Once I add newlines, I can reorganize paragraphs with M-up and M-down (org-metaup and org-metadown). I can move list elements with M-S-right and M-S-left. (Idea: Avy probably has some awesome line-management functions I could get the hang of using.)

Once I've reorganized and cleaned up the text, I add links. Between my consult-omni shortcut and the new bookmarks I'm trying out (I should make a post about that), it's pretty easy.

Prompting for rectangles

Then it's a quick trip to Inkscape to draw rectangles over the things I want to link. It's easy to see where to draw the links because Org Mode highlights the links in the text. The style of the rectangles doesn't matter. After I save the SVG, I hop back into Emacs to turn them into links. This is the fun new part I just added.

Linkify rects

I like this because I got to reuse some code I'd written before to identify and reorder paths for easier animation of SVG topic maps. Using the links I defined in the previous step, all I needed to do was go through the rects (excluding the background rectangle) and offer completing-read on the titles and URLs. Then I createed the link elements and restyled the rectangles.

my-svg-linkify-rects
(defun my-svg-display (buffer-name svg &optional highlight-id full-window)
  "HIGHLIGHT-ID is a string ID or a node."
  (with-current-buffer (get-buffer-create buffer-name)
    (when highlight-id
      ;; make a copy
      (setq svg (with-temp-buffer (svg-print svg) (car (xml-parse-region (point-min) (point-max)))))
      (if-let* ((path (if (stringp highlight-id) (dom-by-id svg highlight-id) highlight-id))
                (view-box (split-string (dom-attr svg 'viewBox)))
                (box (my-svg-bounding-box path))
                (parent (car path)))
          (progn
            ;; find parents for possible rotation
            (while (and parent (not (dom-attr parent 'transform)))
              (setq parent (dom-parent svg parent)))
            (dom-set-attribute path 'style
                               (concat (dom-attr path 'style) "; stroke: 1px red; fill: #ff0000 !important"))
            ;; add a crosshair
            (dom-append-child
             (or parent svg)
             (dom-node 'path
                       `((d .
                            ,(format "M %f,0 V %s M %f,0 V %s M 0,%f H %s M 0,%f H %s"
                                     (elt box 0)
                                     (elt view-box 3)
                                     (elt box 2)
                                     (elt view-box 3)
                                     (elt box 1)
                                     (elt view-box 2)
                                     (elt box 3)
                                     (elt view-box 2)))
                         (stroke-dasharray . "5,5")
                         (style . "fill:none;stroke:gray;stroke-width:3px")))))
        (error "Could not find %s" highlight-id)))
    (let* ((inhibit-read-only t)
           (image (svg-image svg))
           (edges (window-inside-pixel-edges (get-buffer-window))))
      (erase-buffer)
      (if full-window
          (progn
            (delete-other-windows)
            (switch-to-buffer (current-buffer)))
        (display-buffer (current-buffer)))
      (insert-image (append image
                            (list :max-width
                                  (floor (* 0.8 (- (nth 2 edges) (nth 0 edges))))
                                  :max-height
                                  (floor (* 0.8 (- (nth 3 edges) (nth 1 edges)))) )))
      ;; (my-svg-resize-with-window (selected-window))
      ;; (add-hook 'window-state-change-functions #'my-svg-resize-with-window t)
      (current-buffer))))

(cl-defun my-svg-identify-paths (filename &key selector node-func dom)
  "Prompt for IDs for each path in FILENAME."
  (interactive (list (read-file-name "SVG: " nil nil
                                     (lambda (f)
                                       (or (string-match "\\.svg$" f)
                                           (file-directory-p f))))))
  (let* ((dom (or dom (car (xml-parse-file filename))))
         (paths (if (functionp selector)
                    (dom-search dom selector)
                  (or selector
                      (dom-by-tag dom 'path))))
         (vertico-count 3)
         (ids (seq-keep (lambda (path)
                          (and (dom-attr path 'id)
                               (unless (string-match "\\(path\\|rect\\)[0-9]+"
                                                     (or (dom-attr path 'id) "path0"))
                                 (dom-attr path 'id))))
                        paths))
         (edges (window-inside-pixel-edges (get-buffer-window)))
         id)
    (my-svg-display "*image*" dom nil t)
    (dolist (path paths)
      ;; display the image with an outline
      (unwind-protect
          (progn
            (my-svg-display "*image*" dom (dom-attr path 'id) t)
            (if (functionp node-func)
                (funcall node-func path dom)
              (setq id (completing-read
                        (format "ID (%s): " (dom-attr path 'id))
                        ids))
              ;; already exists, merge with existing element
              (if-let* ((old (dom-by-id dom id)))
                  (progn
                    (dom-set-attribute
                     old
                     'd
                     (concat (dom-attr (dom-by-id dom id) 'd)
                             " "
                             ;; change relative to absolute
                             (replace-regexp-in-string "^m" "M"
                                                       (dom-attr path 'd))))
                    (dom-remove-node dom path)
                    (setq id nil))
                (dom-set-attribute path 'id id)
                (add-to-list 'ids id)))))
      ;; save the image just in case we get interrupted halfway through
      (with-temp-file filename
        (svg-print dom)))))

(defun my-svg-identify-rects (filename)
  (interactive (list (read-file-name "SVG: " nil nil
                                     (lambda (f)
                                       (or (string-match "\\.svg$" f)
                                           (file-directory-p f))))))
  (my-svg-identify-paths
   filename
   :selector
   (lambda (elem)
     (and (eq (dom-tag elem) 'rect)
          (not (and (dom-attr elem 'class)
                    (string-match "\\<background\\>" (dom-attr elem 'class))))))))

(defun my-org-links-from-file (filename)
  "Return a list of (description . link) of the Org links in FILENAME."
  (when (file-exists-p filename)
    (let (results)
      (with-temp-buffer
        (insert-file-contents filename)
        (goto-char (point-min))
        (while (re-search-forward org-link-any-re nil t)
          (push (cons (match-string-no-properties 3)
                      (or (match-string-no-properties 2)
                          (match-string-no-properties 0)))
                results)))
      (reverse results))))

(defun my-svg-linkify-rects (filename)
  (interactive (list (read-file-name "SVG: " nil nil
                                     (lambda (f)
                                       (or (string-match "\\.svg$" f)
                                           (file-directory-p f))))))
  (let ((dom (car (xml-parse-file filename)))
        (links-from-text (my-org-links-from-file (concat (file-name-sans-extension filename) ".txt"))))
    (my-svg-identify-paths
     filename
     :dom
     dom
     :selector
     (append
      ;; not yet linked
      (dom-search dom
                  (lambda (elem)
                    (and (eq (dom-tag elem) 'rect)
                         (not (and (dom-attr elem 'class)
                                   (string-match "\\<background\\|link-rect\\>" (dom-attr elem 'class)))))))
      ;; linked
      (dom-search dom
                  (lambda (elem)
                    (and (eq (dom-tag elem) 'rect)
                         (string-match "\\<link-rect\\>" (or (dom-attr elem 'class) ""))))))

     :node-func
     (lambda (elem dom)
       (let* ((current-link-node (my-dom-closest dom elem 'a))
              (current-title-node (or (dom-by-tag elem 'title)
                                      (dom-by-tag current-link-node 'title)))
              (title (string-trim
                      (completing-read
                      "Title: "
                      (mapcar 'car links-from-text)
                      nil nil
                      (dom-text current-title-node))))
              (link (string-trim
                     (read-string
                       "URL: "
                       (or (dom-attr current-link-node 'href)
                           (assoc-default title links-from-text 'string=)))
                     )))
         (cond
          ((and current-link-node (not (string= link "")))
           (dom-set-attribute elem
                              'style
                              "stroke: blue; stroke-dasharray: 4; fill: #006fff; fill-opacity: 0.25")
           (dom-set-attribute current-link-node 'href link))
          ((and current-link-node (string= link ""))
           (dom-add-child-before
            (dom-parent dom current-link-node)
            elem)
           (dom-remove-node current-link-node))
          ((and (null current-link-node) (not (string= link "")))
           (setq current-link-node (dom-node
                                    'a
                                    `((href . ,link)
                                      (class . "link"))))
           (dom-add-child-before (dom-parent dom elem) current-link-node elem)
           (dom-remove-node dom elem)
           (dom-append-child current-link-node elem)
           (dom-remove-attribute elem 'fill)
           (dom-set-attribute elem
                              'style
                              "stroke: blue; stroke-dasharray: 4; fill: #006fff; fill-opacity: 0.25")
           (dom-set-attribute
            elem
            'class
            (if (dom-attr elem 'class)
                (concat (dom-attr elem 'class) " link-rect")
              "link-rect"))))
         (cond
          ((and (string= title "") current-title-node)
           (dom-remove-node current-title-node))
          ((and (not (string= title "")) (not current-title-node))
           (dom-append-child current-link-node (dom-node 'title nil title)))
          ((and (not (string= title "")) current-title-node)
           (setf (car (dom-children current-title-node))
                 title))))))))

(defun my-svg-update-links-from-text (filename)
  (interactive (list (read-file-name
                      "SVG: " nil
                      (if (file-exists-p (concat (file-name-sans-extension (buffer-file-name)) ".svg"))
                          (concat (file-name-sans-extension (buffer-file-name)) ".svg")
                        (cdr (my-embark-image)))
                      (lambda (f)
                        (or (string-match "\\.svg$" f)
                            (file-directory-p f))))))
  (let ((dom (car (xml-parse-file filename)))
        (links-from-text (my-org-links-from-file (concat (file-name-sans-extension filename) ".txt"))))
    (dolist (link (dom-by-tag dom 'a))
      (when (and
             (assoc-default (dom-text (dom-by-tag link 'title))
                            links-from-text)
             (not (string=
                   (dom-attr link 'href)
                   (assoc-default (dom-text (dom-by-tag link 'title))
                                  links-from-text))))
        (dom-set-attribute
         link
         'href
         (assoc-default (dom-text (dom-by-tag link 'title))
                        links-from-text))))
    (with-temp-file filename
      (svg-print dom))))



Writing about the sketch

I tweaked my function for drafting a blog post about a sketch. I added panning and zooming capabilities using Javascript, included the sketch text, and added any sections that I referred to using anchors. (TODO: Come to think of it, I should rewrite those to be absolute links using the permalink so that they'll still make sense even if people bookmark them from the main page of my blog.)

my-write-about-sketch
(defun my-insert-sketch-and-text (sketch)
  (interactive (list (my-complete-sketch-filename)))
  (insert
   (if (string= (file-name-extension sketch) "svg")
       (format
        "#+begin_panzoom\n%s\n#+end_panzoom\n\n"
        (org-link-make-string (concat "file:" sketch)))
     (concat (org-link-make-string (concat "sketchFull:" (file-name-base sketch))) "\n\n")))
  (let ((links (my-org-links-from-file (concat (file-name-sans-extension sketch) ".txt")))
        (subheading-level (1+ (org-current-level))))
    (insert (if links
                "#+begin_my_details Text and links from sketch\n"
              "#+begin_my_details Text from sketch\n"))
    (my-sketch-insert-text sketch)
    (unless (bolp) (insert "\n"))
    (insert "#+end_my_details")
    (dolist (section (seq-filter (lambda (entry) (string-match "^#" (cdr entry)))
                                 links))
      (org-end-of-subtree)
      (insert "\n\n")
      (org-insert-heading nil nil subheading-level)
      (insert (car section))
      (org-entry-put (point) "CUSTOM_ID" (substring (cdr section) 1)))))

(defun my-write-about-sketch (sketch)
  (interactive (list (my-complete-sketch-filename)))
  ;(shell-command "make-sketch-thumbnails")
  (find-file "~/sync/orgzly/posts.org")
  (goto-char (point-min))
  (unless (org-at-heading-p) (outline-next-heading))
  (org-insert-heading nil nil t)
  (insert (file-name-base sketch) "\n\n")
  (my-insert-sketch-and-text sketch)
  (insert "\nFeel free to use this under the [[https://creativecommons.org/licenses/by/4.0/][Creative Commons Attribution License]].\n")
  (delete-other-windows)
  (save-excursion
    (with-selected-window (split-window-horizontally)
      (find-file sketch))))

And then I can export the image as an inline SVGs in Org Mode HTML and Markdown exports, yay!

Other functions not included above are probably somewhere in my Emacs config.

Using an SVG as a sticky table of contents

… and now I can make the image a sticky table of contents as you scroll down, by wrapping it in something like this:

#+begin_sticky-toc-after-scrolling
#+begin_panzoom
file:/home/sacha/sync/sketches/2025-01-17-01 Hyperlinking SVGs -- drawing supernote inkscape svg.svg
#+end_panzoom
#+end_sticky-toc-after-scrolling

Mwahahaha! (Now I just need to make it highlight different sections as we scroll…)

Here's the snippet from my misc.js:

Sticky table of contents after scrolling
function stickyTocAfterScrolling() {
  const elements = document.querySelectorAll('.sticky-toc-after-scrolling');
  let lastScroll = window.scrollY;
  const cloneMap = new WeakMap();

  elements.forEach(element => {
    const clone = element.cloneNode(true);
    clone.setAttribute('class', 'sticky-toc');
    cloneMap.set(element, clone);
    element.parentNode.insertBefore(clone, element.nextSibling);
    const zoom = panZoom = svgPanZoom(clone.querySelector('svg'));
    zoom.resetZoom();
  });

  const observer = new IntersectionObserver(
    (entries) => {
      const currentScroll = window.scrollY;
      const scrollingDown = currentScroll > lastScroll;
      lastScroll = currentScroll;

      entries.forEach(entry => {
        const element = entry.target;
        const clone = cloneMap.get(element);

        if (!entry.isIntersecting && scrollingDown) {
          clone.setAttribute('class', 'sticky-toc');
          clone.style.display = 'block';
        } else if (entry.isIntersecting && !scrollingDown) {
          element.style.visibility = 'visible';
          clone.style.display = 'none';
        }
      });
    },
    {
      root: null,
      threshold: 0,
      rootMargin: '-10px 0px 0px 0px'
    }
  );

  elements.forEach(element => {
    observer.observe(element);
  });

  window.addEventListener('resize', () => {
    elements.forEach(element => {
      const clone = cloneMap.get(element);
      if (clone.style.display != 'none') {
        // reset didn't seem to work
        svgPanZoom(clone.querySelector('svg')).destroy();
        addPanZoomToElement(clone.querySelector('svg'));
      }
    });
  }, { passive: true });
}

stickyTocAfterScrolling();
View org source for this post

Organizing my sketches

| drawing, supernote
Text and links from sketch

Organizing my sketches

What I have now:

  • SuperNote A5X:
    • Monthly notebooks
    • Long-term note with links
    • Daily moments
    • Crafts
    • PDFs (esp. w/ large margins)
  • Highlighted heading
  • ☆ for needs more
  • No antialiasing: … Preferences
  • Finished sketches:
    • ID: YYYY-MM-DD-NN
    • Export as PNG, medium resolution
    • Rename to ID Title – tags.png
    • Save to sketches or private-sketches
      • sketches.sachachua.com
  • Code to recognize/recolor/rename, open/insert/export a sketch, its text, or a list of sketches

What I want:

  • Use sketches to untangle thoughts
  • Share my notes
  • Make visual cues for A+ & me (menus, moments, trackers)
  • Annotate text/transcripts to make sense of them, organize/summarize info
  • Doodle illustrations for my blog
  • Draft videos, posts

Things I want to improve:

View org source for this post

How sketchnotes fit into my personal knowledge management

| pkm, drawing

Text from sketch
  • worth doing even if you don't feel like you can draw well
    • really, I just draw stick figures
  • good for your own thoughts and other people's
  • own thoughts:
    • non-linear
    • visual metaphors & organizers can be helpful
    • can be a launchpad for more details
  • other people's thoughts: distill key points from a talk, book, etc. using my understanding
  • visual cues make it easy to see important things first
  • doodling is fun
  • IDs help with linking (ex: 2024-10-17-02)
  • How I use sketchnotes:
    • Flesh out an idea, especially during non-computer time
    • Sketch talks or books to make them easier to review
    • Optical character recognition (Google Cloud Vision API, etc.) to blog text: I edit this to provide a good text alternative in blog posts
  • My evil plan
    • Sketchnotes are very shareable
      • People are always looking for visuals to add.
    • When people share them, they usually tell me about it
    • I get to find out what else people are thinking about & learning from.
    • More learning! More fun!
    • It's also a nice way to give back to people who've shared what they learned
      • Then they might share more!

I've been enjoying using sketchnotes as an idea launchpad for audio braindumps or blog posts, as a quick way to review the key points of a book or talk, and as a way to participate in the larger conversation. It's easy for me to link to sketches and extract the text within them.

Someday I'll probably improve my ability to search for the text within sketches. Right now, I just go by filenames and the text in my blog posts. I can probably make something that goes through the text annotations in the JSON files from Google Cloud Vision, or maybe I can turn them into a text file that can be updated when I write a blog post. Hmm, that actually sounds pretty straightforward, I should go do that…

Examples of my evil plan working:

Mwahaha!

View org source for this post