Tortoise Explains the Dance

We turn to a story about the code behind our First Dancing Turtle and how words on the page inspire it to move.

Our title for this page unabashedly alludes to dialogs between Tortoise and Achilles in Gödel, Escher, and Bach. For those who have not read that book, those dialogs feature many self-referential and recursive puzzles. One of our favorite experiences with turtle geometry came from mentoring a gifted fifth grader in programming using fractals and recursion. blog

Finding the right song was a pleasant surprise. Once we had the actions() generator function transforming prose into turtle instructions, we needed an example poem to show off the code. Our first thought was Shakespeare, but in brief searching did not quickly find. Maybe a song? The Byrds' Turn, Turn, Turn? Wait! Are there moves in Turn the Beat Around? "Move your feet when you feel that beat, yeah!"

We also want to explain the code and some of the surprises in how it came to be.

Several imported functions must appear first for the javascript module to compile, but are not the most important part of the story. The addTurtle() function attaches a turtle geometry object to an SVG <path> element—so we can display the geometry. The context() and resize() functions from frame.js enable some specific communication with this wiki page.

import {addTurtle} from "https://turtle.dbbs.co/assets/turtle/svg-path.js" import {context, resize} from "https://turtle.dbbs.co/assets/turtle/frame.js"

The actions() generator function is where we convert prose into a collection of turtle instructions. Our current dance vocabulary recognizes only two words. Move is translated into fifteen steps in the collection. Turn changes the turtle's direction and takes one more step to make that turn immediately visible.

function* actions(prose) { const re = /move|turn/g // TODO synonyms? for (let [action] of prose.matchAll(re)) { if (action == "move") { for (let i=0; i<15; i++) { yield (turtle => turtle.move().draw()) } } else if (action == "turn") { yield (turtle => turtle.turn().move().draw()) } } }

We anticipate expanding this vocabulary with synonyms of move and turn and also words to change the angle of our turns, the color, or the stroke width. Perhaps we can also teach turtles to leave named trail markers along the path.

A little diversion into the backstory.

We have been tinkering with different implementations of turtle geometry in javascript for over a decade and interested in animation for several decades. Even in our first implementation of a turtle we created an animation of a pair of fractals: von Koch snowflake and Peano curve.

https://turtle.dbbs.co/assets/turtle/two-fractals.html Fractals from Interactive Turtle Graphics in a Web Browser blog

Given that backstory, we envisioned animating the path of our turtle. While writing actions() we considered 5 steps and 10 steps and settled on 15 steps. At the time we made this decision we had not yet chosen frames per second for our animation. We made this choice from a gut feeling.

A bigger surprise came from a whim in choosing to include one move after each turn. We weighed the question about including some glyph to represent the turtle and visualize its position and direction. That seemed like too much work for this stage of development. One step in the new direction was a shortcut. But the visual results created a delightful little asymmetry—well, there's almost always radial symmetry.

The code inside animate() feels the most confusing.

We create an infinite iterator from an immediately invoked function expression (IIFE) that is also an anonymous generator function. It repeatedly follows the instructions from the given prose and every six repetitions it clears the path to start over. The choice of six repetitions derives from what we know about the default turn size: our turtle turns 60 degrees and six such turns will complete a hexigon.

Then we also create an asynchronous recursive function that combines setTimeout() and requestAnimationFrame() as a clock to take steps through the infinite iterator. Feels like there should be a more expressive way to invoke the steps in the iterator inside the async clock.

function animate(el, prose) { const fps = 30 let requestId, i=0 let iterator = (function* () { do { if (i%6==0) { yield (turtle => turtle.clear().draw()) } yield* actions(prose) } while (++i) })() function step() { setTimeout(function () { requestId = window.requestAnimationFrame(step) let what = iterator.next() what.value(el.turtle) rescale(el.closest('svg')) resize() }, 1000/fps) // setTimeout used to allow speed to be controlled with fps } step() }

The attributes on an <svg> element often restrict the scale in "scalable vector graphics". The rescale() method removed width and height attributes and overrides the width defined in the style attribute. It also adds (or replaces) the viewBox attribute with values from the getBBox() method plus 1% additional padding. We use this within the animate() method to recompute the viewBox with each additional step to keep the entire path in view. One thing we discovered in testing, this method only works for SVGs that have been appended to the DOM.

function rescale (svg) { let {x,y,width,height} = svg.getBBox() let p = Math.min(width,height)*0.01 svg.setAttribute('viewBox', `${x-p} ${y-p} ${width+2*p} ${height+2*p}`) svg.removeAttribute("width") svg.removeAttribute("height") svg.style.width="100%" return svg }

The last two async functions are how we participate in the expectations of our ESM Snippet Template. The emit() function puts our SVG element on the page containing a single path. The bind() function awaits the page content from context()—one of the things we imported at the top of the program—and filters the story for paragraphs to joint into the prose for our dance instructions. It instruments the <path> with a turtle and begins the animation loop.

export async function emit(el) { el.innerHTML = `<svg viewBox="-100 -100 200 200"><path id="it" d="" fill="none" stroke="black"/></svg>` } export async function bind(el) { const {page:{story}} = await context() const text = story .filter(item => item.type == "paragraph") .map(item => item.text.toLowerCase()) .join("\n") addTurtle(it) animate(it, text) document.body.insertAdjacentHTML( 'afterbegin', `<p>${text}</p>`) }

In the frame below we can view the results.

//turtle.dbbs.co/assets/turtle/esm.html