Categories: sharing

RSS - Atom - Subscribe via email

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

Organizing my visual book notes by topic

| blogging, 11ty

I want to start building up more thoughts as chunks and relating them more logically instead of just chronologically. I've been using categories to organize my posts into buckets, but within a category, it's still chronological. I also have a large outline that includes posts from 2017 to 2024. I'd like to break it up into smaller topic pages so that they're easier to link to, although it's a little more challenging to search.

Now that I have a nice gallery view for my visual book notes, I wanted to organize the book notes by topic. I made an async Eleventy paired shortcode called gallerylist that lets me turn a list of links into into thumbnails and links.

I also modified org-html-toc to not include the Table of Contents header and to tweak the HTML attributes assigned to it.

New table of contents code
(defun my-org-html-toc (depth info &optional scope)
  "Build a table of contents.
DEPTH is an integer specifying the depth of the table.  INFO is
a plist used as a communication channel.  Optional argument SCOPE
is an element defining the scope of the table.  Return the table
of contents as a string, or nil if it is empty."
  (let ((toc-entries
   (mapcar (lambda (headline)
       (cons (org-html--format-toc-headline headline info)
       (org-export-get-relative-level headline info)))
     (org-export-collect-headlines info depth scope))))
    (when toc-entries
      (let* ((toc-id-counter (plist-get info :org-html--toc-counter))
             (toc (concat (format "<div class=\"text-table-of-contents toc-id%s\" role=\"doc-toc\">"
                                  (if toc-id-counter (format "-%d" toc-id-counter) ""))
        (org-html--toc-text toc-entries)
        "</div>\n")))
        (plist-put info :org-html--toc-counter (1+ (or toc-id-counter 0)))
  (if scope toc
    (let ((outer-tag (if (org-html--html5-fancy-p info)
             "nav"
           "div")))
      (concat (format "<%s class=\"table-of-contents toc-id%s\" role=\"doc-toc\">\n"
                            outer-tag
                            (if toc-id-counter (format "-%d" toc-id-counter) ""))
              ;; (let ((top-level (plist-get info :html-toplevel-hlevel)))
              ;; (format "<h%d>%s</h%d>\n"
              ;;   top-level
              ;;   (org-html--translate "Table of Contents" info)
              ;;   top-level))
        toc
        (format "</%s>\n" outer-tag))))))))

(with-eval-after-load 'org
  (defalias 'org-html-toc #'my-org-html-toc))

This is what my visual book notes topic page looks like now:

2024-10-25_09-23-26.png
Figure 1: Screenshot of visual book notes

I can improve on this by using the topic maps to determine next/previous links for the posts. Someday!

View org source for this post

Added a gallery and slideshow view for my visual book notes

| 11ty, blogging

I customized my visual book notes - all view to show the thumbnails of the images, and I added You can get to it by going to the visual-book-notes category from a post and then choosing "All". I like the new "View slideshow" and "Shuffle slideshow" buttons I added.

2024-10-23_14-09-26.png
Figure 1: Screenshot of my visual book notes gallery

I also fixed some of the broken images in older posts, so there should be 43 posts with images now.

Someday I want to add a way to go from the sketch in the slideshow to the post, but it might require upgrading the version of Photoswipe I have. I'm currently on 4.x, which hasn't been updated in years.

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

Adding an XSL stylesheet for my RSS and Atom feeds

| 11ty, geek, blogging

Inspired by the styling on Susam's blog feed, I followed this tutorial on using XML stylesheets and added XSL stylesheets for my RSS and Atom feeds. I have RSS and Atom feeds for all my posts as well as for each category or tag (ex: emacs).

2024-01-13_19-06-02.png
Figure 1: RSS feed after styling

To make that happen, I added a line like this to my RSS template:

<?xml-stylesheet href="/assets/rss.xsl" type="text/xsl"?>

and for my Atom template:

<?xml-stylesheet href="/assets/atom.xsl" type="text/xsl"?>

and those refer to:

rss.xsl
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="3.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:dc="http://purl.org/dc/elements/1.1/"        xmlns:atom="http://www.w3.org/2005/Atom">
  <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/">
  <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
    <head>
      <title>
        RSS Feed | <xsl:value-of select="/rss/channel/title"/>
      </title>
      <link rel="stylesheet" href="/assets/style.css"/>
    </head>
    <body>
      <h1 style="margin-bottom:0">Recent posts: <xsl:value-of select="/rss/channel/title"/></h1>
      <p>
        This is an RSS feed. You can subscribe to <a href=" {/rss/channel/link}"><xsl:value-of select="/rss/channel/link"/></a> in a feed reader such as <a href="https://github.com/skeeto/elfeed">Elfeed</a> for Emacs, <a href="https://www.inoreader.com/">Inoreader</a>, or <a href="https://newsblur.com/">NewsBlur</a>, or you can use tools like <a href="https://github.com/rss2email/rss2email">rss2email</a>. The feed includes the full blog posts.
