DEV Community

Unpublished Post. This URL is public but secret, so share at your own discretion.

Marketing code to developers

Last year I put a tweet that showed some code refactoring using animation. Many people asked how I did it.

I think it's interesting why people find things like this interesting. I beleive it has to do with visual movement, it's a lot more compelling that reading text, which is code. When colorful things move we pay attention.

So how did I build it? I did it the hard way. It's just a bunch of <div>s, with many setTimeout() that advance thru a series of steps, placing a css class to hide or show something. It took a lot of time to put together.

So I thought about how I could make this easier to do, maybe I could create a tool where I could build a code animation in minutes instead of hours. Over the past few weeks I started putting together that tool.

This article is about how I created that tool, and how you can do it yourself. I share all the tricks so you can use it for your landing page, docs site, presentation or as GIF on twitter.


Why it works?

(show image comparison)

text tweet -> image tweet -> animation tweet

text tweet: Svelte's compiler writes the hook code for you
image tweet: diff
animation tweet: animated diff


If your app has an API, or is a developer tool, then demonstrating how your code works is vital to getting your software adopted.
Get noticed is hard, because developers are busy writing code all day, getting them to read even more code to see how your thing works can be hard to do.

One way, and the approach I'll talk about in this article, is using animation to demo code. It can be used on your home page, in your docs site or even in blog posts. Stripe is doing this.

The main advantages of animation are:

  • It grabs attention. It's far more captivating than reading thru text.

- It conveys a lot of information quickly.

In this article, I'll show you how to you can roll your own, including syntax highlighting and animation tweening.

Setup

We're going to use Svelte for this example. It's ideal for this because it has built-in support for tweening animations, but React, Vue, etc will work well too.

Let's start by creating the project:

yarn init svelte@next example-project
Enter fullscreen mode Exit fullscreen mode

We're going to use CodeMirror to display the code and take care of all the syntax highlighting for us. I've created a thin wrapper that reduces the boilerplate needed to setup CodeMirror.

yarn add -D codemirror @joshnuss/svelte-codemirror
Enter fullscreen mode Exit fullscreen mode

Now let's add the component to our index page src/routes/index.svelte

<script>
  // import syntax highlighting for js
  import 'codemirror/mode/javascript/javascript'
  // import wrapper component
  import CodeMirror from '@joshnuss/svelte-codemirror'

  // reference to the codemirror api
  let editor

  // editor options
  // full list here: https://codemirror.net/doc/manual.html#config
  const options = {
    readOnly: true,
    lineNumbers: true,
    mode: "javascript",
    value: '' 
  }
</script>

<CodeMirror bind:editor {options}/>
Enter fullscreen mode Exit fullscreen mode

Animation loop

Let's say we want to animate several steps. It's a good idea to keep the steps as data, just in case we want to change things later.

We'll use an array:

const steps = [
  {
    type: 'append',
    text: "function foo() {\n}"
  }
]
Enter fullscreen mode Exit fullscreen mode

Each step has a type of animation (ie append, insert, delete, scroll, delay, etc) has two phases: start and stop. We can organize that into a data structure:

