# Dave Widget — LLM integration guide

Pixel-art animated head ("Dave"). Zero-dependency JS library + PNG sprites, served from
`https://getfenced-dave-face.pages.dev`. Drop into any website to:

1. Embed a **cursor-following head** (floating or inline).
2. Use **specific static frames** (18 expressions, 9 head angles) as avatars, reactions, chat bubbles, etc.

No build step. No framework coupling. One `<script>` tag.

---

## 1. One-line cursor follower (default corner mount)

```html
<script src="https://getfenced-dave-face.pages.dev/dave-widget.js" defer
        data-auto="bottom-right" data-size="140"></script>
```

- `defer` ensures HTML parsing is not blocked.
- `data-auto=<corner>` triggers the auto-mount. Corners: `top-left`, `top-right`, `bottom-left`, `bottom-right`, `inline`.
- Script is ~17 KB uncompressed (~6 KB gzip). First frame PNG is ~20 KB; other angle frames are fetched during browser idle time (`requestIdleCallback`) and total <250 KB, off the critical path.

## 2. Static face anywhere (custom element)

Registered as `<dave-face>`. Works as a normal inline-block element. Reactive to attribute changes.

```html
<dave-face frame="expressions/happy" size="48"></dave-face>
```

- `frame` — `<category>/<name>`; see frame list below.
- `size` — CSS height (bare number = px). Width auto.
- Add `eager` attribute to opt out of `loading="lazy"` (only if the face is above the fold).

## 3. Programmatic API

Global: `window.DaveWidget`.

```js
DaveWidget.configure({ base: 'https://getfenced-dave-face.pages.dev/sprites' });

// Mount a follower
const dave = DaveWidget.follow({
  mount: document.body,        // any element. Default body, fixed-positioned corner.
  position: 'bottom-right',    // 'inline' places in-flow, no fixed positioning
  size: 140,                   // px height
  offset: { x: 24, y: 24 },    // gap from corner, ignored for 'inline'
  react: true,                 // click reaction (shocked → laugh flash)
  maxLean: null,               // px image shifts toward cursor; default = size * 0.06
  maxLeanY: null,              // vertical lean; default = maxLean * 0.6
  maxTilt: 3,                  // degrees of head tilt; 0 disables
  deadZone: 0.2,               // 0–1; larger = steadier (cursor must travel further to swap angle)
  reach: 0.6,                  // 0–2; distance cursor needs to travel for full lean. Lower = more sensitive
  flip: true,                  // mirror sprite when cursor is on the left half
  follow: true,                // master switch; false = locked center
});

dave.update({ maxLean: 20, maxTilt: 6 });   // live retune after mount
dave.getOptions();                          // inspect current state
dave.destroy();                             // remove listeners + DOM

// Build a standalone <img> for one frame
const img = DaveWidget.image('expressions/wink', { size: 64, eager: false });
document.querySelector('.avatar-slot').appendChild(img);

// Enumerate available frames
DaveWidget.frames.expressions          // 18 names
DaveWidget.frames.head_angles          // 9 names
DaveWidget.frames.head_angles_mirrors  // 9 names (horizontally flipped variants)
```

## 4. Data attributes (auto-mount only)

Every option on `follow()` has a corresponding `data-*` attribute. Kebab-case, string values.

| Attribute | Type | Notes |
|---|---|---|
| `data-auto` | corner enum | Presence toggles auto-mount; value is default `position`. |
| `data-position` | corner enum | Overrides `data-auto` value. |
| `data-size` | int | px height. |
| `data-base` | URL | Override sprite folder location. Default: `sprites/` next to the script file. |
| `data-react` | `"false"` disables | Default true. |
| `data-max-lean` | number | px. |
| `data-max-lean-y` | number | px. |
| `data-max-tilt` | number | degrees. |
| `data-dead-zone` | 0–1 | |
| `data-reach` | 0–2 | |
| `data-flip` | `"false"` disables | Default true. |
| `data-follow` | `"false"` locks center | Default true. |

Example, tuned for a subtle effect:

