Little Design Things

 from Red Blob Games
21 Feb 2019

Here I have an unorganized collection of "little things" about my interactive diagram design. I am using this as a reference for myself, so that when I write a new page I can look through things I've done previously. I have previously listed a few of these in my interactive tutorial about how to make interactive tutorials[1].

 1  Visuals#

 1.1 Inline legend

Text on the page might describe elements in a diagram. For example, I might write "the blue lines represent graph edges". For describing elements, we aren't limited to using text. We can draw those elements directly, like we do in hand-drawn notes. For example, I could write, "the blue lines represent graph edges".

<SVG> is an inline element, like <img>. It can be placed in the text of a document. Here are examples where I connect the text and diagram by including elements of the diagram in the text.

Example of inline legendsAnother example of inline legends
Inline legends: describe elements of the diagram in the text

Use <svg width="3em" height="1em" viewBox="…"> to scale it to fit the text. The width and height are the size in the text and the view box is the coordinates of the diagram.

Related: sparklines[2] show data inline in the text. What I'm doing here is showing the legend inline in the text.

 1.2 Color in diagrams match text

I sometimes use colors in the text to match the color in the diagrams. Here's an example from the hexagon guide.

Colors represent concepts in the diagramThe colors represent the same concepts in the code
Semantic colorization: use the same colors for code as used in the diagram

Unfortunately because the text is x, y, z, the colors are not easy to see. I think the colors on the pathfinding pages worked better.

 1.3 Color in diagrams match code

In this diagram on my breadth first search page, I use a red blob for a pathfinding node, blue for the set of nodes to be explored, green for the neighboring nodes, yellow for the current node, brown for visited nodes. In the code, instead of using standard colorizations for strings, keywords, etc., I use the same colors that I used in the diagram. I use the same colors in the text.

Colors represent concepts in the diagramThe colors represent the same concepts in the text
Semantic colorization: use the same colors for text as used in the diagram

I think I used too many colors. It would have been better to break this up into several steps, each focusing on one concept. In each step, I would use color only for the one concept I am trying to explain. Alternatively, this visualization[3] shows all the colors, and then hides all but the one concept the reader is focusing on.

Screenshot of diagram and sample code
Color in code matches color in diagram

I use both color and icons sometimes:

Diagram has iconsCode can have same icons
Code can use icons to match the diagrams

Here I colored the controls that correspond to the lines on the chart:

Color in sliders matches lines on chart
Color in sliders matches the lines in the diagram


 1.4 Negative space

Negative space[7] is the space around the thing we draw. We can use this to our advantage, creating shapes in the mind without actually drawing them. A common example of this for page design is avoid drawing borders. We can often get the same effect by using background colors and leaving some space:

Sidebar sidebar sidebar

I guess I'm a little weird. I like to talk to trees and animals. That's okay though; I have more fun than most people. Let's do that again. There we are. Let's get wild today.

You can do anything here - the only pre-requisite is that it makes you happy. The light is your friend. Preserve it. Maybe there was an old trapper that lived out here and maybe one day he went to check his beaver traps, and maybe he fell into the river and drowned. We don't have to be concerned about it. We just have to let it fall where it will. — Bob Ross text generator[8]

Sidebar with border vs negative space

The first style focuses on the border; the second style focuses on the content.

I use the same idea for diagrams. It's especially noticeable with grids:

Grid with border vs negative space

The first style emphasizes the edges (which is what I want on a page about edges); the second style emphasizes the tiles (which is what I want most of the time). It's not always practical though in some color schemes.

I occasionally use this with node-and-edge graphs:

Circles with border vs negative space

The first one emphasizes the edges and the the second one emphasizes the nodes. Which works better depends on which I want the reader to focus on.

 1.5 Yellow light and blue shadows

I had read somewhere that artists use yellowish tints for sunlight and bluish tints for shadows. I used this idea for my 2D visibility page:

Blue light with yellow shadowYellow light with blue shadow
Yellow light and blue shadows look better

I don't know a lot about this though, and haven't used this idea much. I think what's going on is that light from sun+sky is white, and shadows have only the sky component but not the sun component so it's a bit bluish. Also see: The Muller Formula[9], which says lighter colors should be hue shifted towards yellow and darker colors should be hue shifted towards violet.

 1.6 Custom controls