const commands = {
  // each function name maps to a command
  append(step) {
    // every command returns an object like this
    return {
      start() {
        // stuff to run at the beginning of the step
      },

      stop() {
        // stuff to run at the end of the step
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Then use a series of setTimeout() calls to advance thru each step:

const steps = [...]

onMount(() => next(0))

function next(index) {
  // find the current step
  const step = steps[index]
  // find the command for this step type
  // invoke the command function, passing the editor and step
  const command = commands[step.type](editor, step)

  // trigger the start of the animation
  if (command.start) command.start()

  // after 2 seconds, we'll move to the next step
  setTimeout(() => {
    // trigger the end of the animation
    if (command.stop) command.stop()

    // check that it's not the last step
    if (index < steps.length - 1) {
      // run next step
      next(index + 1)
    } else {
      // it's the last step, so go back to step #1
      next(0)
    }
  }, 2000)
}
Enter fullscreen mode Exit fullscreen mode

Inserting

With the animation loop in place, we can start defining commands. The first is an insert command:

// in commands.js
insert(editor, step) {
  return {
    start() {
      // move the cursor
      editor.setCursor(step.pos)
      // insert text
      editor.replaceSelection(step.text)

      // move the cursor to end
      const end = step.pos + step.text.length
      editor.setCursor(end)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice we didn't declare a stop() function. That's because we didn't need it. Phases are optional.

Appending

The append logic can reuse the insert command, by specifying the insert to happen after the last character.

// in commands.js
append(editor, step) {
  // compute length of code
  // getValue() returns the current code
  const end = editor.getValue().length

  // punt to `insert()` command
  return insert(editor, {...step, pos: end})
}
Enter fullscreen mode Exit fullscreen mode

Tweening & Easing

Currently transitions are linear. To make them feel more fluid, by using Svelte's tweened store and setting up an easing function.

TODO

Cursors

Moving the cursor is fairly straightforward.

Defining a command would look like:

commands = [
  {
    type: 'cursor',
    // set cursor at char 22
    pos: 22
  }
]
Enter fullscreen mode Exit fullscreen mode

The command handler for cursor would be:

function cursor(editor, step) {
  return {
    start() {
      // move cursor to `.pos`
      editor.setCursor(step.pos)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Highlighting text

Highlighting code can be done with a CSS class name or CSS attributes using the editor.markText().

Defining a command will look like this:

commands = [
  {
    type: 'select',
    // start a char 0
    from: 0,
    // end at char 10
    to: 0,
    // set a class name (optional)
    class: 'bg-green-300',
    // set styles (optional)
    style: 'background: green; color: white; text-decoration: underline'
  }
]
Enter fullscreen mode Exit fullscreen mode

Then the command handler would look like:

function select(editor, step) {
  return {
    // in start phase, mark text span
    start() {
      const from = editor.indexFromPos(step.from)
      const to = editor.indexFromPos(step.to)

      // mark a text span
      editor.markText(from, to, {className: step.class, style: step.style})
    },
    // in stop phase, clear the selection
    stop() {
      // clear all marks
      editor
        .getAllMarks()
        .forEach(mark => mark.clear())
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Deleting

TODO

Scrolling

There are several ways we can scroll:

  • To a line
  • To a y position
  • To an x position

x and y are easiest:

// in commands.js
function scroll(editor, step) {
  return {
    start() {
      editor.scrollTo(step.x, step.y)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

To find the y coordinate of a line number, we need to figure out how tall each line is, and then multiply by line:

// in commands.js
function scroll(editor, step) {
  return {
    start() {
      let y = step.y

      // check if scrolling to a line
      if (step.line) {
        // get total number lines in editor
        const lines = editor.lineCount()
        // get bounding box of editor
        const scrollInfo = editor.getScrollInfo()
        // compute the height of a line
        const lineHeight = scrollInfo.height/lines

        // y is line height multiplied by line number
        y = lineHeight * step.line
      }

      // scroll
      editor.scrollTo(step.x, y)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Scroll with tweening

TODO

Delays

This is the easiest one to implement, it's just a command that does nothing:

// in commands.js
function delay() {
  // do nothing, no start() or stop() functions
  return {}
}
Enter fullscreen mode Exit fullscreen mode

Annotations

Showing code alone doesn't explain what's going on. It helps to show captions at the bottom of the editor.

Let's add another attribute caption to each step in the timeline:

const steps = [
  {
    type: 'append',
    // add a caption
    caption: "adding a function"
    text: "function foo() {\n}"
  }
]
Enter fullscreen mode Exit fullscreen mode

Now we can float the caption above the code

<div class="annotations">
  {#if currentStep.caption}
    <div class="text">
      {currentStep.caption}
    </div>
  {/if}
</div>
Enter fullscreen mode Exit fullscreen mode

It's handy to support markdown in captions so we can can bold or link things:

# install markdown parser
yarn add -D marked
Enter fullscreen mode Exit fullscreen mode

Then wrap call marked() on the currentStep.caption

<script>
  import marked from 'marked'
</script>

<div class="annotations">
  {#if currentStep.caption}
    <div class="text">
      {@html marked(currentStep.caption)}
    </div>
  {/if}
</div>
Enter fullscreen mode Exit fullscreen mode

When the caption changes between step, it would be good to have a transition. We can use svelte's fly animation for this:

<script>
  import { fly, fade } from 'svelte/transition'
</script>

<div class="annotations">
  <!-- use #key to make animation change every time caption changed -->
  {#key currentStep.caption}
    {#if currentStep.caption}
      <!-- fly in, fade out -->
      <div class="text" in:fly={{y: 100}} out:fade>
        {@html marked(currentStep.caption)}
      </div>
    {/if}
  {/key}
</div>
Enter fullscreen mode Exit fullscreen mode

Publishing events

Often you want to synchronize the animation with other elements on the page. We accomplish that by firing events whenever a step starts or stops:

const dispatch = createEventDispatcher()

function next(index) {
  const step = steps[index]
  // fire start event, passing current step
  dispatch('stepstart', step)

  setTimeout(() => {
    // fire end event, passing current step
    dispatch('stepend', step)

    if (index < steps.length - 1) {
      next(index+1)
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

Client code can capture those events:

<script>
  // declare event handlers
  function start({detail: step}) {
    // show/hide/scroll accompanying elements
  }
  function end({detail: step}) {
    // show/hide/scroll accompanying elements
  }
</script>

<CodeTimeline on:stepstart={start} on:stepend={end}/>
Enter fullscreen mode Exit fullscreen mode

Conclusion

Animated code is a great way to showcase what your software does. It's captivating to the user, and you can pack in a lot of information quickly.

Building it a little tricky, but all you need is a sequence of steps and a loop that updates the UI at the start/stop of each step.

If you'd prefer to use a premade version of this, check out https://codinator.app.

P.S. This comes from experiments I've been doing over at https://1000experiments.dev, lot's more info and examples there.

Top comments (0)