Skip to content

Latest commit

 

History

History
48 lines (33 loc) · 7.57 KB

File metadata and controls

48 lines (33 loc) · 7.57 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

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.

Development workflow

  • npm install — installs deps and, via the prepare hook, runs the build.
  • npm run buildtsc (emits CommonJS + .d.ts into dist/) then build:browser.
  • npm run build:browser — esbuild bundles src/browser.ts (with raphael + jquery inlined) into dist/gofigure.browser.js for the demo.
  • npm run typechecktsc --noEmit. This is the CI quality gate; there is no linter and no automated test suite. index.html is the manual test harness — add cases there to exercise changes.
  • Releasing: ./publish.sh typechecks, builds, requires a clean tree, tags the package.json version, pushes the tag, and runs gh release create with notes from the top section of CHANGELOG.md. Bump version in package.json and add a matching ## [x.y.z] heading to CHANGELOG.md first. .github/workflows/publish.yml then publishes to npm via OIDC trusted publishing (npm publish --provenance --access public; no NPM_TOKEN, needs id-token: write and npm ≥ 11.5.1) when the GitHub release is published. build-and-test.yml typechecks + 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 publish locally with a token / npm login). After that, configure a Trusted Publisher on npmjs.com (org/user eoftedal, repo eoftedal/gofigure, workflow filename publish.yml — all case-sensitive) and every later release goes through OIDC.
  • To run the demo: npm run build, then open index.html in a browser (Chrome recommended — SVG filter support is best there). It loads only dist/gofigure.browser.js plus the cufón font.
  • Types for Raphaël live in src/raphael.d.ts — a hand-written minimal ambient module, because the raphael npm 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 generated dist/gofigure.d.ts stays clean and consumers don't need @types/* for those.

Architecture (the parts that span multiple files/functions)

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 literally box() 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 to print() the Vegur cufón font, extracting the generated glyph path's d attribute, then removing the printed element. drawcenteredtext renders 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 with atan2 and 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 via createStep.
  • 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.

Packaging, the browser bundle, and fonts

  • raphael and jquery are runtime dependencies. dist/gofigure.js (the package main) requires them — it does not bundle them, so consumers get one shared copy.
  • The demo needs a browser-loadable script, so src/browser.tsdist/gofigure.browser.js is an esbuild IIFE that does inline raphael + jquery. It also assigns window.Raphael, because the cufón font file lib/Vegur.js calls Raphael.registerFont(...) against the global — the same Raphaël instance gofigure uses. Script order in index.html matters: the bundle must load before lib/cufon.js / lib/Vegur.js.
  • lib/ now holds only the cufón font assets (cufon.js, Vegur.js). The previously vendored raphael-min.js / jquery.min.js were 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 to src/. Regenerate/sync it manually if you need to ship plugin changes. See slide-plugins/reveal-js/Readme.md.