You can also view the posts on the website at
<a href="{/rss/channel/atom:link[contains(@rel,'alternate')]/@href}"><xsl:value-of select="/rss/channel/atom:link[contains(@rel,'alternate')]/@href" /></a> .
      </p>
      <xsl:for-each select="/rss/channel/item">
        <div style="margin-bottom:20px">
        <div>
          <xsl:value-of select="pubDate" />
        </div>
        <div>
          <a>
            <xsl:attribute name="href">
              <xsl:value-of select="link/@href"/>
            </xsl:attribute>
            <xsl:value-of select="title"/>
          </a></div></div>
      </xsl:for-each>
    </body>
    </html>
  </xsl:template>
</xsl:stylesheet>
atom.xsl
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="3.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:atom="http://www.w3.org/2005/Atom">
  <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/">
  <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
    <head>
      <title>
        Atom Feed | <xsl:value-of select="/atom:feed/atom:title"/>
      </title>
      <link rel="stylesheet" href="/assets/style.css"/>
    </head>
    <body>
      <h1 style="margin-bottom:0">Recent posts: <xsl:value-of select="/atom:feed/atom:title"/></h1>
      <p>
        This is an Atom feed. You can subscribe to <a href=" {/atom:feed/atom:link/@href}"><xsl:value-of select="/atom:feed/atom:link/@href"/></a> in a feed reader such as <a href="https://github.com/skeeto/elfeed">Elfeed</a> for Emacs, <a href="https://www.inoreader.com/">Inoreader</a>, or <a href="https://newsblur.com/">NewsBlur</a>, or you can use tools like <a href="https://github.com/rss2email/rss2email">rss2email</a>. The feed includes the full blog posts.
You can also view the posts on the website at
<a href="{/atom:feed/atom:link[contains(@rel,'alternate')]/@href}"><xsl:value-of select="/atom:feed/atom:link[contains(@rel,'alternate')]/@href" /></a> .
      </p>
      <xsl:for-each select="/atom:feed/atom:entry">
        <div style="margin-bottom:20px">
          <div>
          <xsl:value-of select="substring(atom:updated, 0, 11)" /></div>
          <div><a>
            <xsl:attribute name="href">
              <xsl:value-of select="atom:link/@href"/>
            </xsl:attribute>
            <xsl:value-of select="atom:title"/>
          </a></div></div>
      </xsl:for-each>
    </body>
    </html>
  </xsl:template>
</xsl:stylesheet>
View org source for this post

Animating SVG topic maps with Inkscape, Emacs, FFmpeg, and Reveal.js

| emacs, drawing, org, ffmpeg, video

tldr (2167 words): I can make animating presentation maps easier by writing my own functions for the Emacs text editor. In this post, I show how I can animate an SVG element by element. I can also add IDs to the path and use CSS to build up an SVG with temporary highlighting in a Reveal.js presentation.

Text from the sketch
  • PNG: Inkscape: trace
  • Supernote (e-ink)
  • iPad: Adobe Fresco

Convert PDF to SVG with Inkscape (Cairo option) or pdftocairo)

  • PNG / Supernote PDF: Combined shapes. Process
    1. Break apart, fracture overlaps
    2. Recombine
    3. Set IDs
    4. Sort paths -> Animation style 1
  • Adobe Fresco: individual elements in order; landscape feels natural

Animation styles

  • Animation style 1: Display elements one after another
  • Animation style 2: Display elements one after another, and also show/hide highlights
    • Table: slide ID, IDs to add, temporary highlights -> Reveal.js: CSS with transitions

Ideas for next steps:

  • Explore graphviz & other diagramming tools
  • Frame-by-frame SVGs
    • on include
    • write to files
  • FFmpeg crossfade
  • Recording Reveal.js presentations
  • Use OCR results?

I often have a hard time organizing my thoughts into a linear sequence. Sketches are nice because they let me jump around and still show the connections between ideas. For presentations, I'd like to walk people through these sketches by highlighting different areas. For example, I might highlight the current topic or show the previous topics that are connected to the current one. Of course, this is something Emacs can help with. Before we dive into it, here are quick previews of the kinds of animation I'm talking about:

animation-loop.gif
Figure 1: Animation style 1: based on drawing order

Animation style 2: building up a map with temporary highlights

Getting the sketches: PDFs are not all the same

