Skip to content

Add cheeks and hat options to the skin - #4

Merged
ScottMorris merged 8 commits into
mainfrom
feat/cheeks-and-hats
Oct 15, 2026
Merged

ScottMorris merged 8 commits into
mainfrom
feat/cheeks-and-hats

Conversation

@ScottMorris

@ScottMorris ScottMorris commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Cheeks are an option, off by default. skin.cheeks (boolean, default false) draws the rosy cheeks in the 3D model and the SVG fallback. The old cheek slabs sat inside the head's bevel and were effectively invisible; they now sit in front of the face and are clearly visible when switched on.
  • Hats. skin.hat is 'none' (default), 'peakCap', 'topHat' or 'party', with the Hat type and HATS array exported and optional colours.hat and colours.hatBand. The hat sits on the head between the ears and moves with it. While the party mood shows its own hat, a chosen peak cap or top hat scales and fades out, and returns afterwards. No new pose channel.
  • Party hat as an option. With hat: 'party' he wears the same party hat the party mood uses, all the time; in the party mood there is exactly one party hat, with no fade or flicker.
  • Fallback and demo. The SVG otter reads both options from options.skin, and the demo has a Cheeks toggle and a Hat select with all four choices.
  • Camera. The camera moved back a little (z 8.6 to 9.2, look-at y -0.45 to -0.3) so the party hat and the top hat are not clipped at the top of the canvas; the otter is about 7% smaller in the frame. This is a separate commit (fix(render)) that can be dropped on its own.

Known limitations

  • Looked at in software-rendered WebGL only, not on a real GPU. The peak cap's visor reads as a flat rectangle from the front, and the top hat's brim is mostly hidden by the head's front face. The hat colours are dark and low in contrast on the demo's near-black background.
  • The end states were checked by eye; a mid-tween frame of the hat swap was not captured.

Test plan

  • bun run test (176 tests), bun run lint, bun run format:check, bun run build
  • node scripts/check-package.mjs and bunx vite build demo
  • Stills: default, cheeks on, each hat including the party hat, the party mood with each hat, and the fallback
  • CI is green on this pull request
  • Look at the demo on a real GPU

**What changed**

- Pulled the `PerspectiveCamera` in `src/render/createJax.ts` back a little and re-centred its look-at point, so there is more room above his head.

**Why**

- The party hat already reached the top edge of the canvas, and the new hats stand as tall. Without the extra headroom they are clipped.

**Details**

- The camera moves from `z = 8.6` to `z = 9.2` and looks at `y = -0.3`. The figure is about 7% smaller in the frame.

**Validation**

- Checked in a software-rendered headless browser with every hat and the party mood. Not looked at on real hardware.
**What changed**

- Added `cheeks?: boolean` (default `false`) and `hat?: Hat` (default `'none'`) to `Skin`, with the `Hat` type and the `HATS` const (`'none'`, `'peakCap'`, `'topHat'`).
- Added optional `colours.hat` and `colours.hatBand` to the skin colours.
- `defaultSkin` sets cheeks off, no hat, a slate `hat` colour and a crimson `hatBand`.

**Why**

- The rosy cheeks were always on, and there was no way to dress him in a hat of the app's choosing.

**Details**

- The new colours are optional, so skins written against the earlier contract still type-check. The renderers fall back to a charcoal-purple hat and the skin's `accent` for the band.
- `colours.blush` stays, and now colours the cheeks and a few small props.

**Validation**

- `src/types.test.ts` covers `HATS`.
**What changed**

- `buildOtter` adds the cheek slabs only when `skin.cheeks` is `true`.
- `buildOtter` builds a peak cap or a top hat from flat slabs on the head pivot when `skin.hat` asks for one, exposed as `OtterRig.hat` (a group named `hat-peakCap` or `hat-topHat`).
- The chosen hat scales and fades out while `propHat` is up and returns after, using the same presence tween as the other props. No new pose channel is needed.

**Why**

- Cheeks become an opt-in look, and apps can dress him in a hat without touching the party mood's own paper hat, which keeps priority.

**Details**

- The head slab's bevelled front face stands proud of its nominal depth, so the cheeks moved forward to `z = 0.43`; they were previously buried in the fur and effectively invisible.
- The cap and the top hat are deep enough in `z` to wrap the front of the head, and sit between the ears without touching them.

**Validation**

- `src/model/otter.test.ts` covers the defaults, cheeks on and off, each hat's group, the hand-over with `propHat` and disposal of the new geometries.
- Looked at in a software-rendered headless browser. Not looked at on real hardware.
**What changed**

- `createFallbackJax` draws the cheeks (`lq-jax-cheek`) only when `options.skin.cheeks` is `true`.
- It draws the chosen hat as a flat SVG group (`lq-jax-worn-hat` with `lq-jax-hat-peakCap` or `lq-jax-hat-topHat`) using `colours.hat` and `colours.hatBand`.
- `FALLBACK_CSS` hides the worn hat for the party mood, with the same opacity transition as the props.

**Why**

- The fallback should match the 3D otter when the app passes the same `skin`.

**Details**

- Without a `skin` the output has no cheeks and no hat. The API is unchanged and there are no inline styles.

**Validation**

- `src/fallback/fallback.test.ts` checks the elements appear only when requested and that no `style` attributes are written.
**What changed**

- The demo has a `Cheeks` toggle and a `Hat` select (none, peak cap, top hat) that rebuild the running companions, in both the 3D and fallback paths.
- The README describes `cheeks`, `hat`, `colours.hat` and `colours.hatBand`.

**Why**

- The options are a matter of look, so they need to be judged by eye.

**Validation**

- Built the demo and drove it in a headless browser through each option.
@ScottMorris ScottMorris added the enhancement New feature or request label Oct 15, 2026
**What changed**

- `HATS` and `Hat` now include `'party'`, and the `Skin.hat` and `colours.hat` doc comments say how
  it behaves. This changes the shared contract in `src/types.ts` deliberately.
- `buildOtter` builds a `hat-party` group on the head for `hat: 'party'`, from the same
  `buildPartyHat` geometry as the party mood's `propHat` prop.
- With the party hat chosen, the mood prop is left empty and hidden, and the chosen hat ignores
  `propHat`, so it stays on with no fade and there is never a second one.

**Why**

- The party hat could only be seen in the party mood, so it was not available as a look.

**Validation**

- Tests cover the `HATS` values, the `hat-party` group, exactly one visible party hat at `propHat`
  0, 0.5 and 1, and unchanged behaviour for the other hats.
**What changed**

- `hat: 'party'` draws one `lq-jax-party-hat` group on the head in every mood.
- With it chosen, the mood's own `lq-jax-prop-hat` is not drawn, so the party mood adds no second
  hat. It carries no `lq-jax-worn-hat` class, so the CSS that hides worn hats does not touch it.
- The other hats keep hiding in the party mood.

**Validation**

- A test checks one party hat element in every mood and no inline `style` attributes.
**What changed**

- The demo's Hat select lists "party hat".
- The README paragraph on cheeks and hats names `'party'` and says it stays on in every mood.
@ScottMorris
ScottMorris merged commit 6c46b1d into main Oct 15, 2026
5 checks passed
@ScottMorris
ScottMorris deleted the feat/cheeks-and-hats branch October 15, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant