This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
gofigurejs is a small TypeScript library for animated, hand-drawn-style SVG drawings of boxes, arrows, lines, paths, circles, and text. Source is src/gofigure.ts; it compiles to dist/ (CommonJS + .d.ts) for publishing to npm. It is built on Raphaël (SVG abstraction) and jQuery (used for $.extend merges and its .animate() tween engine) — both are real runtime dependencies (imported from the raphael / jquery npm packages, not vendored). Version lives in package.json and the header comment of src/gofigure.ts.
The public API is documented in Readme.md. Read it before changing any method signature.
npm install— installs deps and, via thepreparehook, runs the build.npm run build—tsc(emits CommonJS +.d.tsintodist/) thenbuild:browser.npm run build:browser— esbuild bundlessrc/browser.ts(withraphael+jqueryinlined) intodist/gofigure.browser.jsfor the demo.npm run typecheck—tsc --noEmit. This is the CI quality gate; there is no linter and no automated test suite.index.htmlis the manual test harness — add cases there to exercise changes.- Releasing:
./publish.shtypechecks, builds, requires a clean tree, tags thepackage.jsonversion, pushes the tag, and runsgh release createwith notes from the top section ofCHANGELOG.md. Bumpversioninpackage.jsonand add a matching## [x.y.z]heading toCHANGELOG.mdfirst..github/workflows/publish.ymlthen publishes to npm via OIDC trusted publishing (npm publish --provenance --access public; noNPM_TOKEN, needsid-token: writeand npm ≥ 11.5.1) when the GitHub release is published.build-and-test.ymltypechecks + builds on every push. - First publish caveat: trusted publishing can't create a package that doesn't exist yet, so the initial version must be published manually (
npm publishlocally with a token /npm login). After that, configure a Trusted Publisher on npmjs.com (org/usereoftedal, repoeoftedal/gofigure, workflow filenamepublish.yml— all case-sensitive) and every later release goes through OIDC. - To run the demo:
npm run build, then openindex.htmlin a browser (Chrome recommended — SVG filter support is best there). It loads onlydist/gofigure.browser.jsplus the cufón font. - Types for Raphaël live in
src/raphael.d.ts— a hand-written minimal ambient module, because theraphaelnpm package ships no types. Extend it if you use more of Raphaël's surface. - Keep the public types (
FigureOptions,Step,DrawingArea,create) free of Raphaël/jQuery types so the generateddist/gofigure.d.tsstays clean and consumers don't need@types/*for those.
Everything reduces to one SVG path string. Each primitive (box, line, path, circle, drawtext) is a pure function returning { pathString }:
circle(x,y,r)is literallybox()with the radius set to the circle radius — a rounded rect whose corners consume the whole shape.- Text (
drawtext) is rendered by asking Raphaël toprint()the Vegur cufón font, extracting the generated glyph path'sdattribute, then removing the printed element.drawcenteredtextrenders once off-screen, parses the numeric coordinates out of the path string to measure width, then re-renders centered. This coordinate-parsing measurement is intentionally fragile ("quite ugly code, but it seems to work") — treat it carefully. - Arrowheads are extra
M…l…path segments computed withatan2and appended to the line path.
Two-layer fluent API (create → step):
create(containerId, w, h)builds a Raphaël canvas and returns a drawing area. Each primitive method on it (box,arrow,centeredText, …) spins up a fresh step viacreateStep.- A step accumulates
parts({ path, options }). Calling further primitives on a returned step chains more parts onto the same step (e.g.figure.arrow(...).centeredText(...)), so they animate together as a unit..animate()draws all parts of that step in sequence. bindClick(steps)advances through an array of steps, animating the next one on each click of the SVG.
The "draw-on" animation (animate): a hidden full path is created at stroke-width: 0, then jQuery tweens a dummy value 0→1. Each tick computes line.getSubpath(0, totalLength * pos), draws that partial path visibly, and removes the previous frame — producing the progressive-sketch effect. Multiple parts in a step are chained via done callbacks (run(lines, i+1)).
The hand-drawn/sketchy look is an optional, programmatically-built SVG filter. create(containerId, w, h, filterOptions?) — when filterOptions is passed (even {}) — calls addFilter, which builds a <filter> via document.createElementNS (a feTurbulence whose noise drives a feDisplacementMap), injects it into the container, and sets filter: url(#id) on that container. Omit the 4th argument for no filter. FilterOptions = { preset?: "chalk" | "marker"; id? } (default chalk). Each preset is a full primitive chain in filterPresets reproducing an original verbatim: chalk = the old inline #chalk filter; marker = textfilter2.svg's #textFilter (two fractal-noise displacement passes, a blur, and two feComposite ops referencing the undefined inputB, kept intentionally).
Critical — where the filter is applied: addFilter sets filter: url(#id) on the wrapper <div> that gofigure puts around the <svg> (matching gh-pages' #dds div { filter: ... } rule), NOT on the container. The marker preset's feComposite arithmetic k2=1 k3=1 doubles colors, so if the filter saw the container's opaque paper background it would blow it out to near-white; on the transparent wrapper only the black strokes are affected and the background (on the container) shows through. create grabs the wrapper via $("#id svg").wrap("<div>").parent()[0]; addFilter builds nodes with document.createElementNS and appends the defs <svg> to document.body.
Caveat when verifying: headless Chrome does not apply external-file filter refs (url(textfilter2.svg#…)), so rendering the gh-pages site headless looks unfiltered even though real Chrome applies it. The library uses an inline url(#id) ref, which works in both. The filter lives in the library, not index.html.
raphaelandjqueryare runtime dependencies.dist/gofigure.js(the packagemain)requires them — it does not bundle them, so consumers get one shared copy.- The demo needs a browser-loadable script, so
src/browser.ts→dist/gofigure.browser.jsis an esbuild IIFE that does inline raphael + jquery. It also assignswindow.Raphael, because the cufón font filelib/Vegur.jscallsRaphael.registerFont(...)against the global — the same Raphaël instance gofigure uses. Script order inindex.htmlmatters: the bundle must load beforelib/cufon.js/lib/Vegur.js. lib/now holds only the cufón font assets (cufon.js,Vegur.js). The previously vendoredraphael-min.js/jquery.min.jswere removed in favour of the npm deps.slide-plugins/reveal-js/plugin/gofigure/is a separate, self-contained copy of the old pre-TypeScript library with its own vendored libs; it is now stale relative tosrc/. Regenerate/sync it manually if you need to ship plugin changes. Seeslide-plugins/reveal-js/Readme.md.