Web pages as documents have links that navigate to another page. Web pages as "apps" have that perform actions. My pages tend to have a few controls that don't quite match either of these.

I'm clearly not consistent across pages. I think I should spend more time looking at what controls other interactive pages use, and see if I can be more consistent with them.

 1.7 Arrows

I like arrows. I like arrows a lot.

With static diagrams it's fairly easy to figure out where to put arrows. With interactive diagrams we need to calculate the arrow placement. Alexander Wang has a great reference page[14] on this topic, and has a Javascript library called curved-arrows[15] (0.85 kbytes[16] gzipped+minified). Steve Ruiz has some notes on arrows[17] too, and also has a Javascript library called perfect-arrows[18] (2.6 kbytes[19] gzipped+minified).

Arrows are a little bit easier to implement in SVG than in Canvas.

1.7.1. SVG

In SVG, you have to create a marker inside <defs>, like this (my version modified slightly from the docs[20]):

  <marker id="arrowhead" viewBox="0 0 10 10" 
          refX="7" refY="5" markerUnits="strokeWidth"
          markerWidth="4" markerHeight="3" orient="auto">
    <path d="M 0 0 L 10 5 L 0 10 z"/>

Then you can annotate any line/path with the marker, like <path d="M 20,10 l 10,80" marker-end="url(#arrowhead)" fill="none" stroke="black"/>. Let's see what it looks like:

      <path d="M 20,10 l 10,80" />
      <path d="M 60,70 a 100,-30 0,0,0 100,0" />
      <path d="M 210,40 c 50,20 -30,30 0,50 " />
      <path d="M 270,70 c 0,-100 -100,-30 -200,-50 " />
Example of SVG arrows

There are a few tricky things with SVG arrows.

The marker contains the color. It's the fill color in the marker that draws the arrowhead, whereas it's the stroke color in the path that draws the tail. This has long been recognized as a problem, as seen in this discussion[21] suggesting that there might be a solution in svg 2. Also see this article[22] about styling <use> elements, although I don't think that solves the problem with arrowheads, because we both need fill to be none on the arrow and fill to be non-none on the arrowhead. Workaround: generate a new marker for each color :-( (Firefox has support for this SVG 2 feature but Chrome does not, so I don't use it)

    <path fill="none" stroke="red" marker-end="url(#arrowhead)" 
       d="M 20,10 l 260,10" />
Example of SVG arrow line and head colors not matching

The id is unique per page. If you have multiple <svg> elements, the id has to be unique across all of them. It's not per svg. Workaround: generate a new id for each svg :-( if you need them to be different in each diagram.

To apply alpha, use opacity instead of fill-opacity! See this stackoverflow answer[23].

1.7.2. Canvas

I wrote this function a long time ago, for Flash, and have continued using and tweaking it with my Canvas-based projects.

function lerp(a, b, t) { return a * (1-t) + b * t; }