Let's start with getting the sketches. I usually export my sketches as PNGs from my Supernote A5X. But if I know that I'm going to animate a sketch, I can export it as a PDF. I've recently been experimenting with Adobe Fresco on the iPad, which can also export to PDF. The PDF I get from Fresco is easier to animate, but I prefer to draw on the Supernote because it's an e-ink device (and because the kiddo usually uses the iPad).

If I start with a PNG, I could use Inkscape to trace the PNG and turn it into an SVG. I think Inkscape uses autotrace behind the scenes. I don't usually put my highlights on a separate layer, so autotrace will make odd shapes.

It's a lot easier if you start off with vector graphics in the first place. I can export a vector PDF from the SuperNote A5X and either import it into Inkscape using the Cairo option or use the command-line pdftocairo tool.

I've been looking into using Adobe Fresco, which is a free app available for the iPad. Fresco's PDF export can be converted to an SVG using Inkscape or PDF to Cairo. What I like about the output of this app is that it gives me individual elements as their own paths and they're listed in order of drawing. This makes it really easy to animate by just going through the paths in order.

Animation style 1: displaying paths in order

Here's a sample SVG file that pdfcairo creates from an Adobe Fresco PDF export:

pdftocairo -svg ~/Downloads/subed-audio.pdf ~/Downloads/subed-audio.svg
Sample SVG
subed-audio.svg

Adobe Fresco also includes built-in time-lapse, but since I often like to move things around or tidy things up, it's easier to just work with the final image, export it as a PDF, and convert it to an SVG.

I can make a very simple animation by setting the opacity of all the paths to 0, then looping through the elements to set the opacity back to 1 and write that version of the SVG to a separate file. From how-can-i-generate-png-frames-that-step-through-the-highlights:

