Dave Widget — Integration examples

This page is a blank site that pulls in dave-widget.js with a single <script> tag. The head floating in the bottom-right corner follows your cursor. Below are the patterns for using Dave on a real site.

1 · Auto-mount cursor follower

One-liner in <head>. The widget infers where the sprites live from the script's own URL (it looks for sprites/ next to itself). That's what's running on this page.

<script src="/dave-widget.js"
        data-auto="bottom-right"
        data-size="140"></script>

Attributes: data-auto = corner (top-left, top-right, bottom-left, bottom-right, inline), data-size = pixel height, data-react=false to disable click reactions, data-base to override sprite location.

2 · Static image, anywhere (custom element)

Use <dave-face frame="…"> as a regular HTML tag. No JS call needed.

<dave-face frame="expressions/happy" size="48"></dave-face>
Dave
Product Manager · online
Dave
"Are we sure this is going to scale?"
Dave
"Okay that's actually hilarious 😂"

3 · Programmatic API

Three entrypoints on the global DaveWidget:

// Set base URL once (optional; auto-inferred otherwise)
DaveWidget.configure({ base: '/assets/dave/sprites' });

// Mount a follower imperatively (returns a controller)
const widget = DaveWidget.follow({
  position: 'top-right',
  size: 120,
  offset: { x: 16, y: 16 },
  react: true,
});
// widget.destroy();  // remove it later

// Build a static <img> for a single frame
const img = DaveWidget.image('expressions/wink', { size: 64 });
document.querySelector('.avatar').appendChild(img);

// Enumerate what's available
console.log(DaveWidget.frames.expressions);
// → ['happy','laugh','shocked', ... 18 total]

4 · Trigger sequences (wink, excited, etc.)

The follower exposes .play(name) for firing a named animation. Cursor tracking pauses while the sequence runs. Built-ins cover most reactions; register custom ones with DaveWidget.defineSequence(name, steps).

// Programmatic
DaveWidget.__auto.play('wink');
DaveWidget.play('excited');               // shortcut targeting the auto-mounted widget
DaveWidget.play([                           // inline custom sequence
  { frame: 'expressions/disgust', ms: 420 },
  { frame: 'expressions/suspicious', ms: 260 },
]);

// Register a named custom sequence, reusable anywhere
DaveWidget.defineSequence('celebrate', [
  { frame: 'expressions/laugh_big', ms: 240 },
  { frame: 'expressions/exclaim', ms: 300 },
  { frame: 'expressions/happy', ms: 220 },
]);

// HTML-only: any element with data-dave-play fires on click
<button data-dave-play="wink">Say hi</button>

These buttons use data-dave-play — no handler wiring, the widget owns the delegation. Any click inside [data-dave-play] fires that sequence on the auto-mounted Dave.

5 · Tune the motion live

Every motion knob is configurable. Drag the sliders — they reach into the auto-mounted follower via controller.update({…}) and take effect immediately.

DaveWidget.follow({
  maxLean: 10,      // px the image can shift toward the cursor (0 disables)
  maxLeanY: 6,      // vertical lean; default = maxLean * 0.6
  maxTilt: 3,       // degrees of head tilt (0 disables)
  deadZone: 0.2,    // cursor must pass this fraction of image size to swap angle
  reach: 0.6,       // cursor distance that maps to full lean; lower = more sensitive
  flip: true,       // mirror sprite when cursor is on the left
  follow: true,     // master switch; false = locked forward
});

You can also set these on the <script> tag via data-max-lean, data-max-tilt, data-dead-zone, data-reach, data-flip, data-follow.

6 · Rendered image endpoint (with bubbles)

Hit /api/render on the server (serve.py) to get a self-contained SVG of Dave with an optional speech or thought bubble. The sprite is base64-embedded, so the returned SVG is one file — drop it into an <img>, an email, an og:image, or a markdown ![].

<!-- Straight HTML -->
<img src="/api/render?frame=expressions/laugh_big&bubble=speech&text=Ship%20it!">

// Via the helper
const url = DaveWidget.imageUrl({
  frame: 'expressions/suspicious',
  bubble: 'thought',
  text: 'Are we sure this will scale?',
  size: 180,
  side: 'left',   // which side of Dave the bubble sits on
});

// Or inject an <img> directly
document.body.appendChild(DaveWidget.bubble({
  frame: 'expressions/wink', bubble: 'speech', text: 'you got this',
}));

Parameters: frame (required, e.g. expressions/happy or head_angles/up_right), bubble (none, speech, thought), text, size (sprite height px, 48–800), side (left | right). Unknown frames and path-traversal attempts return 400.

This preview hits /api/render on every change (served by serve.py). Keep text short — long strings wrap to multiple lines automatically.

7 · Frame gallery

Every shipped frame, rendered declaratively: