mochi-avatar 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BRAND.md +85 -0
- package/LICENSE.md +104 -0
- package/README.md +135 -0
- package/dist/canvas2d/avatar.d.ts +264 -0
- package/dist/canvas2d/avatar.d.ts.map +1 -0
- package/dist/canvas2d/avatar.js +811 -0
- package/dist/canvas2d/avatar.js.map +1 -0
- package/dist/canvas2d/face.d.ts +74 -0
- package/dist/canvas2d/face.d.ts.map +1 -0
- package/dist/canvas2d/face.js +298 -0
- package/dist/canvas2d/face.js.map +1 -0
- package/dist/canvas2d/paths.d.ts +13 -0
- package/dist/canvas2d/paths.d.ts.map +1 -0
- package/dist/canvas2d/paths.js +24 -0
- package/dist/canvas2d/paths.js.map +1 -0
- package/dist/characters/colourways.d.ts +78 -0
- package/dist/characters/colourways.d.ts.map +1 -0
- package/dist/characters/colourways.js +76 -0
- package/dist/characters/colourways.js.map +1 -0
- package/dist/characters/index.d.ts +8 -0
- package/dist/characters/index.d.ts.map +1 -0
- package/dist/characters/index.js +8 -0
- package/dist/characters/index.js.map +1 -0
- package/dist/characters/mochi.d.ts +19 -0
- package/dist/characters/mochi.d.ts.map +1 -0
- package/dist/characters/mochi.js +66 -0
- package/dist/characters/mochi.js.map +1 -0
- package/dist/core/colour.d.ts +25 -0
- package/dist/core/colour.d.ts.map +1 -0
- package/dist/core/colour.js +37 -0
- package/dist/core/colour.js.map +1 -0
- package/dist/core/envelope.d.ts +153 -0
- package/dist/core/envelope.d.ts.map +1 -0
- package/dist/core/envelope.js +141 -0
- package/dist/core/envelope.js.map +1 -0
- package/dist/core/geometry.d.ts +106 -0
- package/dist/core/geometry.d.ts.map +1 -0
- package/dist/core/geometry.js +157 -0
- package/dist/core/geometry.js.map +1 -0
- package/dist/core/idle.d.ts +163 -0
- package/dist/core/idle.d.ts.map +1 -0
- package/dist/core/idle.js +262 -0
- package/dist/core/idle.js.map +1 -0
- package/dist/core/layout.d.ts +142 -0
- package/dist/core/layout.d.ts.map +1 -0
- package/dist/core/layout.js +172 -0
- package/dist/core/layout.js.map +1 -0
- package/dist/core/lens.d.ts +47 -0
- package/dist/core/lens.d.ts.map +1 -0
- package/dist/core/lens.js +67 -0
- package/dist/core/lens.js.map +1 -0
- package/dist/core/looks.d.ts +77 -0
- package/dist/core/looks.d.ts.map +1 -0
- package/dist/core/looks.js +168 -0
- package/dist/core/looks.js.map +1 -0
- package/dist/core/motion.d.ts +168 -0
- package/dist/core/motion.d.ts.map +1 -0
- package/dist/core/motion.js +527 -0
- package/dist/core/motion.js.map +1 -0
- package/dist/core/mouth.d.ts +93 -0
- package/dist/core/mouth.d.ts.map +1 -0
- package/dist/core/mouth.js +92 -0
- package/dist/core/mouth.js.map +1 -0
- package/dist/core/plain.d.ts +23 -0
- package/dist/core/plain.d.ts.map +1 -0
- package/dist/core/plain.js +63 -0
- package/dist/core/plain.js.map +1 -0
- package/dist/core/spec.d.ts +156 -0
- package/dist/core/spec.d.ts.map +1 -0
- package/dist/core/spec.js +236 -0
- package/dist/core/spec.js.map +1 -0
- package/dist/core/spring.d.ts +53 -0
- package/dist/core/spring.d.ts.map +1 -0
- package/dist/core/spring.js +68 -0
- package/dist/core/spring.js.map +1 -0
- package/dist/core/vocabulary.d.ts +182 -0
- package/dist/core/vocabulary.d.ts.map +1 -0
- package/dist/core/vocabulary.js +66 -0
- package/dist/core/vocabulary.js.map +1 -0
- package/dist/element/dough-avatar.d.ts +34 -0
- package/dist/element/dough-avatar.d.ts.map +1 -0
- package/dist/element/dough-avatar.js +189 -0
- package/dist/element/dough-avatar.js.map +1 -0
- package/dist/element/index.d.ts +3 -0
- package/dist/element/index.d.ts.map +1 -0
- package/dist/element/index.js +35 -0
- package/dist/element/index.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/svg/silhouette.d.ts +97 -0
- package/dist/svg/silhouette.d.ts.map +1 -0
- package/dist/svg/silhouette.js +143 -0
- package/dist/svg/silhouette.js.map +1 -0
- package/package.json +90 -0
package/BRAND.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Brand, Character, and Trademark
|
|
2
|
+
|
|
3
|
+
[`LICENSE.md`](LICENSE.md) splits this package in two. **Part 1 is MIT and covers
|
|
4
|
+
the engine** — the geometry, the physics, the format, the renderers. **Part 2
|
|
5
|
+
covers the character**, and it is not MIT.
|
|
6
|
+
|
|
7
|
+
This file is the reasoning behind that split. `LICENSE.md` is the operative
|
|
8
|
+
document; where the two differ, `LICENSE.md` governs.
|
|
9
|
+
|
|
10
|
+
This is the same split used by Rust, Kubernetes, Mozilla, and Docker: the code
|
|
11
|
+
is free, the identity is not.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. What is reserved
|
|
16
|
+
|
|
17
|
+
**The Mochi character.** A soft green ovoid companion with a face, defined by
|
|
18
|
+
the parameter set `MOCHI` in `src/shared/avatar-spec.ts` and rendered by the
|
|
19
|
+
geometry in `src/renderer/companion/rig/`. The character comprises, without
|
|
20
|
+
limitation:
|
|
21
|
+
|
|
22
|
+
- the silhouette — an ovoid whose widest point sits at 0.295 of its height,
|
|
23
|
+
formed from superellipse exponents 1.86 above the waist and 2.58 below;
|
|
24
|
+
- the two-tone flat shading, the lit copy displaced up and to the right,
|
|
25
|
+
leaving an uncovered band along the lower-left edge;
|
|
26
|
+
- the palette `#8ec8a8` body, `#7dbd99` shadow, `#24463a` ink, `#ef8f86` cheek;
|
|
27
|
+
- the facial layout — paired round eyes at 0.3 of half-width and 0.46 of
|
|
28
|
+
height, a lower-arc mouth at 0.24, radial cheeks at 0.62 / 0.33;
|
|
29
|
+
- the eight named expressions and the deformation values that produce them;
|
|
30
|
+
- the area-preserving squash, the height-proportional lean, and the breathing
|
|
31
|
+
cycle that together give the character its motion identity;
|
|
32
|
+
- any work derived from, or substantially similar to, the above.
|
|
33
|
+
|
|
34
|
+
**The artwork.** `src/characters/__fixtures__/mochi-icon.png`, and every
|
|
35
|
+
rendering of the character produced by this software.
|
|
36
|
+
|
|
37
|
+
**The name and marks.** "Mochi" as the name of this application, the character
|
|
38
|
+
name, and the icon as used to identify the software or the character.
|
|
39
|
+
|
|
40
|
+
Copyright © 2026 HANDO K.K. All rights reserved. Not licensed under the MIT
|
|
41
|
+
licence, and not placed in the public domain.
|
|
42
|
+
|
|
43
|
+
## 2. What you may do without asking
|
|
44
|
+
|
|
45
|
+
- Use, modify, and redistribute the **engine** under the MIT licence, including
|
|
46
|
+
in commercial work.
|
|
47
|
+
- Display and animate **Mochi herself** in your own projects, including ones you
|
|
48
|
+
sell. She is not a paid asset and there is no non-commercial restriction.
|
|
49
|
+
- Design **your own** avatar as a `FaceSpec` JSON file, using the plugin format
|
|
50
|
+
documented in `src/shared/avatar-spec.ts`. Your parameter values are yours.
|
|
51
|
+
The format, the renderer, and the built-in `MOCHI` values are not.
|
|
52
|
+
- Refer to the project by name in prose — reviews, articles, tutorials,
|
|
53
|
+
comparisons, and "works with Mochi" statements. Nominative use is fine and
|
|
54
|
+
needs no permission.
|
|
55
|
+
|
|
56
|
+
## 3. What requires written permission
|
|
57
|
+
|
|
58
|
+
- Presenting the character under another name, or as a character of your own.
|
|
59
|
+
- Selling her _as_ the goods — asset packs, sticker sets, merchandise.
|
|
60
|
+
- Using the character or the name as the primary identity of a product or
|
|
61
|
+
organisation.
|
|
62
|
+
- Using the character or the marks on merchandise, in a logo, or in any way
|
|
63
|
+
suggesting affiliation, sponsorship, or endorsement.
|
|
64
|
+
- Registering the character, the name, or any confusingly similar mark as a
|
|
65
|
+
trademark in any jurisdiction, or as a copyright in any registry.
|
|
66
|
+
- Training a generative model for the purpose of reproducing the character.
|
|
67
|
+
|
|
68
|
+
**If you want a character of your own, change the numbers.** The format exists
|
|
69
|
+
precisely so that costs a JSON file rather than a fork: start from `PLAIN`, and
|
|
70
|
+
what comes out is yours, with nothing here constraining it.
|
|
71
|
+
|
|
72
|
+
## 4. Attribution
|
|
73
|
+
|
|
74
|
+
Where attribution is required by the MIT licence, use:
|
|
75
|
+
|
|
76
|
+
> Mochi © 2026 HANDO K.K. — https://github.com/xiaolai/mochi
|
|
77
|
+
|
|
78
|
+
## 5. Contact
|
|
79
|
+
|
|
80
|
+
Requests for permission: https://github.com/xiaolai/mochi/issues
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
_This document states the licensor's position. It is not legal advice, and it
|
|
85
|
+
does not enlarge or reduce any right either party has under applicable law._
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Licence
|
|
2
|
+
|
|
3
|
+
This package is under **two** licences, split by directory. The engine is MIT.
|
|
4
|
+
The character is not.
|
|
5
|
+
|
|
6
|
+
| Path | Licence |
|
|
7
|
+
| ------------------------------------------------------------ | ------------------------------------ |
|
|
8
|
+
| everything except `src/characters/` (and `dist/characters/`) | MIT — Part 1 |
|
|
9
|
+
| `src/characters/` and `dist/characters/` | The Mochi Character Licence — Part 2 |
|
|
10
|
+
|
|
11
|
+
The split exists because the two things are genuinely different. The engine is a
|
|
12
|
+
way of drawing soft-bodied characters and belongs to anyone who wants it. Mochi
|
|
13
|
+
is one specific character, and she is not a free asset.
|
|
14
|
+
|
|
15
|
+
**If you want a character of your own, the engine is all you need.** Start from
|
|
16
|
+
`PLAIN`, move the numbers, and what comes out owes nothing to Part 2.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Part 1 — MIT License (the engine)
|
|
21
|
+
|
|
22
|
+
Applies to every file in this package **except** those under `src/characters/`
|
|
23
|
+
and `dist/characters/`.
|
|
24
|
+
|
|
25
|
+
Copyright (c) 2026 HANDO K.K.
|
|
26
|
+
|
|
27
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
28
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
29
|
+
in the Software without restriction, including without limitation the rights
|
|
30
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
31
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
32
|
+
furnished to do so, subject to the following conditions:
|
|
33
|
+
|
|
34
|
+
The above copyright notice and this permission notice shall be included in all
|
|
35
|
+
copies or substantial portions of the Software.
|
|
36
|
+
|
|
37
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
38
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
39
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
40
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
41
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
42
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
43
|
+
SOFTWARE.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Part 2 — The Mochi Character Licence
|
|
48
|
+
|
|
49
|
+
Applies to `src/characters/` and `dist/characters/`: the `MOCHI` parameter set,
|
|
50
|
+
the colourways, and the reference artwork. Together these are the **Character**.
|
|
51
|
+
|
|
52
|
+
Copyright © 2026 HANDO K.K. All rights reserved.
|
|
53
|
+
|
|
54
|
+
### You may, without asking
|
|
55
|
+
|
|
56
|
+
- **Use her.** Display and animate the Character in your own work — a website, an
|
|
57
|
+
app, a game, a school project, a video — including work you sell.
|
|
58
|
+
- **Recolour her** using the colourways in this package, or your own.
|
|
59
|
+
- **Redistribute this package** unmodified, including as a dependency.
|
|
60
|
+
- **Name her.** Say your project uses Mochi. Nominative reference needs no
|
|
61
|
+
permission.
|
|
62
|
+
|
|
63
|
+
There is no fee, no registration, and no requirement to be non-commercial.
|
|
64
|
+
|
|
65
|
+
### You may not, without written permission
|
|
66
|
+
|
|
67
|
+
- **Rename her.** Presenting the Character under another name, or as a character
|
|
68
|
+
of your own creation, is the one thing this licence exists to prevent.
|
|
69
|
+
- **Sell her as the goods.** She may appear in what you sell; she may not _be_
|
|
70
|
+
what you sell — no asset packs, sticker sets, merchandise, or NFTs.
|
|
71
|
+
- **Make her your brand.** Using the Character as the primary identity of a
|
|
72
|
+
product or organisation, or in a way suggesting affiliation with or
|
|
73
|
+
endorsement by HANDO K.K.
|
|
74
|
+
- **Register her.** As a trademark, design right, or copyright, in any
|
|
75
|
+
jurisdiction.
|
|
76
|
+
- **Train on her** for the purpose of generating her, or characters
|
|
77
|
+
substantially similar to her.
|
|
78
|
+
|
|
79
|
+
### Attribution
|
|
80
|
+
|
|
81
|
+
Not required, but appreciated:
|
|
82
|
+
|
|
83
|
+
> Mochi © 2026 HANDO K.K. — https://github.com/xiaolai/mochi
|
|
84
|
+
|
|
85
|
+
### Modifications
|
|
86
|
+
|
|
87
|
+
You may change her parameters for your own use. If the result is still
|
|
88
|
+
recognisably Mochi, this licence still applies to it. If it is a different
|
|
89
|
+
creature, it is yours and this licence has nothing to say about it — which is
|
|
90
|
+
the intended outcome, and why the format is data rather than code.
|
|
91
|
+
|
|
92
|
+
### No warranty
|
|
93
|
+
|
|
94
|
+
The Character is provided "as is", without warranty of any kind.
|
|
95
|
+
|
|
96
|
+
### Asking
|
|
97
|
+
|
|
98
|
+
Permission for anything above: https://github.com/xiaolai/mochi/issues
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
_Part 2 states the licensor's position. It is not legal advice, and it does not
|
|
103
|
+
enlarge or reduce any right either party has under applicable law. Nothing in
|
|
104
|
+
Part 2 restricts any right granted by Part 1 over the engine._
|
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Mochi
|
|
2
|
+
|
|
3
|
+
<img src="https://raw.githubusercontent.com/xiaolai/mochi/main/src/characters/__fixtures__/mochi-icon.png" width="120" align="right" alt="Mochi">
|
|
4
|
+
|
|
5
|
+
A soft-body companion character, and the engine that draws her.
|
|
6
|
+
|
|
7
|
+
She is an ovoid whose widest point sits at 0.295 of her height, formed from
|
|
8
|
+
superellipse exponents 1.86 above the waist and 2.58 below. She squashes without
|
|
9
|
+
changing area, leans by a shear pinned at the point where she meets the surface,
|
|
10
|
+
breathes on a one-sided curve and settles on an underdamped spring. Her face
|
|
11
|
+
rides on her at a grip of 0.82 — following the body without looking printed on.
|
|
12
|
+
|
|
13
|
+
Zero runtime dependencies.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install mochi-avatar
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## From a plain HTML page
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<script type="module">
|
|
23
|
+
import 'mochi-avatar/element'
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<dough-avatar emotion="happy" style="width: 200px; height: 200px"></dough-avatar>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That is the whole integration. The element makes its own canvas, sizes it at
|
|
30
|
+
device resolution, runs the frame loop, and stops when it leaves the document.
|
|
31
|
+
|
|
32
|
+
Attributes: `emotion` (one of the eight below), `size` (a percentage or
|
|
33
|
+
`fit-canvas`), `face` (a `FaceSpec` as JSON). Removing an attribute resets it.
|
|
34
|
+
|
|
35
|
+
## Against a canvas
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import { DoughAvatar, MOCHI } from 'mochi-avatar'
|
|
39
|
+
|
|
40
|
+
const avatar = new DoughAvatar(canvas.getContext('2d'), { face: MOCHI, size: 'fit-canvas' })
|
|
41
|
+
avatar.resize(300, 300, devicePixelRatio)
|
|
42
|
+
avatar.setEmotion({ emotion: 'happy', intensity: 1 })
|
|
43
|
+
|
|
44
|
+
const tick = (now) => {
|
|
45
|
+
avatar.render(now)
|
|
46
|
+
requestAnimationFrame(tick)
|
|
47
|
+
}
|
|
48
|
+
requestAnimationFrame(tick)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Expressions** — `neutral` · `happy` · `shy` · `sad` · `angry` · `surprised` ·
|
|
52
|
+
`thinking` · `sleepy`. Each is a set of multipliers on the neutral geometry, not
|
|
53
|
+
separate artwork, so one silhouette carries all eight.
|
|
54
|
+
|
|
55
|
+
**Colourways** — `matcha` (the original) · `sakura` · `kinako` · `yuzu` ·
|
|
56
|
+
`ramune` · `budo`.
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { mochiIn, MOCHI } from 'mochi-avatar'
|
|
60
|
+
const sakura = mochiIn('sakura', MOCHI)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Just the geometry
|
|
64
|
+
|
|
65
|
+
`core` is pure arithmetic. It returns points and numbers, and imports no canvas,
|
|
66
|
+
no document and no platform — useful for hit-testing, your own renderer, or
|
|
67
|
+
working out where to put something.
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import { domeOutline, squashed, widthAt } from 'mochi-avatar'
|
|
71
|
+
|
|
72
|
+
const shape = {
|
|
73
|
+
halfWidth: 50,
|
|
74
|
+
height: 78,
|
|
75
|
+
waist: 0.295,
|
|
76
|
+
upperShoulder: 1.86,
|
|
77
|
+
lowerShoulder: 2.58,
|
|
78
|
+
lean: 0,
|
|
79
|
+
}
|
|
80
|
+
domeOutline(shape) // the closed outline, as points
|
|
81
|
+
widthAt(shape, 0.5) // half-width at half height, 0..1
|
|
82
|
+
squashed(shape, 0.2) // area-preserving
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Where it runs
|
|
86
|
+
|
|
87
|
+
| Target | How |
|
|
88
|
+
| ---------------- | ---------------------------------- |
|
|
89
|
+
| Browser | `canvas.getContext('2d')` |
|
|
90
|
+
| Worker | `OffscreenCanvas` |
|
|
91
|
+
| Node | `@napi-rs/canvas` or `node-canvas` |
|
|
92
|
+
| No canvas at all | `core` only, or the SVG emitter |
|
|
93
|
+
|
|
94
|
+
`CanvasRenderingContext2D` is used structurally, so no adapter is needed. The
|
|
95
|
+
conformance test has been rendering her under Node this whole time.
|
|
96
|
+
|
|
97
|
+
## Making your own character
|
|
98
|
+
|
|
99
|
+
A face is **data**, not code — see `FaceSpec`. A design is a JSON file, and the
|
|
100
|
+
format bounds every field so a bad one is refused with a reason instead of
|
|
101
|
+
rendering something wrong in a way nothing mentions.
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import { DoughAvatar, PLAIN } from 'mochi-avatar'
|
|
105
|
+
new DoughAvatar(ctx, { face: { ...PLAIN, waist: 0.5, colBody: '#c88e9d' }, size: 'fit-canvas' })
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`PLAIN` is the default face and is deliberately plain — a near-symmetric egg,
|
|
109
|
+
one flat tone, no blush. It exists to be replaced.
|
|
110
|
+
|
|
111
|
+
## Licence
|
|
112
|
+
|
|
113
|
+
**Two licences, split by directory.** The engine is [MIT](LICENSE.md). Mochi
|
|
114
|
+
herself — `src/characters/` — is not: use her as she is, in anything including
|
|
115
|
+
what you sell, but don't rename her, sell her _as_ the goods, or make her your
|
|
116
|
+
brand. The full terms are short and in [LICENSE.md](LICENSE.md); the reasoning
|
|
117
|
+
is in [BRAND.md](BRAND.md).
|
|
118
|
+
|
|
119
|
+
If you want a character of your own, the engine is all you need.
|
|
120
|
+
|
|
121
|
+
## The desktop app has been retired
|
|
122
|
+
|
|
123
|
+
Mochi began as a realtime-voice AI companion that lived on your macOS desktop.
|
|
124
|
+
That app is no longer developed.
|
|
125
|
+
|
|
126
|
+
- **Downloads still work.** Every release remains available, including
|
|
127
|
+
[v0.1.20](https://github.com/xiaolai/mochi/releases/tag/v0.1.20) with signed
|
|
128
|
+
and notarized builds for Apple silicon and Intel.
|
|
129
|
+
- **The source is preserved** on the
|
|
130
|
+
[`archive/app`](https://github.com/xiaolai/mochi/tree/archive/app) branch,
|
|
131
|
+
exactly as it was at v0.1.20.
|
|
132
|
+
- **No further releases will be published here**, so installed copies will go on
|
|
133
|
+
reporting themselves up to date rather than trying to update into a package.
|
|
134
|
+
|
|
135
|
+
The character outlived the application, which is the usual way round.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mochi, drawn.
|
|
3
|
+
*
|
|
4
|
+
* This is the composition root for the rig: it owns time and state, and defers
|
|
5
|
+
* every shape to the pure modules beside it. The layer order is the one the
|
|
6
|
+
* architecture fixes -- idle, then expression, then the mouth, always last.
|
|
7
|
+
* Nothing above the mouth may write to it, because a gesture that holds her
|
|
8
|
+
* jaw shut while audio is still playing reads as broken rather than as angry.
|
|
9
|
+
*
|
|
10
|
+
* The silhouette is kept as a Path2D and used for BOTH filling and hit testing,
|
|
11
|
+
* so "only what you can see takes the mouse" is true by construction rather
|
|
12
|
+
* than by two pieces of geometry agreeing. Everything drawn is clipped to it.
|
|
13
|
+
*/
|
|
14
|
+
import { type AvatarBackend, type AvatarBackendCaps, type AvatarKind, type EmotionSignal, type MonotonicMs, type VisemeWeights } from '../core/vocabulary.js';
|
|
15
|
+
import { type FaceSpec } from '../core/spec.js';
|
|
16
|
+
/**
|
|
17
|
+
* How much deeper she breathes asleep, against the waking amplitude.
|
|
18
|
+
*
|
|
19
|
+
* ## Why sleep needs its own number at all
|
|
20
|
+
*
|
|
21
|
+
* The breath was made one-sided and quieter to stop it stretching her head into
|
|
22
|
+
* a point, and awake that is right — there is a blink, there is drift, there is
|
|
23
|
+
* a face doing things, and the breath only has to keep her from looking frozen.
|
|
24
|
+
*
|
|
25
|
+
* Asleep there is none of that. The eyes are held shut, `ASLEEP_DRIFT` keeps a
|
|
26
|
+
* fifth of the drift, and the breath is the ONLY thing left moving. At the
|
|
27
|
+
* waking amplitude that came to 2.0px of width and 1.5px of height on a 94px
|
|
28
|
+
* body across 3.4 seconds — measured, not guessed — which is not a quiet breath,
|
|
29
|
+
* it is an invisible one. She read as switched off, which is the exact reading
|
|
30
|
+
* the kept breath exists to prevent.
|
|
31
|
+
*
|
|
32
|
+
* ## Why 2, and not more
|
|
33
|
+
*
|
|
34
|
+
* Twice the waking breath puts the asleep excursion at 0.048, on a `sleepy`
|
|
35
|
+
* resting pose of 0.09, so the widest frame is 0.138. The pose plus breath
|
|
36
|
+
* already peaked at 0.135 before any of this session's changes and was fine
|
|
37
|
+
* there; past that lies the 1.24-times-her-width spread that `looks.ts` records
|
|
38
|
+
* as reading like a puddle. So this restores the movement without reopening the
|
|
39
|
+
* defect the sleeping pose was cut for.
|
|
40
|
+
*
|
|
41
|
+
* A real body breathes deeper asleep than awake, so the direction is not a
|
|
42
|
+
* concession to visibility — it is what sleep looks like.
|
|
43
|
+
*/
|
|
44
|
+
export declare const ASLEEP_BREATH_GAIN = 2;
|
|
45
|
+
export interface AvatarOptions {
|
|
46
|
+
readonly face?: FaceSpec;
|
|
47
|
+
/**
|
|
48
|
+
* Her size: a percentage of the base scale, or "fill the canvas".
|
|
49
|
+
*
|
|
50
|
+
* REQUIRED, and a union rather than an optional number, because the two
|
|
51
|
+
* cases are a real choice and defaulting silently picked the wrong one. The
|
|
52
|
+
* companion window forgot to pass a percentage and fell through to the
|
|
53
|
+
* canvas fit -- which LOOKED right, because main had sized that canvas from
|
|
54
|
+
* the layout, so the renderer was re-deriving main's answer from main's
|
|
55
|
+
* output instead of computing the same answer from the same input. They
|
|
56
|
+
* agreed to within a rounding error rather than by construction. Making this
|
|
57
|
+
* mandatory turns that omission into a compile error.
|
|
58
|
+
*
|
|
59
|
+
* `'fit-canvas'` is for the tuner, which lays out a grid of cells and cannot
|
|
60
|
+
* be told how big to be.
|
|
61
|
+
*/
|
|
62
|
+
readonly size: number | 'fit-canvas';
|
|
63
|
+
/** Injected so a test can pin the blink schedule. */
|
|
64
|
+
readonly random?: () => number;
|
|
65
|
+
}
|
|
66
|
+
export declare class DoughAvatar implements AvatarBackend {
|
|
67
|
+
private readonly ctx;
|
|
68
|
+
readonly kind: AvatarKind;
|
|
69
|
+
readonly caps: AvatarBackendCaps;
|
|
70
|
+
private face;
|
|
71
|
+
private sizePercent;
|
|
72
|
+
private readonly idleLayer;
|
|
73
|
+
private readonly squashSpring;
|
|
74
|
+
/** The clip playing, and when it began. Null start = begins on the next frame. */
|
|
75
|
+
private motion;
|
|
76
|
+
private motionStartedAt;
|
|
77
|
+
private cssWidth;
|
|
78
|
+
private cssHeight;
|
|
79
|
+
/** How far into the canvas she stands. See `setFeet`. */
|
|
80
|
+
private feetFromTop;
|
|
81
|
+
/** Eyes shut and not listening. See `setAsleep`. */
|
|
82
|
+
private asleep;
|
|
83
|
+
/** Whether a voice is coming out of her right now. See `setSpeaking`. */
|
|
84
|
+
private speaking;
|
|
85
|
+
private pixelRatio;
|
|
86
|
+
private disposed;
|
|
87
|
+
private mouthOpen;
|
|
88
|
+
private idle;
|
|
89
|
+
/**
|
|
90
|
+
* Somebody asked their system for less movement, and this is where it lands.
|
|
91
|
+
*
|
|
92
|
+
* The one flag, because there are two kinds of motion here and both are
|
|
93
|
+
* ambient: the idle layer — breath, drift, the blink schedule — and any clip
|
|
94
|
+
* that LOOPS. A looping clip is ambient by definition; it stands for a state
|
|
95
|
+
* rather than answering an event, which is what `stopMotion`'s own comment
|
|
96
|
+
* says about `sway` running from the first turn to the end of the session.
|
|
97
|
+
*
|
|
98
|
+
* One-shots are left alone deliberately. `nod`, `hop` and `turn` are replies
|
|
99
|
+
* — she was spoken to, or she was picked up — and a companion who answers
|
|
100
|
+
* nothing is a picture, not a quieter companion. The preference asks for less
|
|
101
|
+
* movement, not for no feedback.
|
|
102
|
+
*/
|
|
103
|
+
private reducedMotion;
|
|
104
|
+
private emotion;
|
|
105
|
+
private emotionExpiresAt;
|
|
106
|
+
/** A hold requested before the first frame, when there was no clock to add it to. */
|
|
107
|
+
private pendingHoldMs;
|
|
108
|
+
/** When the blink schedule was last armed against a real timestamp. */
|
|
109
|
+
private idleSeededAt;
|
|
110
|
+
private impulse;
|
|
111
|
+
private gaze;
|
|
112
|
+
private gazeTarget;
|
|
113
|
+
private lastRenderMs;
|
|
114
|
+
/** This frame's outline. Filled and hit-tested, so the two cannot disagree. */
|
|
115
|
+
private silhouette;
|
|
116
|
+
constructor(ctx: CanvasRenderingContext2D, options: AvatarOptions);
|
|
117
|
+
/**
|
|
118
|
+
* Tell the rig how big it is, in CSS pixels.
|
|
119
|
+
*
|
|
120
|
+
* Explicit rather than read from the canvas element, because the backend has
|
|
121
|
+
* no element -- it has a context. That is what lets the same class render in
|
|
122
|
+
* the browser, in the tuner, and in a test against a headless rasteriser.
|
|
123
|
+
*/
|
|
124
|
+
resize(cssWidth: number, cssHeight: number, pixelRatio?: number): void;
|
|
125
|
+
/**
|
|
126
|
+
* Resize her without rebuilding the rig.
|
|
127
|
+
*
|
|
128
|
+
* A setter because the window can be resized while she is awake and mid
|
|
129
|
+
* sentence; recreating the backend would drop the session's mouth state and
|
|
130
|
+
* restart the blink schedule.
|
|
131
|
+
*/
|
|
132
|
+
/**
|
|
133
|
+
* How far into the canvas she stands.
|
|
134
|
+
*
|
|
135
|
+
* Set by main during a drag: against the top of the display the window can
|
|
136
|
+
* rise no further, so she rises inside it instead. See `dragTo`.
|
|
137
|
+
*/
|
|
138
|
+
setFeet(feetFromTop: number): void;
|
|
139
|
+
setSizePercent(percent: number | 'fit-canvas'): void;
|
|
140
|
+
/**
|
|
141
|
+
* Recolour her, or reshape her, without rebuilding the rig.
|
|
142
|
+
*
|
|
143
|
+
* The same argument as `setSizePercent`: the window can be open and mid
|
|
144
|
+
* sentence when a theme changes, and recreating the backend would drop the
|
|
145
|
+
* session's mouth state and restart the blink schedule. The silhouette is
|
|
146
|
+
* dropped because a face carries geometry as well as colour — a themed face
|
|
147
|
+
* changes only the palette today, but nothing in this signature promises
|
|
148
|
+
* that, and a cached hit region from the previous shape is exactly the bug
|
|
149
|
+
* `setSizePercent` already had.
|
|
150
|
+
*/
|
|
151
|
+
setFace(next: FaceSpec): void;
|
|
152
|
+
setMouthOpen(value: number): void;
|
|
153
|
+
/** No-op, per caps.visemes === false. Explicitly does not fall back to driving
|
|
154
|
+
* mouthOpen from the loudest vowel: a caller that sent visemes and saw the
|
|
155
|
+
* mouth move would conclude the precise path works here. */
|
|
156
|
+
setVisemes(_weights: VisemeWeights): void;
|
|
157
|
+
setEmotion(signal: EmotionSignal): void;
|
|
158
|
+
/**
|
|
159
|
+
* Start a motion, or say nothing happened.
|
|
160
|
+
*
|
|
161
|
+
* An unknown name is reported rather than ignored: a motion that silently
|
|
162
|
+
* does nothing is indistinguishable from one that played and was too subtle
|
|
163
|
+
* to see, and the second is what somebody will assume.
|
|
164
|
+
*/
|
|
165
|
+
playMotion(name: string): void;
|
|
166
|
+
/**
|
|
167
|
+
* Honour `prefers-reduced-motion`, or stop honouring it.
|
|
168
|
+
*
|
|
169
|
+
* Separate from `setIdle`, which is the TUNER's switch: that one exists so a
|
|
170
|
+
* face can be measured against a still body, and it is called by the shelf's
|
|
171
|
+
* preview tiles. Folding the two together would mean a preview could not be
|
|
172
|
+
* told apart from an accessibility preference, and one of them wants the
|
|
173
|
+
* blink schedule re-armed on the way out while the other does not care.
|
|
174
|
+
*/
|
|
175
|
+
setReducedMotion(on: boolean): void;
|
|
176
|
+
/**
|
|
177
|
+
* Stop whatever is playing, and let the springs settle her back.
|
|
178
|
+
*
|
|
179
|
+
* The missing half of `playMotion`, and it is only missing for one-shots. A
|
|
180
|
+
* clip with `loop: true` never ends by itself — `progress` wraps it forever
|
|
181
|
+
* rather than returning null — so `sway` was started on the first turn and
|
|
182
|
+
* played for the rest of the session. Nobody reported it because a slow lean
|
|
183
|
+
* looks like idle motion, which is exactly why it is worth a method: a state
|
|
184
|
+
* whose animation outlives it is a state that has stopped meaning anything.
|
|
185
|
+
*
|
|
186
|
+
* Clearing the clip is enough. The pose layer simply stops contributing, and
|
|
187
|
+
* `spring.ts` carries her back rather than snapping — the same path a
|
|
188
|
+
* finished one-shot already takes.
|
|
189
|
+
*/
|
|
190
|
+
stopMotion(): void;
|
|
191
|
+
lookAt(nx: number, ny: number): void;
|
|
192
|
+
/**
|
|
193
|
+
* Asleep, or awake.
|
|
194
|
+
*
|
|
195
|
+
* Only the drawing. The microphone is the renderer's to close and main's to
|
|
196
|
+
* decide about — this makes her LOOK asleep, which is the half that has to be
|
|
197
|
+
* true on screen for the other half to be believable.
|
|
198
|
+
*/
|
|
199
|
+
setAsleep(on: boolean): void;
|
|
200
|
+
/**
|
|
201
|
+
* Whether sound is actually coming out of her — the analyser's answer.
|
|
202
|
+
*
|
|
203
|
+
* ## Why the rig needs to know
|
|
204
|
+
*
|
|
205
|
+
* So that she cannot talk with her eyes shut. `asleep` held `blink: 1` for the
|
|
206
|
+
* whole of the state, and there were real paths to her speaking inside it:
|
|
207
|
+
* `voice:config` handed back a greeting whenever the `speak_first` grant was
|
|
208
|
+
* on, without consulting rest, and a session is re-opened every hour (§53) —
|
|
209
|
+
* each one a NEW session, so each one greeted. Nobody saw it because it
|
|
210
|
+
* happened to an empty room.
|
|
211
|
+
*
|
|
212
|
+
* Both halves are fixed and both are needed. Main no longer asks for the
|
|
213
|
+
* greeting while she rests, which is the cause; this is the property, and it
|
|
214
|
+
* holds for any path anybody adds later. A mouth moving under closed eyes is
|
|
215
|
+
* the thing to make impossible, not the thing to remember to avoid.
|
|
216
|
+
*
|
|
217
|
+
* ## The analyser, not a frame
|
|
218
|
+
*
|
|
219
|
+
* `face.ts` passes `envelope.speaking`, which is measured from her own audio.
|
|
220
|
+
* `output_audio_buffer.started` is a promise of audio and §64 measured it
|
|
221
|
+
* arriving followed by silence, so it would open her eyes on a turn where
|
|
222
|
+
* nothing was ever said.
|
|
223
|
+
*/
|
|
224
|
+
setSpeaking(on: boolean): void;
|
|
225
|
+
setIdle(on: boolean): void;
|
|
226
|
+
/** A squash impulse — a poke, or a reaction. The spring resolves it. */
|
|
227
|
+
poke(amount?: number): void;
|
|
228
|
+
hitTest(x: number, y: number): boolean;
|
|
229
|
+
render(now: MonotonicMs): void;
|
|
230
|
+
/**
|
|
231
|
+
* Where the playing clip is now, or nothing.
|
|
232
|
+
*
|
|
233
|
+
* The start is taken on the first frame AFTER `playMotion` rather than in
|
|
234
|
+
* it: `performance.now()` at the moment of the call can be most of a frame
|
|
235
|
+
* before the frame that draws it, so the first visible frame would already
|
|
236
|
+
* be part-way in. Clearing the clip when it finishes is what stops a
|
|
237
|
+
* one-shot holding its last pose forever.
|
|
238
|
+
*/
|
|
239
|
+
private motionPose;
|
|
240
|
+
dispose(): void;
|
|
241
|
+
private applyTransform;
|
|
242
|
+
private advanceGaze;
|
|
243
|
+
/**
|
|
244
|
+
* `drift` is passed in rather than computed here, because this method has no
|
|
245
|
+
* clock — `render` owns time, and a second reading of it inside the paint
|
|
246
|
+
* would be a second answer to what instant this frame is.
|
|
247
|
+
*/
|
|
248
|
+
private paint;
|
|
249
|
+
/**
|
|
250
|
+
* Shadow first, then the lit shape on top of it, displaced.
|
|
251
|
+
*
|
|
252
|
+
* Two flat fills and no gradient at all, which is what the artwork turned out
|
|
253
|
+
* to be: its interior luminance is bimodal, 86% at 0.725 against 14% at
|
|
254
|
+
* 0.675, with nothing between the two. So the band has a hard inner edge, and
|
|
255
|
+
* the way to get a hard edge that follows the contour is to cover the shadow
|
|
256
|
+
* with a copy of the SAME outline rather than to fade one colour into
|
|
257
|
+
* another. The crescent the copy fails to cover is the band.
|
|
258
|
+
*
|
|
259
|
+
* Everything here is already clipped to the silhouette by the caller, so the
|
|
260
|
+
* displaced copy cannot spill past her outline on the up-right side.
|
|
261
|
+
*/
|
|
262
|
+
private paintBody;
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=avatar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../src/canvas2d/avatar.ts"],"names":[],"mappings":"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"}
|