my-animate-svg-paths: Add one path at a time. Save the resulting SVGs to OUTPUT-DIR.
(defun my-animate-svg-paths (filename output-dir)
  "Add one path at a time. Save the resulting SVGs to OUTPUT-DIR."
  (unless (file-directory-p output-dir)
    (make-directory output-dir t))
  (let* ((dom (xml-parse-file filename))
         (paths (seq-filter (lambda (e) (dom-attr e 'style))
                            (dom-by-tag dom 'path)))
         (total (length paths))
         (frame-num (length paths))
         result)
    (dolist (elem paths)
      (dom-set-attribute elem 'style
                         (concat
                          (dom-attr elem 'style)
                          ";mix-blend-mode:darken")))
    (with-temp-file (expand-file-name (format "frame-%03d.svg" (1+ frame-num)) output-dir)
      (xml-print dom))
    (dolist (elem paths)
      (dom-set-attribute elem 'style
                         (concat
                          (dom-attr elem 'style)
                          ";fill-opacity:0")))
    (dolist (elem paths)
      (with-temp-file (expand-file-name
                       (format "frame-%03d.svg"
                               (- total frame-num))
                       output-dir)
        (message "%03d" frame-num)
        (dom-set-attribute elem 'style
                           (concat (dom-attr elem 'style)
                                   ";fill-opacity:1"))
        (push (list (format "frame-%03d.svg"
                            (1+ (- total frame-num)))
                    (dom-attr elem 'id))
              result)
        (setq frame-num (1- frame-num))
        (xml-print dom)))
    (reverse result)))

Here's how I call it:

(my-animate-svg-paths "~/Downloads/subed-audio.svg" "/tmp/subed-audio/frames" t)

Then I can use FFmpeg to combine all of those frames into a video:

ffmpeg -i frame-%03d.svg -vf palettegen -y palette.png
ffmpeg -framerate 30 -i frame-%03d.svg -i palette.png -lavfi "paletteuse" -loop 0 -y animation-loop.gif
animation-loop.gif
Figure 2: Animating SVG paths based on drawing order

Neither Supernote nor Adobe Fresco give me the original stroke information. These are filled shapes, so I can't animate something drawing it. But having different elements appear in sequence is fine for my purposes. If you happen to know how to get stroke information out of Supernote .note files or of an iPad app that exports nice single-line SVGs that have stroke direction, I would love to hear about it.

Identifying paths from Supernote sketches

When I export a PDF from Supernote and convert it to an SVG, each color is a combined shape with all the elements. If I want to animate parts of the image, I have to break it up and recombine selected elements (Inkscape's Ctrl-k shortcut) so that the holes in shapes are properly handled. This is a bit of a tedious process and it usually ends up with elements in a pretty random order. Since I have to reorder elements by hand, I don't really want to animate the sketch letter-by-letter. Instead, I combine them into larger chunks like topics or paragraphs.

The following code takes the PDF, converts it to an SVG, recolours highlights, and then breaks up paths into elements:

my-sketch-convert-pdf-and-break-up-paths: Convert PDF to SVG and break up paths.
(defun my-sketch-convert-pdf-and-break-up-paths (pdf-file &optional rotate)
  "Convert PDF to SVG and break up paths."
  (interactive (list (read-file-name
                      (format "PDF (%s): "
                              (my-latest-file "~/Dropbox/Supernote/EXPORT/" "pdf"))
                      "~/Dropbox/Supernote/EXPORT/"
                      (my-latest-file "~/Dropbox/Supernote/EXPORT/" "pdf")
                      t
                      nil
                      (lambda (s) (string-match "pdf" s)))))
  (unless (file-exists-p (concat (file-name-sans-extension pdf-file) ".svg"))
    (call-process "pdftocairo" nil nil nil "-svg" (expand-file-name pdf-file)
                  (expand-file-name (concat (file-name-sans-extension pdf-file) ".svg"))))
  (let ((dom (xml-parse-file (expand-file-name (concat (file-name-sans-extension pdf-file) ".svg"))))
        highlights)
    (setq highlights (dom-node 'g '((id . "highlights"))))
    (dom-append-child dom highlights)
    (dolist (path (dom-by-tag dom 'path))
      ;;  recolor and move
      (unless (string-match (regexp-quote "rgb(0%,0%,0%)") (or (dom-attr path 'style) ""))
        (dom-remove-node dom path)
        (dom-append-child highlights path)
        (dom-set-attribute
         path 'style
         (replace-regexp-in-string
          (regexp-quote "rgb(78.822327%,78.822327%,78.822327%)")
          "#f6f396"
          (or (dom-attr path 'style) ""))))
      (let ((parent (dom-parent dom path)))
        ;; break apart
        (when (dom-attr path 'd)
          (dolist (part (split-string (dom-attr path 'd) "M " t " +"))
            (dom-append-child
             parent
             (dom-node 'path `((style . ,(dom-attr path 'style))
                               (d . ,(concat "M " part))))))
          (dom-remove-node dom path))))
    ;; remove the use
    (dolist (use (dom-by-tag dom 'use))
      (dom-remove-node dom use))
    (dolist (use (dom-by-tag dom 'image))
      (dom-remove-node dom use))
    ;; move the first g down
    (let ((g (car (dom-by-id dom "surface1"))))
      (setf (cddar dom)
            (seq-remove (lambda (o)
                          (and (listp o) (string= (dom-attr o 'id) "surface1")))
                        (dom-children dom)))
      (dom-append-child dom g)
      (when rotate
        (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)))
          (dom-set-attribute highlights 'transform rotate)
          (dom-set-attribute g 'transform rotate))))
    (with-temp-file (expand-file-name (concat (file-name-sans-extension pdf-file) "-split.svg"))
      (svg-print (car dom)))))

2023-10-split.svg
Figure 3: Image after splitting up into elements

You can see how the spaces inside letters like "o" end up being black. Selecting and combining those paths fixes that.

Combining paths in Inkscape

If there were shapes that were touching, then I need to draw lines and fracture the shapes in order to break them apart.

Fracturing shapes and checking the highlights

The end result should be an SVG with the different chunks that I might want to animate, but I need to identify the paths first. You can assign object IDs in Inkscape, but this is a bit of an annoying process since I haven't figured out a keyboard-friendly way to set object IDs. I usually find it easier to just set up an Autokey shortcut (or AutoHotkey in Windows) to click on the ID text box so that I can type something in.

Autokey script for clicking
import time
x, y = mouse.get_location()
# Use the coordinates of the ID text field on your screen; xev can help
mouse.click_absolute(3152, 639, 1)
time.sleep(1)
keyboard.send_keys("<ctrl>+a")
mouse.move_cursor(x, y)

Then I can select each element, press the shortcut key, and type an ID into the textbox. I might use "t-…" to indicate the text for a map section, "h-…" to indicate a highlight, and arrows by specifying their start and end.

Setting IDs in Inkscape

To simplify things, I wrote a function in Emacs that will go through the different groups that I've made, show each path in a different color and with a reasonable guess at a bounding box, and prompt me for an ID. This way, I can quickly assign IDs to all of the paths. The completion is mostly there to make sure I don't accidentally reuse an ID, although it can try to combine paths if I specify the ID. It saves the paths after each change so that I can start and stop as needed. Identifying paths in Emacs is usually much nicer than identifying them in Inkscape.

Identifying paths inside Emacs

my-svg-identify-paths: Prompt for IDs for each path in FILENAME.
(defun my-svg-identify-paths (filename)
  "Prompt for IDs for each path in FILENAME."
  (interactive (list (read-file-name "SVG: " nil nil
                                     (lambda (f) (string-match "\\.svg$" f)))))
  (let* ((dom (car (xml-parse-file filename)))
         (paths (dom-by-tag dom 'path))
         (vertico-count 3)
         (ids (seq-keep (lambda (path)
                          (unless (string-match "path[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)
      (when (string-match "path[0-9]+" (or (dom-attr path 'id) "path0"))
        ;; display the image with an outline
        (unwind-protect
            (progn
              (my-svg-display "*image*" dom (dom-attr path 'id) t)
              (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))))))

Sorting and animating the paths by IDs

Then I can animate SVGs by specifying the IDs. I can reorder the paths in the SVG itself so that I can animate it group by group, like the way that the Adobe Fresco SVGs were animated element by element.

Reordering paths
(my-svg-reorder-paths "~/proj/2023-12-audio-workflow/map.svg"
                      '("t-start" "h-audio" "h-capture" "t-but" "t-mic" "h-mic"
                        "t-reviewing" "h-reviewing"
                        "t-words" "h-words" "t-workflow" "h-workflow"
                        "t-lapel" "h-lapel"
                        "mic-recorder" "t-recorder" "h-recorder"
                        "t-syncthing" "h-sync"
                        "t-keywords" "h-keywords" "t-keyword-types"
                        "t-lines" "h-lines"
                        "t-align" "h-align"
                        "arrow"
                        "t-org" "h-org" "t-todo" "h-todo" "h-linked"
                        "t-jump" "h-jump"
                        "t-waveform" "h-waveform"
                        "t-someday"
                        "h-sections"
                        "t-speech-recognition" "h-speech-recognition"
                        "t-ai" "h-ai"
                        "t-summary"
                        "extra")
                      "~/proj/2023-12-audio-workflow/map-output.svg")
(my-animate-svg-paths "~/proj/2023-12-audio-workflow/map-output.svg"
                      "~/proj/2023-12-audio-workflow/frames/")
Table of filenames after reordering paths and animating the image
frame-001.svg t-start
frame-002.svg h-audio
frame-003.svg h-capture
frame-004.svg t-but
frame-005.svg t-mic
frame-006.svg h-mic
frame-007.svg t-reviewing
frame-008.svg h-reviewing
frame-009.svg t-words
frame-010.svg h-words
frame-011.svg t-workflow
frame-012.svg h-workflow
frame-013.svg t-lapel
frame-014.svg h-lapel
frame-015.svg mic-recorder
frame-016.svg t-recorder
frame-017.svg h-recorder
frame-018.svg t-syncthing
frame-019.svg h-sync
frame-020.svg t-keywords
frame-021.svg h-keywords
frame-022.svg t-keyword-types
frame-023.svg t-lines
frame-024.svg h-lines
frame-025.svg t-align
frame-026.svg h-align
frame-027.svg arrow
frame-028.svg t-org
frame-029.svg h-org
frame-030.svg t-todo
frame-031.svg h-todo
frame-032.svg h-linked
frame-033.svg t-jump
frame-034.svg h-jump
frame-035.svg t-waveform
frame-036.svg h-waveform
frame-037.svg t-someday
frame-038.svg h-sections
frame-039.svg t-speech-recognition
frame-040.svg h-speech-recognition
frame-041.svg t-ai
frame-042.svg h-ai
frame-043.svg t-summary
frame-044.svg extra

The table of filenames makes it easy to use specific frames as part of a presentation or video.

Here is the result as a video:

(let ((compile-media-output-video-width 1280)
      (compile-media-output-video-height 720))
  (my-ffmpeg-animate-images
   (directory-files "~/proj/2023-12-audio-workflow/frames/" t "\\.svg$")
   (expand-file-name "~/proj/2023-12-audio-workflow/frames/animation.webm")
   4))

Animation of SVG by paths

The way it works is that the my-svg-reorder-paths function removes and readds elements following the list of IDs specified, so everything's ready to go for step-by-step animation. Here's the code:

my-svg-reorder-paths: Sort paths in FILENAME.
(defun my-svg-reorder-paths (filename &optional ids output-filename)
  "Sort paths in FILENAME."
  (interactive (list (read-file-name "SVG: " nil nil (lambda (f) (string-match "\\.svg$" f)))
                     nil (read-file-name "Output: ")))
  (let* ((dom (car (xml-parse-file filename)))
         (paths (dom-by-tag dom 'path))
         (parent (dom-parent dom (car paths)))
         (ids-left
          (nreverse (seq-keep (lambda (path)
                                (unless (string-match "path[0-9]+" (or (dom-attr path 'id) "path0"))
                                  (dom-attr path 'id)))
                              paths)))
         list)
    (when (called-interactively-p)
      (while ids-left
        (my-svg-display "*image*" dom (car ids-left))
        (let ((current (completing-read
                        (format "ID (%s): "
                                (car ids-left))
                        ids-left nil nil nil nil (car ids-left)))
              node)
          (add-to-list 'ids current)
          (setq ids-left (seq-remove (lambda (o) (string= o current)) ids-left)))))
    (if ids ;; reorganize under the first path's parent
        (progn
          (dolist (id ids)
            (if-let ((node (car (dom-by-id dom id))))
                (progn
                  (dom-remove-node dom node)
                  (dom-append-child parent node))
              (message "Could not find %s" id)))
          (with-temp-file (or output-filename filename)
            (svg-print dom))))
    (nreverse (seq-keep (lambda (path)
                          (unless (string-match "path[0-9]+" (or (dom-attr path 'id) "path0"))
                            (dom-attr path 'id)))
                        (dom-by-tag dom 'path)))))

Animation style 2: Building up a map with temporary highlights

I can also use CSS rules to transition between opacity values for more complex animations. For my EmacsConf 2023 presentation, I wanted to make a self-paced, narrated presentation so that people could follow hyperlinks, read the source code, and explore. I wanted to include a map so that I could try to make sense of everything. For this map, I wanted to highlight the previous sections that were connected to the topic for the current section.

I used a custom Org link to include the full contents of the SVG instead of just including it with an img tag.

#+ATTR_HTML: :class r-stretch
my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html

my-include-export: Export PATH to FORMAT using the specified wrap parameter.
(defun my-include-export (path _ format _)
  "Export PATH to FORMAT using the specified wrap parameter."
  (let (params body start end)
    (when (string-match "^\\(.*+?\\)\\(?:::\\|\\?\\)\\(.*+\\)" path)
      (setq params (save-match-data (org-protocol-convert-query-to-plist (match-string 2 path)))
            path (match-string 1 path)))
    (with-temp-buffer
      (insert-file-contents-literally path)
      (when (string-match "\\.org$" path)
        (org-mode))
      (if (plist-get params :name)
          (when (org-babel-find-named-block (plist-get params :name))
            (goto-char (org-babel-find-named-block (plist-get params :name)))
            (let ((block (org-element-context)))
              (setq start (org-element-begin block)
                    end (org-element-end block))))
        (goto-char (point-min))
        (when (plist-get params :from-regexp)
          (re-search-forward (url-unhex-string (plist-get params :from-regexp)))
          (goto-char (match-beginning 0)))
        (setq start (point))
        (setq end (point-max))
        (when (plist-get params :to-regexp)
          (re-search-forward (url-unhex-string (plist-get params :to-regexp)))
          (setq end (match-beginning 0))))
      (setq body (buffer-substring start end)))
    (with-temp-buffer
      (when (plist-get params :wrap)
        (let* ((wrap (plist-get params :wrap))
               block args)
          (when (string-match "\\<\\(\\S-+\\)\\( +.*\\)?" wrap)
            (setq block (match-string 1 wrap))
            (setq args (match-string 2 wrap))
            (setq body (format "#+BEGIN_%s%s\n%s\n#+END_%s\n"
                               block (or args "")
                               body
                               block)))))
      (when (plist-get params :summary)
        (setq body (format "#+begin_my_details %s\n%s\n#+end_my_details\n"
                           (plist-get params :summary)
                           body)))
      (insert body)
      (org-export-as format nil nil t))))

I wanted to be able to specify the entire sequence using a table in the Org Mode source for my presentation. Each row had the slide ID, a list of highlights in the form prev1,prev2;current, and a comma-separated list of elements to add to the full-opacity view.

Slide Highlight Additional elements
props-map h-email;h-properties t-email,email-properties,t-properties
file-prefixes h-properties;h-filename t-filename,properties-filename
renaming h-filename;h-renaming t-renaming,filename-renaming
shell-scripts h-renaming;h-shell-scripts renaming-shell-scripts,t-shell-scripts
availability h-properties;h-timezone t-timezone,properties-timezone
schedule h-timezone;h-schedule t-schedule,timezone-schedule
emailing-speakers h-timezone,h-mail-merge;h-emailing-speakers schedule-emailing-speakers,t-emailing-speakers
template h-properties;h-template t-template,properties-template
wiki h-template;h-wiki t-wiki,template-wiki,schedule-wiki
pad h-template;h-pad template-pad,t-pad
mail-merge h-template;h-mail-merge t-mail-merge,template-mail-merge,schedule-mail-merge,emailing-speakers-mail-merge
bbb h-bbb t-bbb
checkin h-mail-merge;h-checkin t-checkin,bbb-checkin
redirect h-bbb;h-redirect t-redirect,bbb-redirect
shortcuts h-email;h-shortcuts t-shortcuts,email-shortcuts
logbook h-shortcuts;h-logbook shortcuts-logbook,t-logbook
captions h-captions t-captions,captions-wiki
tramp h-captions;h-tramp t-tramp,captions-tramp
crontab h-tramp;h-crontab tramp-crontab,bbb-crontab,t-crontab
transitions h-crontab;h-transitions shell-scripts-transitions,t-transitions,shortcuts-transitions,transitions-crontab
irc h-transitions;h-irc t-irc,transitions-irc

Reveal.js adds a "current" class to the slide, so I can use that as a trigger for the transition. I have a bit of Emacs Lisp code that generates some very messy CSS, in which I specify the ID of the slide, followed by all of the elements that need their opacity set to 1, and also specifying the highlights that will be shown in an animated way.

my-reveal-svg-progression-css: Make the CSS.
(defun my-reveal-svg-progression-css (map-progression &optional highlight-duration)
  "Make the CSS.
map-progression should be a list of lists with the following format:
((\"slide-id\" \"prev1,prev2;cur1\" \"id-to-add1,id-to-add2\") ...)."
  (setq highlight-duration (or highlight-duration 2))
  (let (full)
    (format
     "<style>%s</style>"
     (mapconcat
      (lambda (slide)
        (setq full (append (split-string (elt slide 2) ",") full))
        (format "#slide-%s.present path { opacity: 0.2 }
%s { opacity: 1 !important }
%s"
                (car slide)
                (mapconcat (lambda (id) (format "#slide-%s.present #%s" (car slide) id))
                           full
                           ", ")
                (my-reveal-svg-highlight-different-colors slide)))
      map-progression
      "\n"))))
I call it from my Org file like this:

#+NAME: progression-css
#+begin_src emacs-lisp :exports code :var map-progression=progression :var highlight-duration=2 :results silent
(my-reveal-svg-progression-css map-progression highlight-duration)
#+end_src

Here's an excerpt showing the kind of code it makes:

<style>#slide-props-map.present path { opacity: 0.2 }
#slide-props-map.present #t-email, #slide-props-map.present #email-properties, #slide-props-map.present #t-properties { opacity: 1 !important }
#slide-props-map.present #h-email { fill: #c6c6c6; opacity: 1 !important; transition: fill 0.5s; transition-delay: 0.0s }#slide-props-map.present #h-properties { fill: #f6f396; opacity: 1 !important; transition: fill 0.5s; transition-delay: 0.5s }
#slide-file-prefixes.present path { opacity: 0.2 }
#slide-file-prefixes.present #t-filename, #slide-file-prefixes.present #properties-filename, #slide-file-prefixes.present #t-email, #slide-file-prefixes.present #email-properties, #slide-file-prefixes.present #t-properties { opacity: 1 !important }
#slide-file-prefixes.present #h-properties { fill: #c6c6c6; opacity: 1 !important; transition: fill 0.5s; transition-delay: 0.0s }#slide-file-prefixes.present #h-filename { fill: #f6f396; opacity: 1 !important; transition: fill 0.5s; transition-delay: 0.5s }
...</style>

Since it's automatically generated, I don't have to worry about it once I've gotten it to work. It's all hidden in a results drawer. So this CSS highlights specific parts of the SVG with a transition, and the highlight changes over the course of a second or two. It highlights the previous names and then the current one. The topics I'd already discussed would be in black, and the topics that I had yet to discuss would be in very light gray. This could give people a sense of the progress through the presentation.

Code for making the CSS
(defun my-reveal-svg-animation (slide)
  (string-join
   (seq-map-indexed
    (lambda (step-ids i)
      (format "%s { fill: #f6f396; transition: fill %ds; transition-delay: %ds }"
              (mapconcat
               (lambda (id) (format "#slide-%s.present #%s" (car slide) id))
               (split-string step-ids ",")
               ", ")
              highlight-duration
              (* i highlight-duration)))
    (split-string (elt slide 1) ";"))
   "\n"))

(defun my-reveal-svg-highlight-different-colors (slide)
  (let* ((colors '("#f6f396" "#c6c6c6")) ; reverse
         (steps (split-string (elt slide 1) ";"))
         (step-length 0.5))
    (string-join
     (seq-map-indexed
      (lambda (step-ids i)
        (format "%s { fill: %s; opacity: 1 !important; transition: fill %.1fs; transition-delay: %.1fs }"
                (mapconcat
                 (lambda (id) (format "#slide-%s.present #%s" (car slide) id))
                 (split-string step-ids ",")
                 ", ")
                (elt colors (- (length steps) i 1))
                step-length
                (* i 0.5)))
      steps))))

(defun my-reveal-svg-progression-css (map-progression &optional highlight-duration)
  "Make the CSS.
map-progression should be a list of lists with the following format:
((\"slide-id\" \"prev1,prev2;cur1\" \"id-to-add1,id-to-add2\") ...)."
  (setq highlight-duration (or highlight-duration 2))
  (let (full)
    (format
     "<style>%s</style>"
     (mapconcat
      (lambda (slide)
        (setq full (append (split-string (elt slide 2) ",") full))
        (format "#slide-%s.present path { opacity: 0.2 }
%s { opacity: 1 !important }
%s"
                (car slide)
                (mapconcat (lambda (id) (format "#slide-%s.present #%s" (car slide) id))
                           full
                           ", ")
                (my-reveal-svg-highlight-different-colors slide)))
      map-progression
      "\n"))))

As a result, as I go through my presentation, the image appears to build up incrementally, which is the effect that I was going for. I can test this by exporting only my map slides:

(save-excursion
  (goto-char (org-babel-find-named-block "progression-css"))
  (org-babel-execute-src-block))
(let ((org-tags-exclude-from-inheritance "map")
      (org-export-select-tags '("map")))
   (oer-reveal-export-to-html))

Ideas for next steps

  • Graphviz, mermaid-js, and other diagramming tools can make SVGs. I should be able to adapt my code to animate those diagrams by adding other elements in addition to path. Then I'll be able to make diagrams even more easily.
  • Since SVGs can contain CSS, I could make an SVG equivalent of the CSS rules I used for the presentation, maybe calling a function with a Lisp expression that specifies the operations (ex: ("frame-001.svg" "h-foo" opacity 1)). Then I could write frames to SVGs.
  • FFmpeg has a crossfade filter. With a little bit of figuring out, I should be able to make the same kind of animation in a webm form that I can include in my regular videos instead of using Reveal.js and CSS transitions.
  • I've also been thinking about automating the recording of my Reveal.js presentations. For my EmacsConf talk, I opened my presentation, started the recording with the system audio and the screen, and then let it autoplay the presentation. I checked on it periodically to avoid the screensaver/energy saving things from kicking in and so that I could stop the recording when it's finished. If I want to make this take less work, one option is to use ffmpeg's "-t" argument to specify the expected duration of the presentation so that I don't have to manually stop it. I'm also thinking about using Puppeteer to open the presentation, check when it's fully loaded, and start the process to record it - maybe even polling to see whether it's finished. I haven't gotten around to it yet. Anyhow, those are some ideas to explore next time.
  • As for animation, I'm still curious about the possibility of finding a way to access the raw stroke information if it's even available from my Supernote A5X (difficult because it's a proprietary data format) or finding an app for the iPad that exports single line SVGs that use stroke information instead of fill. That would only be if I wanted to do those even fancier animations that look like the whole thing is being drawn for you. I was trying to figure out if I could green screen the Adobe Fresco timelapse videos so that even if I have a pre-sketch to figure out spacing and remind me what to draw, I can just export the finished elements. But there's too much anti-aliasing and I haven't figured out how to do it cleanly yet. Maybe some other day.
  • I use Google Cloud Vision's text detection engine to convert my handwriting to text. It can give me bounding polygons for words or paragraphs. I might be able to figure out which curves are entirely within a word's bounding polygon and combine those automatically.
  • It would be pretty cool if I could combine the words recognized by Google Cloud Vision with the word-level timestamps from speech recognition so that I could get word-synced sketchnote animations with maybe a little manual intervention.

Anyway, those are some workflows for animating sketches with Inkscape and Emacs. Yay Emacs!

View org source for this post

2023-12-30-01 Daily moments

| drawing, life

Text from sketch

Inspired by Arne Bab (who mentioned being inspired by my sketches) I've been drawing daily moments since 2023-03-20. Nothing fancy, just a quick reminder of our day.

I draw while the kiddo watches a bedtime video. Sometimes she suggests a moment to draw, or flips through the pages and laughs at the memories.

I also have my text journal (occasionally with photos) and my time tracker. It doesn't take a lot of time to update them, and I like what they let me do.

I like this. It makes the path visible. I'm looking forward to seeing what this is like after years

I used to draw and write monthly reviews. I'd like to get back to those. They help with the annual reviews, too.

  • phone: review sketches, jot keywords on phone
  • computer: draw sketch, braindump, blog

Right now I put 12 days on one A5.

  • Week? nah, not really needed
  • More details? longer to review, though. Redirect drawing to monthly notes

Still working on shaping the day/week more proactively. A+ likes to take the lead, so maybe it's more like strewing.

Moments: https://sketches.sachachua.com/tags/moment

If you're viewing this on my blog, you might be able to click on the links below to open them in a viewer and then swipe or use arrow keys to navigate.

Here are older ones.

ArneBab's post: "My best thing today in sketchnotes"

View org source for this post