Categories: sharing

RSS - Atom - Subscribe via email

Through blogging, we discover our thoughts and other people

| connecting, blogging, writing
Text and links from sketch

Through blogging, we discover our thoughts and other people.

Henrik Karlsson's "Advice for a friend who wants to start a blog" nudged me to explore two threads of thought:

Writing helps you refine your thoughts:

  • This reminds me of Sonke Ahrens's How to Take Smart Notes and David Bessis's Mathematica.
    • Everything drives toward writing; writing is how to clarify your thoughts
    • Writing helps you improve your intuition, which feeds your writing.
  • I want to understand:
    • What's possible?
    • What's easier? what's harder (for now?)
    • How can we make things. easier? How can we make more things doable?
  • and also:
    • What am I thinking?
    • what do I want to try?
  • I want to get better at this through practice.

Writing helps you find your tribe:

  • Definitely - and the more idiosyncratic my posts are, the more amazing it is when someone resonates with it, even years later.
  • (I was amused to see him trace his tweet's flow through Stian Håklev, who reached out for a conversation in 2010 about peer-to-peer education because he read my blog.)
  • I deliberately boost my tribe's information flow:
  • I want to get better at this by
    • following my curiosity
    • improving search and serendipity
    • connecting people & ideas with community infrastructure and resources

Both sides: Because it's fun and leads to more awesomeness.

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-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)
    ("pdf"
     (setq file
           (my-image-store
            (my-sketch-svg-prepare file))))
    ("png"
     (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)
  (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

Looking at my blog post stats by year

| blogging
blog-stats.svg
Figure 1: Blog statistics

I was curious about the shape of my blog over the years, excluding Emacs News and my link-heavy weekly/monthly reviews. It started off with lots of little posts like the way other weblogs were also quick links and notes. As weblogs morphed into blogs with more text, I also settled down into fewer, longer posts with lots of code (analyzed by looking for <pre> blocks). I wrote much less after A+ was born. Interestingly, I've been shifting towards longer posts with more images.

  • Blog posts exclude permalinks that match emacs-news|review|week-ending, which casts a bit of a wide net but should give me the general shape of things.
  • Total words per year and average words per post both exclude code snippets.

Here's how I got those numbers:

(append
 '(("Year" "Posts" "Total words" "Words per post" "Posts with pre" "Posts with images")
   hline)
 (cl-loop for i from 2001 to 2024
          collect
          (let* ((default-directory (expand-file-name (number-to-string i) "~/proj/static-blog/blog"))
                 (exclude (shell-quote-argument "emacs-news|review|week-ending"))
                 (files (format "find . -name '*.html' | grep -v -e '%s' | " exclude))
                 (posts (string-to-number
                         (string-trim
                          (shell-command-to-string (concat files "wc -l")))))
                 (words (string-to-number
                         (replace-regexp-in-string
                          "TOTAL: " ""
                          (shell-command-to-string
                           (concat files "xargs ~/bin/count-words | grep TOTAL")))))
                 (posts-with-images
                  (string-to-number
                   (string-trim
                    (shell-command-to-string (concat files "xargs grep -l '<img' | wc -l")))))
                 (posts-with-pre
                  (string-to-number
                   (string-trim
                    (shell-command-to-string (concat files "xargs grep -l '<pre' | wc -l"))))))
            (list i
                  posts
                  words
                  (/ words posts)
                  posts-with-images
                  posts-with-pre))))
Year Posts Total words Words per post Posts with pre Posts with images
2001 3 438 146 0 0
2002 31 4336 139 0 0
2003 863 64953 75 0 59
2004 967 125789 130 2 98
2005 679 135334 199 4 40
2006 869 171042 196 19 42
2007 489 107011 218 33 32
2008 380 121158 318 85 57
2009 400 175692 439 81 20
2010 335 160289 478 93 19
2011 324 163274 503 93 28
2012 286 124300 434 111 12
2013 273 173021 633 172 11
2014 272 186788 686 138 30
2015 173 133682 772 82 36
2016 25 11560 462 13 6
2017 37 24063 650 6 2
2018 66 46827 709 7 8
2019 18 13054 725 3 6
2020 13 6791 522 4 5
2021 31 17389 560 8 16
2022 21 11264 536 4 9
2023 68 47188 693 26 52
2024 74 58439 789 27 40

And here's how I plotted the charts:

import pandas as pd
import matplotlib.pyplot as plt
df = pd.DataFrame(data[1:], columns=data[0])
# Create a figure with subplots
fig, (ax1, ax4, ax2, ax3) = plt.subplots(4, 1, figsize=(10, 12))
fig.suptitle('Blog Statistics by Year', fontsize=16)

# Plot Posts
ax1.bar(df['Year'], df['Posts'], color='lightblue', label='Other posts')
ax1.bar(df['Year'], df['Posts with pre'] , color='darkblue', label='With preformatted blocks')
ax1.set_title('Number of posts per year')
ax1.set_ylabel('Posts')
ax1.legend()

# Plot Posts
ax4.bar(df['Year'], df['Posts'], color='lightblue', label='Other posts')
ax4.bar(df['Year'], df['Posts with images'] , color='darkgreen', label='With images')
ax4.set_title('Number of posts per year')
ax4.set_ylabel('Posts')
ax4.legend()

# Plot Total Words
ax2.bar(df['Year'], df['Total words'], color='lightblue')
ax2.set_title('Total words per year')
ax2.set_ylabel('Total words')

# Plot Words per Post
ax3.bar(df['Year'], df['Words per post'], color='lightblue')
ax3.set_title('Average words per post')
ax3.set_ylabel('Words per post')
ax3.set_xlabel('Year')

# Adjust layout and display
plt.savefig(f)
View org source for this post

Thinking about webpage margins

| blogging, design

I want to write more, and I want to enjoy going through my archive. Some posts are long, especially those that come from transcripts. If I sat with the ideas for longer, I might be able to make them more concise or break them up into more atomic notes; but I also want to get things out faster in order to learn from potential conversations. So I'm thinking about text structure and margins, since I want to re-read my blog more and I sometimes glaze over when there's lots of text.

More headings are a good start. Org Mode makes it easy enough to add them: M-RET calls org-insert-heading.

I'm experimenting with sticky tables of contents on large screens: one for "on this page" on the left, and one for long posts on the right.

2024-11-06_16-42-18.png
Figure 1: Screenshot of my blog with tables of content on both sides

On the individual post page, it'll just be the table of contents for the post, like this one.

2024-11-06_16-43-45.png
Figure 2: Screenshot of individual post

It feels a little busy. If I write some Javascript, I might be able to use IntersectionObservers to highlight where we are. Maybe I can even squeeze the article's TOC into the "on this page" TOC if there is one, which means it can stay on one side.

I want to do other things with the margins. Doodles for fun? My cargo bike post started with the doodles pulled all the way into the margins, and then I moved them back into the text so that I don't have to worry about bumping into the table of contents.

2024-11-06_16-45-02.png
Figure 3: Screenshot of doodles in the margins

Sometimes I use a sketchnote to help me think through or summarize a topic. It might be fun to use the sketchnote as a table of contents or overview, maybe even highlighting different sections of it as I scroll. handwritten.blog uses mix-blend-mode for hyperlinks. If the SVG isn't too big, maybe I can use the same kind of technique I used in animating SVG topic maps with Inkscape. Alternatively, I could put extracted regions from the sketchnote in the margins for context and visual variety.

Sidenotes? I like how A Scripter's Notes has both an active, expanding TOC on the right as well as side notes on the left.

2024-11-06_16-46-00.png
Figure 4: Screenshot from A Scripter's Notes

Karthinks uses a sticky TOC and sidenotes:

2024-11-06_16-51-50.png
Figure 5: Screenshot from karthinks.com showing table of contents and sidenotes

A Blog With Relevant Information uses just sidenotes, so the rest of the page feels pretty clear:

2024-11-06_17-07-37.png
Figure 6: Screenshot of sidenotes

Maybe keywords, like the Cornell method of note-taking? Kind of like sidenotes, but more structural, for skimming. I'm having a hard time finding a blog example, though. If I figure out side nodes, I could probably just use a different style to indicate those Cornell-style cues.

But there's so much more I want to do with the space. I like the stacking of https://notes.andymatuschak.org , and I like that you can link to a particular stacked state.

2024-11-06_16-47-44.png
Figure 7: Stacked items from Andy's working notes

Then every so often, I come across a blog that is just clean and refreshing and then I want to get rid of everything in the margins.

There are plenty of CSS and JS resources out there. Figuring out what I want is the tough part.

View org source for this post

How do I want to get better at learning out loud? Part 1 of 4: Starting

| sharing, blogging, writing

Nudged by Thierry Stoehr's toot about my 23rd blogiversary, I've been thinking about how much I've learned thanks to blogging, and how I can get even better at learning out loud. I'm curious about what this could become over the next twenty years, when I'm in my sixties.

The first part of the text from the sketch is duplicated and expanded in the list below. There are a lot of different aspects I want to get better at, so I'm not going to try to work on all of them in one go, but it's fun mapping out so much room for growth.

Besides, maybe one of these aspects will resonate with you as either something you're learning or something you've figured out something about, and then you'll get in touch, and then we'll both learn more. Wouldn't that be cool?

I'm experimenting with getting stuff out in smaller chunks, so this is part 1 of 4: Starting.

Noticing

I think of this as seeing the opportunity for learning, which I sometimes miss out on because I take things for granted or I don't connect the dots. I can get better at this by slowing down and by borrowing other people's questions. I've been giving myself more time to write and draw these days. It feels a little weird ignoring the other tasks on my TODO lists that are more clearly defined or that are related to other people's requests, but I like the way this feels.

Imagining

It's easy for me to come up with all sorts of ideas for things I want to tweak about Emacs. I can get better at this by reading more about what other people are doing and what other capabilities are there.

I can also get better at exploring ideas for non-Emacs topics, like ways to respond to parenting situations and things I can do support the causes I care about. I can expand my toolbox by reading books and blog posts, and depending on the topic, I can also listen to podcasts and videos.

Bumping into things

It's useful to bump into things I might not think of looking for. One way to do that would be to save various manuals on my e-ink notebook and phone so that I can read them during quiet moments.

I've added some randomness to shuffle old blog posts and tasks, although browsing through this tends to be low-priority. (I never get to the bottom of my reading/thinking list!)

"On this day" might be interesting too. This is more for fun and serendipity. I used to have it on my blog, and it should be pretty easy to reimplement using 11ty.

Learning from others

I'd like to spend more time thinking about and building on other people's ideas, maybe starting with Emacs and then branching out to other topics.

I also want to get back to reading people's blogs through an RSS reader so that I can get a slightly wider view of people's interests and learn more about non-Emacs things. I've added Feeder to my phone.

Taking notes

I capture a lot of snippets in my Org Mode inbox. I'd like to get better at adding some more context and quick thoughts when I create a note so that it's easier to pick up the idea later on. I do most of this capturing on my phone, so I'm getting the hang of slowing down and adding some more notes.

I also want to get better at actually reviewing and refining those notes. My inbox tends to grow and grow, especially when I get interrupted by an interesting idea. I have some writing/editing time while I keep A+ company during virtual school, so it'll be fun revisiting the notes I stashed.

Collecting

This is about putting a bunch of related notes together, which I usually do by refiling them. It's probably also related to clustering, which I'll get to in the next post about thinking.

I do most of this collecting on my computer, so I can write a few Emacs functions to make it easier. For example, I have some code to do the opposite of refiling so that while I'm looking at a topic, I can pull in a subtree from somewhere else.

Some buckets collect thoughts for blog posts, some for projects, some just for areas I'm interested in. I feel like I tend to lose track of the buckets that I'm collecting thoughts into–the list of slightly-less-active thoughts, as the active thoughts are easily findable. Maybe this is okay.

Expanding

I want to get better at going from a microblog post/toot or a quick index-card-type sketch to a longer blog post or sketchnote.

Actually, since my current workflow focuses mostly on blog posts, I think this part is more about contracting: picking out a small thought that I can share right now instead of waiting until I write the rest of it. This idea might also include picking a medium-sized chunk and making it the first post in a series, and the current post is an experiment in doing so. I'm a little hesitant to do so because my brain tends to wander off towards the end of a series, but it might be worth an experiment.

I'll also need some code to make it easier to add links between things in a series, which could be manual (handy for other non-series links too) or possibly something handled on the 11ty side (like How to build a blog series with 11ty/Eleventy).

I can also experiment with spreading posts out by scheduling them as Org tasks. I've theoretically added support for holding back future-dated posts in my 11ty config, but I think managing that on the Org side might be easier for now.

Boosting

This is linked to learning from others. Boosting what other people have said or thought can be a quick and easy way of learning out loud and enlarging the conversation. I can do more of that through Mastodon toots, rolling them up into my blog. I often add snippets to my config based on the things I come across in Emacs News.

I also like the commentary on blogs like Irreal and would like to grow into things like that.

Next up: thinking, making, and sharing

Over the next little while, I'm looking forward to fleshing out the next sections. Let's see if breaking things up into posts works…

  • Thinking: trying; reflecting; developing thoughts; shifting; reviewing; clustering; building on; searching (local, web, exact, approximate); questioning; reframing
  • Making: organizing; writing; editing; drawing (sketchnotes, doodles); diagramming; plotting; charts; coding; showing
  • Sharing: designing; linking; mapping thoughts; joining the conversation
View org source for this post

How to Take Smart Notes - Sonke Ahrens (2017)

| visual-book-notes, writing, pkm, productivity, learning

I want to get better at making sense of things and sharing what I'm learning. Nudged by Chris Maiorana's post on Second Brain, Second Nature, I borrowed How to Take Smart Notes by Sönke Ahrens (2017). Here are my notes.

Text from sketch

How to Take Smart Notes - Sönke Ahrens. 2017 - sketched by Sacha Chua 2024-10-26-01

  • Niklas Luhmann: everything - writing; slipbox, Zettelkasten
  • Instead of: brainstorm (blank paper), then research (wrong topic? wrong understanding?), then write
  • Try a loop of:
    • Read with a pen in hand: short notes, your own understanding
    • Refine and connect your notes: elaborate.
    • Notice clusters
    • Develop into topics, write about them
    • reading ⇒ thinking ⇒writing
  • Types of notes
    • Fleeting: try to review within a day
    • Permanent: complete sentences, makes sense at a glance
    • Literature: short; use own words
    • Project: can be archived after
  • Work on multiple projects so you can switch between them and they can feed each other.
  • Things to think about.
    • Why is this interesting?
    • Why is this relevant?
    • How does this relate to other things?
    • What's not mentioned?
  • Numbering, physical references: let ideas mingle
    • 22, 22a, 22a1, 22b, 23, …
  • Retrieval cues
  • Saving cut pieces = easier editing
  • Verbund: by-products = resources
  • Writing → break it up!
    • reading, understanding, reflecting, getting ideas, connecting, distinguishing, rewording, structuring, organizing, editing, rewriting
  • Positive feedback loop: reading with pen, writing permanent notes, writing arguments…

The book goes into detail about Niklas Luhmann's Zettelkasten or slipbox system. Lots of people have written about Zettelkasten and various implementations. There's even a whole micro-industry around Notion templates. So I won't spend a lot of time right now describing what it is or what the key aspects are. I can focus instead on what that means to me and what I want to do with it.

Writing

By doing everything with the clear purpose of writing about it, you will do what you do deliberately.

I like chapter 5's focus on keeping writing in mind. I want to push most things towards writing and drawing (posts, code, whatever; public as much as possible) because it's a good way for me to remember and to learn from others. It's a reminder to not try speeding through my to-do list; it's good to slow down and write about stuff.

Following the work

I only do what is easy. I only write when I immediately know how to do it. If I falter for a moment, I put the matter aside and do something else.

I always work on different manuscripts at the same time. With this method, to work on different things simultaneously, I never encounter any mental blockages.

During my discretionary time, I usually follow the butterflies of my interest: working on what I feel like working on, moving on to something else when I get stuck. Sometimes I will work on something I have to do because it's got to be done, but those moments are rarer. Amidst all those productivity books that exhort you to focus on a limited number of things, it was nice to know that Luhmann also jumped from interest to interest, that the process of accumulating these notes builds things up into clusters with critical mass, and that these good habits build themselves up through positive feedback loops.

Different types of notes

I do all right capturing fleeting notes on my phone, but I want to get better at turning my fleeting notes into literature notes and permanent notes. I'd like to review them more frequently and spend some more time fleshing them out, with the goal of eventually turning more of those things into blog posts and code that I can share as I learn out loud.

I also don't really have a good way of putting topics "near" other topics yet. Categories are a little coarse, but maybe topic maps are a good starting point. It would be nice to have a quick way to put something before/after something else, though.

Different types of tasks

Writing a paper involves much more than just typing on the keyboard. It also means reading, understanding, reflecting, getting ideas, making connections, distinguishing terms, finding the right words, structuring, organizing, editing, correcting and rewriting.

I wonder if making these distinctions between the subtasks of writing will make it easier for me to break writing down into tiny tasks that can be completed and gotten out of my brain.

Thinking about connections, thinking about what's missing

I want to get better at connecting ideas to other things I've thought about by linking to blog posts or notes. That might also help me build up thoughts out of smaller chunks, which would be helpful when it comes to working with fragmented thoughts.

Thinking about what's not in the picture is hard, and that kind of critical thinking is something I want to practise more. I can pay attention to the follow-up questions I have so that I can get a sense of where to look for more insights or what to experiment with. Questioning the way something is framed is also good and something I don't do often enough.

For example, I wanted to dig into this quote:

Luhmann’s only real help was a housekeeper who cooked for him and his children during the week, not that extraordinary considering he had to raise three children on his own after his wife died early.

I ended up doing a tiny bit of research on my phone and putting it into Niklas Luhmann's Zettelkasten and life with kids (the kids were in their teens at the time, so they were probably a lot more independent than A+ is at the moment).

Related

View org source for this post