```html
<script src="https://getfenced-dave-face.pages.dev/dave-widget.js" defer
        data-auto="bottom-right" data-size="120"
        data-max-lean="6" data-max-tilt="1.5" data-dead-zone="0.3"></script>
```

## 5. Frame catalog

All frames live under two categories. Use the string `"<category>/<name>"` anywhere a `frame` is expected.

**expressions** (18 — faces)

```
happy        laugh        shocked      angry       sad         furious
worried      disgust      wink         blank_stare laugh_big   exclaim
neutral      suspicious   dejected     tired       disappointed shock_wide
```

**head_angles** (9 originals + 9 mirrors — head orientation)

```
up_left      up           up_right
left         center       right
down_left    down         down_right
```

Each original has an auto-generated mirror: `up_left_mirror`, `up_mirror`, ..., `down_right_mirror`. Mirrors are horizontally flipped — useful for head-shake animations or when you need a pose that faces the other way.

## 6. Performance

Designed to coexist with any host site:

- **Script**: ~17 KB (~6 KB gzip), `defer`-safe. No blocking, no parse-time DOM work.
- **CSS**: Injected once, scoped to `.dw-*` classes, no `!important`, no layout-triggering animations (uses `transform` + `opacity` only).
- **First paint**: Only the `head_angles/center` PNG loads eagerly (~20 KB). Other swap frames preload with `fetchpriority="low"` inside `requestIdleCallback`.
- **Cursor handler**: `requestAnimationFrame`-throttled (max 60 updates/sec). `getBoundingClientRect` is cached between resize/scroll events. CSS variable writes are dirty-checked so idle cursors cost zero DOM mutations.
- **Events**: all listeners are `{ passive: true }` where relevant. No `mousemove` handler on `body`.
- **Caching**: Sprites served `Cache-Control: public, immutable, max-age=604800`. Library served cacheable with 1h browser / 1d edge TTL.
- **CORS**: All assets serve `Access-Control-Allow-Origin: *`. Safe to import cross-origin.

## 7. Common recipes

**Chat-message avatar tied to sender mood:**

```html
<dave-face frame="expressions/laugh_big" size="40"></dave-face>
<dave-face frame="expressions/suspicious" size="40"></dave-face>
```

**Change an existing face based on app state:**

```js
document.querySelector('dave-face').setAttribute('frame', 'expressions/angry');
```

**Mount + unmount with a route change (SPA):**

```js
let widget = null;
router.on('enter', () => widget = DaveWidget.follow({ position: 'bottom-right' }));
router.on('leave', () => widget?.destroy());
```

**Disable on mobile** (follower is desktop-only best experience):

```html
<script>
  if (window.matchMedia('(hover: hover)').matches) {
    const s = document.createElement('script');
    s.src = 'https://getfenced-dave-face.pages.dev/dave-widget.js';
    s.defer = true;
    s.dataset.auto = 'bottom-right';
    document.head.appendChild(s);
  }
</script>
```

**Respect `prefers-reduced-motion`:**

```js
const m = window.matchMedia('(prefers-reduced-motion: reduce)');
DaveWidget.follow({ maxTilt: m.matches ? 0 : 3, maxLean: m.matches ? 0 : 10 });
```

## 8. What this is not

- Not a React/Vue/Svelte component. Use the custom element or programmatic API.
- Not user-content-driven. Frames are a fixed set drawn by the artist.
- Not a mascot chatbot / NPC runtime. Just expressions + head-angle rendering.

## 9. Origin paths at a glance

```
https://getfenced-dave-face.pages.dev/dave-widget.js     ← the library
https://getfenced-dave-face.pages.dev/sprites/expressions/<name>.png
https://getfenced-dave-face.pages.dev/sprites/head_angles/<name>.png
https://getfenced-dave-face.pages.dev/                   ← integration docs (HTML)
https://getfenced-dave-face.pages.dev/playground         ← sequence demo
https://getfenced-dave-face.pages.dev/follow             ← full-size follow demo
https://getfenced-dave-face.pages.dev/llms.md            ← this file
```