function drawArrow(ctx, tail, head) {
    const stemLength = 0.5;
    const headLength = 0.5;
    const stemWidth = 0.15;
    const headWidth = 0.4;
    const tailLength = 0.1;
    let sx = lerp(tail[0], head[0], 0.2);
    let sy = lerp(tail[1], head[1], 0.2);
    let dx = (head[0] - tail[0]) * 0.7;
    let dy = (head[1] - tail[1]) * 0.7;

    ctx.moveTo(sx + dx*tailLength, sy + dy*tailLength);
    ctx.lineTo(sx - dy*stemWidth, sy + dx*stemWidth);
    ctx.lineTo(sx + dx*stemLength - dy*stemWidth, sy + dy*stemLength + dx*stemWidth);
    ctx.lineTo(sx + dx*(1-headLength) - dy*headWidth, sy + dy*(1-headLength) + dx*headWidth);
    ctx.lineTo(sx + dx, sy + dy);
    ctx.lineTo(sx + dx*(1-headLength) + dy*headWidth, sy + dy*(1-headLength) - dx*headWidth);
    ctx.lineTo(sx + dx*stemLength + dy*stemWidth, sy + dy*stemLength - dx*stemWidth);
    ctx.lineTo(sx + dy*stemWidth, sy - dx*stemWidth);
    ctx.lineTo(sx + dx*tailLength, sy + dy*tailLength);

It doesn't support curved paths like the SVG version, but it also doesn't have the fill vs stroke color problem like the SVG version.

1.7.3. Across elements

It turns out SVG elements default to overflow: hidden but you can change that CSS to overflow: visible to let the SVG exceed the containing box. I've written about this elsewhere on this page. The most unusual thing I've done with it is to have arrows extend far outside the <svg>. I haven't used this much, but here are some demos:

Example of arrows in text pointing inside a diagram
Interior arrows: arrows from text pointing inside a diagram

I haven't written a full tutorial on this topic but you can look at my implementation, interior-arrow.js. I also found leader-line[25], a library that implements arrows like this (it also has a vue wrapper[26]).

 2  Interactivity#

 2.1 Interaction feedback

Highlighting options

These effects and others can be combined. Also the drag and hover effects can be different. For example, set the mouse pointer and fill color on hover, and then add a drop shadow and slight rotation on drag. Try dragging this shape:

Interactive example: drag this shape

 2.2 Drag point

The simplest way to deal with dragging is to set the object's position to the mouse position. This works fine for small shapes but it has a problem that's noticeable with larger shapes. Suppose we pick up the square from its corner and drag to the right. What should happen? If setting the square's position to the mouse position, it will "snap" down and to the right. Instead, we should remember where on the square we're dragging from, and position the square relative to that.

Moving a shape to the mouse pointer: center vs offset

Try it yourself:

Interactive example: try dragging the square from its corner

What does the code look like?

  1. When starting a drag operation, remember the offset between the object position and the mouse position. offset = {dx: pos.x - event.x, dy: pos.y - event.y};
  2. When dragging, add the offset to the mouse position to get the object's position. pos.x = event.x + offset.dx; pos.y = event.y + offset.dy;

There are other equivalent ways to implement this, and which way is easiest will depend on the API and what else I'm doing with the mouse coordinates. When I'm using d3, d3-drag[27] supports this feature by setting the "subject" for the drag operation.

A different approach is each time mousemove is called, increment the object's position by the change in mouse position. In some browsers, mousemoveX, mousemoveY contain the change, and they continue to work even if the mouse is taken off screen (using Pointer Lock). I prefer using the absolute change since the drag started instead of relative position since the last mousemove. It allows constrained / snap-to-object movement as I explore on this page, and as you can see in the demo above.

Also see Peter Collingridge's interactive explanation[28] (2012, before pointerevents but same concepts apply).

 2.3 Expanded hit area

The previous section was about dragging something large. We have a different problem when dragging something small. With some input devices, it's hard to be precise enough to point to a small object. Even if I am able to point to it, the act of clicking will move my pointer slightly for some types of input, and that causes me to miss. Even when I do grab it, the mouse pointer or my finger can hide the object.

Interactive example: try dragging the circles

On the left is the regular object drag approach. On the right:

  1. An invisible draggable area is underneath the circle, to make it easier to pick up the cricle.
  2. The cursor goes away while dragging so that I can see the object being dragged. (Implementation could be better.) (Alternatively, use the crosshaircursor[29].)

How do I implement this with SVG? I usually make the invisible area draggable, and it updates an underlying position object. I then draw a non-draggable object on top of it at that same position. The next problem is that the non-draggable object eats up the mouse events before the invisible area receives them. Solution:

.draggable { cursor: grab; pointer-events: all; }
.object { pointer-events: none; }

Now SVG will not pass pointer events to the visible object on top, and only pass them to the invisible object underneath. The pointer-events[30] CSS property is quite useful here!

When using Canvas or WebGL, I can't use invisible SVG objects so I have to reimplement all of this. Either go through all your objects and find something close enough to the mouse pointer, or use Voronoi to build to build a map of which object is closest the mouse pointer. The Voronoi approach is used in the D3 community[31], in part because D3 comes with a nice Voronoi library.

 2.4 Transitions

The main reason I would use a transition is because I want to show the states in between two states the reader is looking at. Most of the time, the in-between states are automatically visible as the reader moves a slider or other control between one state and another. If I can redraw the diagram quickly enough, I don't need to add transitions or animations in those situations. Transitions are useful when switching between states where the slider/control doesn't go through the intermediate states. An example is the two orientations of hexagonal grids, "flat topped" and "pointy topped". Compare without and with transitions:

Interactive example: without and with transitions

On the left, you can see the two orientations but it's hard to see that they're related. The transition on the right diagram shows the connection between the two grid types.

I think the default transitions in CSS and d3 are pretty good, but for the hexagon page I added a slight overshoot, like easeInOutBack[32] but subtle.

 2.5 Comparisons

It's been observed that it's harder to assign a 1-5 star rating to something than to say whether one thing is better or worse than another. I first ran across this in the 1990s and kept noticing it after that. For visualizations, I find it's often hard to evaluate something on its own, but it's easier to see when it's next to something else.

I use this on my A* page, for:

I also use this on my hexagon page, for:

It's often better to show all the variants than to ask the reader to interact with the diagram to see the variants. Interactivity isn't always needed. Related: Ladder of Abstraction[33]

 2.6 Touch events

I'd love to write everything for desktop browsers and not worry about mobile, but the reality is that “desktop” and “mobile” are not great categories. Instead, I have:

narrow, wide, small, large, portrait, landscape, even print — lots of variety! I use responsive design for this, including switching between two and one column layout on some pages.
mouse, trackball, trackpad, touch.

I first design pages for mouse/trackball input. This means: left-click, left-drag, right-click, right-drag, hover. However, given how many trackpads there are, these days I limit myself to left-click, left-drag, hover.

 2.7 Sliders

Especially for debugging and tweaking, it's useful to attach a slider to some value. I think dat.gui[34] is a common choice, but these days html5 makes it easy to make your own in 30 lines of code. Demo (use View Source to see the code). I wanted to be able to use global sliders like this:

// Add slider - current value, min, max, number of digits
slider('velocity', 0, -5, 5, 4)
slider('num_sides', 2, 0, 5)

// Read the slider value like an ordinary field
for (let i = 0; i < slider.num_sides; i++) { }

// Set the slider value like an ordinary field
slider.velocity = 1.7321

Variants: a callback function that gets called whenever any or a specific slider changes; a user-visible label separate from the property name; make the url parameters match the sliders; store the value in an external object instead of inside slider. Since I copy these 30 lines into a project, I can easy modify it to match my needs instead of making a more complicated reusable library that supports all features.

 2.8 SVG outside its container

By default, <svg> elements clip the content to their container. This can be changed! By default, SVG uses overflow: hidden[35]. Compare:

Interactive example: hidden vs visible overflow on SVG

Try dragging the green circle to see how it feels different with and without the walls.

I discovered this feature many years ago (probably 2011) when I was testing in Internet Explorer, which used to use overflow: visible as the default, whereas Firefox used overflow: hidden as the default. At first I was annoyed that Internet Explorer was different, but then I realized, maybe its behavior is useful! I have experimented a little bit over the years:

Note that some browsers won't catch input events outside the boundary.

 2.9 Detect bad input

When making static diagrams, we can pick good illustrative examples. With the interactive diagrams, sometimes the reader will end up in a bad or unhandled state. Ideally, I'll design the controls so that the bad states aren't reachable. But if I can't do that, I try to detect the bad states and alert the reader. For an example of this, see the 2d visibility page. Move the squares so they overlap. It'll highlight the conflict in red because the algorithm doesn't handle overlapping lines.

 2.10 State machines

It's often useful to express the interaction state with a state machine, so that you can make sure every transition is handled. For example, even a simple thing like a button might have seven states, as shown in this interactive demo[39].

 2.11 Canvas + SVG overlaid

Sometimes I want to use both Canvas and SVG for the same diagram. SVG provides easy interactive elements; Canvas provides better performance and image handling. In these situations I will put an SVG over a Canvas:

<div style="position:relative">
  <canvas style="position:absolute;z-index:0" width="800" height="800"/>
  <svg style="position:relative;z-index:1" viewBox="0 0 800 800"/>

I've been using this technique less often these days, but I think CSS Grid would be a reasonable way to implement it too.

 2.12 Continuity

One problem I run into is that an algorithm that calculates f(x) is fine in production, but in an interactive setting, it needs to be continuous with respect to x. That way, as you manipulate x you can see how it affects the output f(x).

With pathfinding, this showed up when there are many ties for paths on the map. Each time I ran pathfinding, it might pick a different one. This is fine for a normal implementation, but for the interactive diagram, it was distracting to see the path "flicker" as you changed some unrelated parameter.

I also had this happen with map generation. Minor changes to some parameter wold cause the generated rivers to be in completely different places. I had to switch the interactive version to use a slower deterministic algorithm so that it wouldn't flicker.

 2.13 Islands of interactivity

I generally design my pages to be mostly static, with sections of the page being interactive. Each section is usually its own separate interactive section that doesn't interact with the rest of the page. Part of the reason for this is that if the reader is interacting with one section of the page, they're not able to see the other sections, so I don't want any "spooky action at a distance". But ideally my pages will work as static pages without needing any interactivity, and I've made some of my pages work without Javascript enabled.

Also see[40] and[41]

 3  General design#

 3.1 Color & size

Use brighter (saturated) colors for important things, but in small areas only. If it's large then it makes everything seem unimportant.

 3.2 HSL colors

When picking colors, the default is to use RGB. It's well known and widely supported. Web browsers also support HSL colors. I find these much easier to work with. The standard colors are easy to switch to HSL:

RGB vs HSL standard colors

Setting saturation to half (S=50%) results in colors I like better:

RGB vs HSL half saturation colors

With RGB, we replaced 0→64, 255→191. With HSL we had to change S from 100% to 50%. HSL's advantage shows up when I want to go beyond these. For example, changing hue:

RGB vs HSL change hue

or saturation:

RGB vs HSL change saturation

or lightness:

RGB vs HSL change lightness

I find it much easier to work with HSL than RGB. I can pick sets of colors that match each other. HSL is supported for SVG, HTML, and Canvas vector drawing, but not WebGL or Canvas pixel manipulation. HSL has some flaws, but since it's widely supported there's no real downside compared to RGB. For a better color space, consider Oklab[42].

This is a nice article about picking colors[43].

 3.3 Text shadow on header text

I used this trick on Circular Obstacle Pathfinding[44]: add a black text shadow around white text to increase its contrast a little bit. There's not a lot to do if your colors are black and white, but a dark gray or light gray offer more options.

Text shadow
Text shadow
Dark mode: without and with faint text shadow
Text shadow
Text shadow
Light mode: without and with faint text shadow

I've mostly used this for the headers, where the fonts are big enough for the text-shadow to work. I think it doesn't work as well for dark text on a light background.

Related: Lanczos sharpening filter[45].

 3.4 Two column layout

Some of my pages switch from one column on narrower screens to two columns on wider screens. Try them:

If you make the browser narrow it will shrink the two columns and then become one column, then shrink the text as you keep narrowing it.

I wrote a page with interactive diagrams that shows how the column size varies based on the browser width, and what CSS I used to implement it. Mouse over the diagrams to see the layout at that size.

The next challenge is the ordering of content. I wrote a blog post about ordering[46]. When A + B are side by side, and then I change to a one column layout, it's usually A then B. But if it's A+C and B side by side, and then I change to a one column layout, it's sometimes A then B then C, not A then C then B.

The CSS to change the layout when it's wide is easy. But writing the content to be balanced between columns and also reorder nicely is a lot of work. As mobile browsing has increased, I've focused on one column layouts, except for pages that will get a lot of views, where the extra work to make two columns might be worth it.

 4  Site#

 4.1 Longevity

One of my biggest frustrations on the web is following a link to something that would be useful, except the page is gone.

I try to follow an old piece of advice: "Cool URIs don't change" — from W3C[47]. If I'm going to rewrite something, I'll keep the old page up until the new page is ready, and sometimes even after that. I think it's a big mistake to erase everything first and because you're planning a redesign. Redesign it in a separate place, and then swap it in when it's ready.

Chasing virality is a trap[48]. It's fun to focus on the new & fresh, but I think most of the value comes from the long term. And that means a page that's several years old is still valuable. Not only do I want to keep those pages up, I also want to update them. I now treat my site like a wiki rather than a blog — I'll update pages as I learn more.

I think a lot of my site's success is because I keep my pages up. There have been lots of great sites over the years and almost all of them are gone now.

On reference pages especially, I make each section linkable. It's an extra step though so sometimes I forget. My site template turns these into clickable # markers on the right side of each section header. I'm not especially happy with how the markers look, but I'd rather have something than nothing, so I put them up and can improve them later.

 4.3 Reduce build steps

Modern web develop has a lot of neat tech — webpack, gulp, rollup, vite, elm, react, nuxt, astro, clojurescript, svelte, etc. As longevity is important to me, I want to not only still have my pages working in ten or twenty years, I want to be able to update them too. So I evaluate every piece of tech not only in how much time it might save me now but also how much time it might cost me later.

One of the biggest problems I've found in maintaining projects from ten or twenty years ago is that the build tools no longer work. Will today's build tools work in ten years? If not, that's more work for me later. For most of my pages I don't use a build step at all. When I do use a build step, I'm using esbuild, as it seems like the simplest and fastest tool. I also use Vue. I'm not the only one; Julia Evans describes picking tech to minimize build steps[49]. And Svelte is trying to minimize build time by switching from TypeScript to JavaScript[50].

I think most people don't work this way. They work on a project, they launch it, they might update it for a little while, but then it never gets updated again. So my choice of technologies is a little different from most people. I often make my own tools, which is great for me by myself, but makes it harder to share things.

 4.4 Add build steps

In the previous section I mentioned how I try to reduce the build steps not only because it slows down development but it lets me maintain pages for decades. But what about build steps that are fast and are under my control, so that they will still work in ten or twenty years? I've added some of these.

It's easier to write prose in markdown than in html, and many static site generators let you do this. I've used markdown for some pages but I found it doesn't give me enough control, especially for interactive content. I've switched to org-mode, which is part of emacs. But for many of my pages I don't even use org-mode, because it doesn't give me full control. I write xhtml directly.

To keep the entire site consistent in look & feel, I use xsltproc to process the xhtml into html, adding the header, logo, footer, comment section, etc. This has evolved over time and is a mess but it's a manageable mess. The xhtml has extra tags that I use to trigger xslt rules.

I have my emacs trigger the page build when I save the file. I try to keep this step fast so that I don't notice the build time.

I further modify the html output to improve load times. I modify the resource urls to include timestamps, to improve cachability, and I also modify the image tags to include image sizes, to improve initial layout.

So that means the individual pages are org-mode → xhtml → html → html, and the site as a whole is building the individual pages but also checking for broken links and building a site map for search engines. It's all custom code that I keep updating over time.

All of this limits the ability for other people to contribute to my pages, but that's not a priority for me.

 4.5 Freedom to experiment

Around 2015 I ran into a problem where my expectations for my pages were too high. This made it really hard to start on something new. I would make grand plans but never get anywhere. I decided to make a separate folder (called x) where I can put experimental projects. This worked really well! I made a lot more things this way. If something works out, I can move it out of that folder and turn it into a full project.

My regular projects are fixed scope, variable time. My x projects are fixed time, variable scope. I'll try to end them after one week. This helps me avoid going too deep into a rabbit hole.

Even though it helped "unblock" me, I found that it's so easy to make a new x project that I work on lots of small projects and not many big projects now. This is a downside I hadn't anticipated and don't know what to do about.

 4.6 Prerendering

The hexagons page has a lot of diagrams. To improve load time, I prerendered all the diagrams so that when you load the page, you get 600k of svg instead of javascript that has to generate the svg. This (surprisingly) improved layout speed and perceived load time. And it also means that the page works if you don't have javascript, or if you want to print it.

However, it was all a hack. At the page level, I wrote some hacks that reused Vue's pre-rendering support, without using Vue's official solution. And at the site level, I had lots of special cases for how the site should be built. And at the machine level, I was using Chrome's --headless --disable-gpu --dump-dom which mostly worked but sometimes failed.

Even though I liked the results, the hackery made me unhappy and I took it out in <2022-09>.

 4.7 Printing

I sometimes get requests for a printable version of one of my pages. I've decided not to make a separate printable version. Instead, I've attempted to make pages printer friendly in a generic way:

I wrote up the implementation details on my blog[51].

 4.8 OpenGraph meta tags

When I'm sharing on Twitter I'll attach images or a gif to the post. But when other people tweet my urls, it will use the images they attach, and if they don't attach an image, then it will fall back to meta tags to look for an image. So if I include those meta tags, I can attach a default image to be included.

I am tags like these, mostly preferring the OpenGraph ones but using some Twitter ones:

<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@redblobgames" />
<meta name="og:url" content="" />
<meta name="og:title" content="Making of: Line drawing tutorial" />
<meta name="og:description" content="Interactive tutorial about making interactive tutorials" />
<meta name="og:image" content="" />

I've only done this on my more popular pages. Most of my pages don't get linked to often so they're a lower priority for me.

Email me , or tweet @redblobgames, or comment: