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.
Files changed (96) hide show
  1. package/BRAND.md +85 -0
  2. package/LICENSE.md +104 -0
  3. package/README.md +135 -0
  4. package/dist/canvas2d/avatar.d.ts +264 -0
  5. package/dist/canvas2d/avatar.d.ts.map +1 -0
  6. package/dist/canvas2d/avatar.js +811 -0
  7. package/dist/canvas2d/avatar.js.map +1 -0
  8. package/dist/canvas2d/face.d.ts +74 -0
  9. package/dist/canvas2d/face.d.ts.map +1 -0
  10. package/dist/canvas2d/face.js +298 -0
  11. package/dist/canvas2d/face.js.map +1 -0
  12. package/dist/canvas2d/paths.d.ts +13 -0
  13. package/dist/canvas2d/paths.d.ts.map +1 -0
  14. package/dist/canvas2d/paths.js +24 -0
  15. package/dist/canvas2d/paths.js.map +1 -0
  16. package/dist/characters/colourways.d.ts +78 -0
  17. package/dist/characters/colourways.d.ts.map +1 -0
  18. package/dist/characters/colourways.js +76 -0
  19. package/dist/characters/colourways.js.map +1 -0
  20. package/dist/characters/index.d.ts +8 -0
  21. package/dist/characters/index.d.ts.map +1 -0
  22. package/dist/characters/index.js +8 -0
  23. package/dist/characters/index.js.map +1 -0
  24. package/dist/characters/mochi.d.ts +19 -0
  25. package/dist/characters/mochi.d.ts.map +1 -0
  26. package/dist/characters/mochi.js +66 -0
  27. package/dist/characters/mochi.js.map +1 -0
  28. package/dist/core/colour.d.ts +25 -0
  29. package/dist/core/colour.d.ts.map +1 -0
  30. package/dist/core/colour.js +37 -0
  31. package/dist/core/colour.js.map +1 -0
  32. package/dist/core/envelope.d.ts +153 -0
  33. package/dist/core/envelope.d.ts.map +1 -0
  34. package/dist/core/envelope.js +141 -0
  35. package/dist/core/envelope.js.map +1 -0
  36. package/dist/core/geometry.d.ts +106 -0
  37. package/dist/core/geometry.d.ts.map +1 -0
  38. package/dist/core/geometry.js +157 -0
  39. package/dist/core/geometry.js.map +1 -0
  40. package/dist/core/idle.d.ts +163 -0
  41. package/dist/core/idle.d.ts.map +1 -0
  42. package/dist/core/idle.js +262 -0
  43. package/dist/core/idle.js.map +1 -0
  44. package/dist/core/layout.d.ts +142 -0
  45. package/dist/core/layout.d.ts.map +1 -0
  46. package/dist/core/layout.js +172 -0
  47. package/dist/core/layout.js.map +1 -0
  48. package/dist/core/lens.d.ts +47 -0
  49. package/dist/core/lens.d.ts.map +1 -0
  50. package/dist/core/lens.js +67 -0
  51. package/dist/core/lens.js.map +1 -0
  52. package/dist/core/looks.d.ts +77 -0
  53. package/dist/core/looks.d.ts.map +1 -0
  54. package/dist/core/looks.js +168 -0
  55. package/dist/core/looks.js.map +1 -0
  56. package/dist/core/motion.d.ts +168 -0
  57. package/dist/core/motion.d.ts.map +1 -0
  58. package/dist/core/motion.js +527 -0
  59. package/dist/core/motion.js.map +1 -0
  60. package/dist/core/mouth.d.ts +93 -0
  61. package/dist/core/mouth.d.ts.map +1 -0
  62. package/dist/core/mouth.js +92 -0
  63. package/dist/core/mouth.js.map +1 -0
  64. package/dist/core/plain.d.ts +23 -0
  65. package/dist/core/plain.d.ts.map +1 -0
  66. package/dist/core/plain.js +63 -0
  67. package/dist/core/plain.js.map +1 -0
  68. package/dist/core/spec.d.ts +156 -0
  69. package/dist/core/spec.d.ts.map +1 -0
  70. package/dist/core/spec.js +236 -0
  71. package/dist/core/spec.js.map +1 -0
  72. package/dist/core/spring.d.ts +53 -0
  73. package/dist/core/spring.d.ts.map +1 -0
  74. package/dist/core/spring.js +68 -0
  75. package/dist/core/spring.js.map +1 -0
  76. package/dist/core/vocabulary.d.ts +182 -0
  77. package/dist/core/vocabulary.d.ts.map +1 -0
  78. package/dist/core/vocabulary.js +66 -0
  79. package/dist/core/vocabulary.js.map +1 -0
  80. package/dist/element/dough-avatar.d.ts +34 -0
  81. package/dist/element/dough-avatar.d.ts.map +1 -0
  82. package/dist/element/dough-avatar.js +189 -0
  83. package/dist/element/dough-avatar.js.map +1 -0
  84. package/dist/element/index.d.ts +3 -0
  85. package/dist/element/index.d.ts.map +1 -0
  86. package/dist/element/index.js +35 -0
  87. package/dist/element/index.js.map +1 -0
  88. package/dist/index.d.ts +38 -0
  89. package/dist/index.d.ts.map +1 -0
  90. package/dist/index.js +42 -0
  91. package/dist/index.js.map +1 -0
  92. package/dist/svg/silhouette.d.ts +97 -0
  93. package/dist/svg/silhouette.d.ts.map +1 -0
  94. package/dist/svg/silhouette.js +143 -0
  95. package/dist/svg/silhouette.js.map +1 -0
  96. package/package.json +90 -0
@@ -0,0 +1,142 @@
1
+ /**
2
+ * How big she is, and how much room her deformation needs.
3
+ *
4
+ * ## The direction of the dependency
5
+ *
6
+ * The body is the fixed thing and the CANVAS follows from it. That is the
7
+ * inverse of how this started: the window was 280x280 and the rig fitted her
8
+ * into a fraction of it, which meant the real size was an emergent property of
9
+ * two constants in different files, and "make her smaller" was a change to a
10
+ * fraction rather than to a size. Now `bodyW x bodyH` design units times a
11
+ * scale IS the size, and everything else is computed from it.
12
+ *
13
+ * Host-agnostic: nothing here knows about a window, a screen, or a desktop.
14
+ * What sizes an OS window from these numbers lives in the host application.
15
+ */
16
+ import { type FaceSpec } from './spec.js';
17
+ /**
18
+ * The furthest the squash channel may travel, in either direction.
19
+ *
20
+ * Lives here rather than in the rig because it is a SIZE fact before it is an
21
+ * animation fact: the window has to be big enough for the most deformed frame
22
+ * that can ever be drawn, so whoever sizes the window needs this number. The
23
+ * rig imports it back for the clamp that makes it true.
24
+ *
25
+ * What it cost to leave unbounded: sleepy posture at the top of a breath
26
+ * widened her past the window edge, and the part that did not fit was painted
27
+ * as two flat vertical sides.
28
+ */
29
+ export declare const SQUASH_LIMIT = 0.26;
30
+ /** The most any look may lean. `looks.ts` is checked against this by a test. */
31
+ export declare const LEAN_LIMIT = 0.1;
32
+ /** How much of the half-width a full lean displaces the apex by. */
33
+ export declare const SHEAR_GAIN = 0.55;
34
+ /**
35
+ * Clearance around her worst case, in DESIGN UNITS.
36
+ *
37
+ * In her own units rather than pixels, so it scales with her: at 200% the gap
38
+ * around her looks the same as at 50%, which a pixel margin would not.
39
+ */
40
+ export declare const BREATHING_UNITS = 8;
41
+ /**
42
+ * Pixels per design unit at 100%.
43
+ *
44
+ * 0.94 puts her resting body at 94 x 73 CSS pixels, which is the chosen
45
+ * default size. Expressed as a scale rather than as the width itself because
46
+ * `bodyW` belongs to the FACE — every avatar declares its own proportions, and
47
+ * a default stated as "94 wide" would silently mean something different for a
48
+ * face that is not 100 units across.
49
+ *
50
+ * 100% is the default rather than an extreme, so the 50–200% range gives room
51
+ * in both directions. She was previously drawn at twice this, which is what
52
+ * 200% now restores.
53
+ */
54
+ export declare const BASE_UNIT_SCALE = 0.94;
55
+ /**
56
+ * How far into a canvas the character stands, by default.
57
+ *
58
+ * A DEFAULT the host is expected to replace. The number came from one
59
+ * application's window and means nothing here; it survives only as the fallback
60
+ * for a caller that never says where the ground is. Anything with its own
61
+ * layout should call `setFeet` and ignore it.
62
+ *
63
+ * Why the stance is settable at all rather than fixed: a host may not be free
64
+ * to place its window where it likes. macOS pins a window's top edge to the
65
+ * work area, so a fixed standing height meant the character could never be
66
+ * carried close to the top of the display — dragged upward, she stopped dead in
67
+ * mid-air. Letting the host move the stance instead is what fixed that, and it
68
+ * is the general case, not a macOS special case.
69
+ */
70
+ export declare const FEET_FROM_TOP = 340;
71
+ /**
72
+ * Where she stands inside a canvas of a given height.
73
+ *
74
+ * ONE rule, called by the rig that draws her and by the code that anchors the
75
+ * bubble to her. They used to compute it separately — one as a fraction of the
76
+ * canvas and one as an offset from its bottom — which agree only when the
77
+ * canvas happens to be exactly her layout's height.
78
+ */
79
+ export declare function feetY(cssHeight: number, clearance: number, feetFromTop?: number): number;
80
+ /**
81
+ * What the size setting accepts, as a percentage of `BASE_UNIT_SCALE`.
82
+ *
83
+ * DERIVED from `FACE_BOUNDS.size`, which is where a user-supplied value is
84
+ * refused. That table's own comment states the rule — "One table, read by BOTH
85
+ * the validator and the tuner's sliders. Two copies would let the editor offer
86
+ * a value the loader rejects" — and there had come to be four copies of these
87
+ * three numbers: here, in `persona-change.ts` as `SIZE_BAND`, and in the
88
+ * shelf's size control as `BAND`.
89
+ *
90
+ * The `fallback` is genuinely this module's own: `FACE_BOUNDS` says what is
91
+ * PERMITTED, and what to do with a value that is not a number at all is a
92
+ * question only `clampSizePercent` asks.
93
+ */
94
+ export declare const SIZE_PERCENT: {
95
+ readonly min: number;
96
+ readonly max: number;
97
+ readonly step: number;
98
+ readonly fallback: 100;
99
+ };
100
+ export interface AvatarLayout {
101
+ /** Pixels per design unit. */
102
+ readonly scale: number;
103
+ /** The window, and therefore the canvas, in CSS pixels. */
104
+ readonly width: number;
105
+ readonly height: number;
106
+ /** Where her base sits, as a fraction of the height. */
107
+ readonly ground: number;
108
+ /** Her resting body, in CSS pixels. What a person means by "how big is she". */
109
+ readonly bodyWidth: number;
110
+ readonly bodyHeight: number;
111
+ }
112
+ /**
113
+ * The widest and tallest she can ever be drawn, in design units.
114
+ *
115
+ * Deliberately conservative on the lean: the shear displaces the apex, where
116
+ * the body is narrowest, so adding it to the full width over-estimates. A
117
+ * window a few pixels too large is invisible; one a few pixels too small
118
+ * crops her, and that is the failure this whole calculation exists to prevent.
119
+ */
120
+ export declare function worstCaseUnits(face: FaceSpec): {
121
+ width: number;
122
+ height: number;
123
+ };
124
+ /** Clamp a stored percentage to something renderable. */
125
+ export declare function clampSizePercent(value: unknown): number;
126
+ /**
127
+ * Everything both processes need, from a face and a size.
128
+ *
129
+ * Rounded to whole pixels because a BrowserWindow cannot be 293.7 wide — and
130
+ * if main rounds while the renderer does not, her base drifts off the ground
131
+ * line by a fraction of a pixel and the two disagree about where she is.
132
+ */
133
+ export declare function layoutFor(face: FaceSpec, sizePercent: number): AvatarLayout;
134
+ /**
135
+ * The largest scale whose WORST CASE still fits a canvas somebody else sized.
136
+ *
137
+ * For the tuner, which lays out its own grid of cells and cannot be told what
138
+ * size to be. The app never uses this — it sizes the window from `layoutFor`,
139
+ * which is the whole point of the inversion.
140
+ */
141
+ export declare function fitToCanvas(face: FaceSpec, cssWidth: number, cssHeight: number): number;
142
+ //# sourceMappingURL=layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/core/layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAA;AACtD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,YAAY,OAAO,CAAA;AAEhC,gFAAgF;AAChF,eAAO,MAAM,UAAU,MAAM,CAAA;AAE7B,oEAAoE;AACpE,eAAO,MAAM,UAAU,OAAO,CAAA;AAE9B;;;;;GAKG;AACH,eAAO,MAAM,eAAe,IAAI,CAAA;AAEhC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,OAAO,CAAA;AAEnC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,aAAa,MAAM,CAAA;AAEhC;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,SAAgB,GAAG,MAAM,CAU/F;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY;;;;uBAAoC,GAAG;CAAW,CAAA;AAE3E,MAAM,WAAW,YAAY;IAC3B,8BAA8B;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,wDAAwD;IACxD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,gFAAgF;IAChF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAMhF;AAED,yDAAyD;AACzD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGvD;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,GAAG,YAAY,CAc3E;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAevF"}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * How big she is, and how much room her deformation needs.
3
+ *
4
+ * ## The direction of the dependency
5
+ *
6
+ * The body is the fixed thing and the CANVAS follows from it. That is the
7
+ * inverse of how this started: the window was 280x280 and the rig fitted her
8
+ * into a fraction of it, which meant the real size was an emergent property of
9
+ * two constants in different files, and "make her smaller" was a change to a
10
+ * fraction rather than to a size. Now `bodyW x bodyH` design units times a
11
+ * scale IS the size, and everything else is computed from it.
12
+ *
13
+ * Host-agnostic: nothing here knows about a window, a screen, or a desktop.
14
+ * What sizes an OS window from these numbers lives in the host application.
15
+ */
16
+ import { FACE_BOUNDS } from './spec.js';
17
+ /**
18
+ * The furthest the squash channel may travel, in either direction.
19
+ *
20
+ * Lives here rather than in the rig because it is a SIZE fact before it is an
21
+ * animation fact: the window has to be big enough for the most deformed frame
22
+ * that can ever be drawn, so whoever sizes the window needs this number. The
23
+ * rig imports it back for the clamp that makes it true.
24
+ *
25
+ * What it cost to leave unbounded: sleepy posture at the top of a breath
26
+ * widened her past the window edge, and the part that did not fit was painted
27
+ * as two flat vertical sides.
28
+ */
29
+ export const SQUASH_LIMIT = 0.26;
30
+ /** The most any look may lean. `looks.ts` is checked against this by a test. */
31
+ export const LEAN_LIMIT = 0.1;
32
+ /** How much of the half-width a full lean displaces the apex by. */
33
+ export const SHEAR_GAIN = 0.55;
34
+ /**
35
+ * Clearance around her worst case, in DESIGN UNITS.
36
+ *
37
+ * In her own units rather than pixels, so it scales with her: at 200% the gap
38
+ * around her looks the same as at 50%, which a pixel margin would not.
39
+ */
40
+ export const BREATHING_UNITS = 8;
41
+ /**
42
+ * Pixels per design unit at 100%.
43
+ *
44
+ * 0.94 puts her resting body at 94 x 73 CSS pixels, which is the chosen
45
+ * default size. Expressed as a scale rather than as the width itself because
46
+ * `bodyW` belongs to the FACE — every avatar declares its own proportions, and
47
+ * a default stated as "94 wide" would silently mean something different for a
48
+ * face that is not 100 units across.
49
+ *
50
+ * 100% is the default rather than an extreme, so the 50–200% range gives room
51
+ * in both directions. She was previously drawn at twice this, which is what
52
+ * 200% now restores.
53
+ */
54
+ export const BASE_UNIT_SCALE = 0.94;
55
+ /**
56
+ * How far into a canvas the character stands, by default.
57
+ *
58
+ * A DEFAULT the host is expected to replace. The number came from one
59
+ * application's window and means nothing here; it survives only as the fallback
60
+ * for a caller that never says where the ground is. Anything with its own
61
+ * layout should call `setFeet` and ignore it.
62
+ *
63
+ * Why the stance is settable at all rather than fixed: a host may not be free
64
+ * to place its window where it likes. macOS pins a window's top edge to the
65
+ * work area, so a fixed standing height meant the character could never be
66
+ * carried close to the top of the display — dragged upward, she stopped dead in
67
+ * mid-air. Letting the host move the stance instead is what fixed that, and it
68
+ * is the general case, not a macOS special case.
69
+ */
70
+ export const FEET_FROM_TOP = 340;
71
+ /**
72
+ * Where she stands inside a canvas of a given height.
73
+ *
74
+ * ONE rule, called by the rig that draws her and by the code that anchors the
75
+ * bubble to her. They used to compute it separately — one as a fraction of the
76
+ * canvas and one as an offset from its bottom — which agree only when the
77
+ * canvas happens to be exactly her layout's height.
78
+ */
79
+ export function feetY(cssHeight, clearance, feetFromTop = FEET_FROM_TOP) {
80
+ // Clamped, so a canvas smaller than the standing height still rests her on
81
+ // something rather than dropping her through the floor. The tuner sizes its
82
+ // own cells and is the caller that hits this.
83
+ //
84
+ // `clearance` is in PIXELS and therefore scaled — `BREATHING_UNITS` is in her
85
+ // design units, and passing it raw put her 4px low at 50% and 4px high at
86
+ // 150%, which is the whole distance between resting on the ground and
87
+ // hovering over it.
88
+ return Math.min(feetFromTop, cssHeight - clearance);
89
+ }
90
+ /**
91
+ * What the size setting accepts, as a percentage of `BASE_UNIT_SCALE`.
92
+ *
93
+ * DERIVED from `FACE_BOUNDS.size`, which is where a user-supplied value is
94
+ * refused. That table's own comment states the rule — "One table, read by BOTH
95
+ * the validator and the tuner's sliders. Two copies would let the editor offer
96
+ * a value the loader rejects" — and there had come to be four copies of these
97
+ * three numbers: here, in `persona-change.ts` as `SIZE_BAND`, and in the
98
+ * shelf's size control as `BAND`.
99
+ *
100
+ * The `fallback` is genuinely this module's own: `FACE_BOUNDS` says what is
101
+ * PERMITTED, and what to do with a value that is not a number at all is a
102
+ * question only `clampSizePercent` asks.
103
+ */
104
+ export const SIZE_PERCENT = { ...FACE_BOUNDS.size, fallback: 100 };
105
+ /**
106
+ * The widest and tallest she can ever be drawn, in design units.
107
+ *
108
+ * Deliberately conservative on the lean: the shear displaces the apex, where
109
+ * the body is narrowest, so adding it to the full width over-estimates. A
110
+ * window a few pixels too large is invisible; one a few pixels too small
111
+ * crops her, and that is the failure this whole calculation exists to prevent.
112
+ */
113
+ export function worstCaseUnits(face) {
114
+ const width = face.bodyW * (1 + SQUASH_LIMIT);
115
+ return {
116
+ width: width + width * LEAN_LIMIT * SHEAR_GAIN,
117
+ height: face.bodyH / (1 - SQUASH_LIMIT),
118
+ };
119
+ }
120
+ /** Clamp a stored percentage to something renderable. */
121
+ export function clampSizePercent(value) {
122
+ if (typeof value !== 'number' || !Number.isFinite(value))
123
+ return SIZE_PERCENT.fallback;
124
+ return Math.min(SIZE_PERCENT.max, Math.max(SIZE_PERCENT.min, value));
125
+ }
126
+ /**
127
+ * Everything both processes need, from a face and a size.
128
+ *
129
+ * Rounded to whole pixels because a BrowserWindow cannot be 293.7 wide — and
130
+ * if main rounds while the renderer does not, her base drifts off the ground
131
+ * line by a fraction of a pixel and the two disagree about where she is.
132
+ */
133
+ export function layoutFor(face, sizePercent) {
134
+ const scale = BASE_UNIT_SCALE * (clampSizePercent(sizePercent) / 100);
135
+ const worst = worstCaseUnits(face);
136
+ const width = Math.round((worst.width + BREATHING_UNITS * 2) * scale);
137
+ const height = Math.round((worst.height + BREATHING_UNITS * 2) * scale);
138
+ return {
139
+ scale,
140
+ width,
141
+ height,
142
+ // Her base sits one clearance up from the bottom edge.
143
+ ground: (height - BREATHING_UNITS * scale) / height,
144
+ bodyWidth: face.bodyW * scale,
145
+ bodyHeight: face.bodyH * scale,
146
+ };
147
+ }
148
+ /**
149
+ * The largest scale whose WORST CASE still fits a canvas somebody else sized.
150
+ *
151
+ * For the tuner, which lays out its own grid of cells and cannot be told what
152
+ * size to be. The app never uses this — it sizes the window from `layoutFor`,
153
+ * which is the whole point of the inversion.
154
+ */
155
+ export function fitToCanvas(face, cssWidth, cssHeight) {
156
+ // A canvas has to be a positive finite size to be fitted into. It used to
157
+ // take whatever it was given, and every bad answer flowed downstream in a
158
+ // shape nothing tested for: zero produced scale 0, which makes the renderer's
159
+ // ground calculation `0 / 0`, so she was positioned at NaN and vanished with
160
+ // no error; a negative width produced a negative scale, which mirrors her.
161
+ //
162
+ // A zero-sized canvas is not exotic -- an element that is display:none, or
163
+ // measured a frame before layout, reports exactly that.
164
+ if (!isPositive(cssWidth) || !isPositive(cssHeight))
165
+ return 0;
166
+ const worst = worstCaseUnits(face);
167
+ return Math.min(cssWidth / (worst.width + BREATHING_UNITS * 2), cssHeight / (worst.height + BREATHING_UNITS * 2));
168
+ }
169
+ function isPositive(value) {
170
+ return Number.isFinite(value) && value > 0;
171
+ }
172
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/core/layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,WAAW,EAAiB,MAAM,WAAW,CAAA;AACtD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAA;AAEhC,gFAAgF;AAChF,MAAM,CAAC,MAAM,UAAU,GAAG,GAAG,CAAA;AAE7B,oEAAoE;AACpE,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAA;AAE9B;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAA;AAEhC;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAA;AAEnC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAA;AAEhC;;;;;;;GAOG;AACH,MAAM,UAAU,KAAK,CAAC,SAAiB,EAAE,SAAiB,EAAE,WAAW,GAAG,aAAa;IACrF,2EAA2E;IAC3E,4EAA4E;IAC5E,8CAA8C;IAC9C,EAAE;IACF,8EAA8E;IAC9E,0EAA0E;IAC1E,sEAAsE;IACtE,oBAAoB;IACpB,OAAO,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,SAAS,GAAG,SAAS,CAAC,CAAA;AACrD,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,GAAG,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAW,CAAA;AAe3E;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,IAAc;IAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,YAAY,CAAC,CAAA;IAC7C,OAAO;QACL,KAAK,EAAE,KAAK,GAAG,KAAK,GAAG,UAAU,GAAG,UAAU;QAC9C,MAAM,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,YAAY,CAAC;KACxC,CAAA;AACH,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,YAAY,CAAC,QAAQ,CAAA;IACtF,OAAO,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAA;AACtE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,IAAc,EAAE,WAAmB;IAC3D,MAAM,KAAK,GAAG,eAAe,GAAG,CAAC,gBAAgB,CAAC,WAAW,CAAC,GAAG,GAAG,CAAC,CAAA;IACrE,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,CAAA;IAClC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,eAAe,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAA;IACrE,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,eAAe,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAA;IACvE,OAAO;QACL,KAAK;QACL,KAAK;QACL,MAAM;QACN,uDAAuD;QACvD,MAAM,EAAE,CAAC,MAAM,GAAG,eAAe,GAAG,KAAK,CAAC,GAAG,MAAM;QACnD,SAAS,EAAE,IAAI,CAAC,KAAK,GAAG,KAAK;QAC7B,UAAU,EAAE,IAAI,CAAC,KAAK,GAAG,KAAK;KAC/B,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,IAAc,EAAE,QAAgB,EAAE,SAAiB;IAC7E,0EAA0E;IAC1E,0EAA0E;IAC1E,8EAA8E;IAC9E,6EAA6E;IAC7E,2EAA2E;IAC3E,EAAE;IACF,2EAA2E;IAC3E,wDAAwD;IACxD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,CAAC,CAAA;IAC7D,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,CAAA;IAClC,OAAO,IAAI,CAAC,GAAG,CACb,QAAQ,GAAG,CAAC,KAAK,CAAC,KAAK,GAAG,eAAe,GAAG,CAAC,CAAC,EAC9C,SAAS,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,eAAe,GAAG,CAAC,CAAC,CACjD,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAA;AAC5C,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The lens: the region between an upper edge and a lower edge.
3
+ *
4
+ * One primitive for both the eyes and the mouth, and that unification is the
5
+ * design rather than a saving. Because both edges are independently signed, a
6
+ * single shape covers every state the face needs:
7
+ *
8
+ * upper +, lower + a dot, or an open mouth
9
+ * upper ~0, lower ~0 a closed sliver -- a blink
10
+ * upper +, lower - a crescent -- the ^ ^ happy eye
11
+ * upper -, lower + a frown
12
+ *
13
+ * A NEGATIVE lower is the trick worth naming: it curves the bottom edge upward
14
+ * past the baseline, which is what turns an eye into a smile without a second
15
+ * asset and without a special case. Lip-sync and expression therefore drive one
16
+ * code path, so a mouth cannot be open according to one and shut according to
17
+ * the other.
18
+ *
19
+ * Each edge is half a SUPERELLIPSE, not a quadratic. The first version used
20
+ * quadratics and every eye came out a pointed almond, because two quadratics
21
+ * meeting at the corners always meet at an angle -- there is no value of the
22
+ * parameters that produces a round dot. A superellipse at `roundness` 2 is an
23
+ * ellipse, so the two halves join with a shared vertical tangent and the corner
24
+ * disappears; below 2 it sharpens toward a leaf, above 2 it squares off.
25
+ *
26
+ * Local space, origin at the lens centre, +y UP -- the same convention as
27
+ * geometry.ts.
28
+ */
29
+ import type { Point } from './geometry.js';
30
+ export interface LensShape {
31
+ readonly halfWidth: number;
32
+ /** How far the top edge bulges above the baseline. Negative dips it below. */
33
+ readonly upper: number;
34
+ /** How far the bottom edge dips below the baseline. Negative lifts it above. */
35
+ readonly lower: number;
36
+ /** Radians. Mirror it per side so both inner corners move the same way. */
37
+ readonly tilt: number;
38
+ /** Superellipse exponent. 2 is an ellipse; below sharpens, above squares off. */
39
+ readonly roundness: number;
40
+ }
41
+ export declare function lensOutline(lens: LensShape, steps?: number): Point[];
42
+ /**
43
+ * Peak vertical extent, used to offset gaze so the pupil moves within the eye
44
+ * rather than by a fraction of nothing when the lid is nearly shut.
45
+ */
46
+ export declare function lensHeight(lens: LensShape): number;
47
+ //# sourceMappingURL=lens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lens.d.ts","sourceRoot":"","sources":["../../src/core/lens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAA;AAE1C,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,iFAAiF;IACjF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAMD,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,SAAK,GAAG,KAAK,EAAE,CA4BhE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAElD"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The lens: the region between an upper edge and a lower edge.
3
+ *
4
+ * One primitive for both the eyes and the mouth, and that unification is the
5
+ * design rather than a saving. Because both edges are independently signed, a
6
+ * single shape covers every state the face needs:
7
+ *
8
+ * upper +, lower + a dot, or an open mouth
9
+ * upper ~0, lower ~0 a closed sliver -- a blink
10
+ * upper +, lower - a crescent -- the ^ ^ happy eye
11
+ * upper -, lower + a frown
12
+ *
13
+ * A NEGATIVE lower is the trick worth naming: it curves the bottom edge upward
14
+ * past the baseline, which is what turns an eye into a smile without a second
15
+ * asset and without a special case. Lip-sync and expression therefore drive one
16
+ * code path, so a mouth cannot be open according to one and shut according to
17
+ * the other.
18
+ *
19
+ * Each edge is half a SUPERELLIPSE, not a quadratic. The first version used
20
+ * quadratics and every eye came out a pointed almond, because two quadratics
21
+ * meeting at the corners always meet at an angle -- there is no value of the
22
+ * parameters that produces a round dot. A superellipse at `roundness` 2 is an
23
+ * ellipse, so the two halves join with a shared vertical tangent and the corner
24
+ * disappears; below 2 it sharpens toward a leaf, above 2 it squares off.
25
+ *
26
+ * Local space, origin at the lens centre, +y UP -- the same convention as
27
+ * geometry.ts.
28
+ */
29
+ function rotate(x, y, sin, cos) {
30
+ return { x: x * cos - y * sin, y: x * sin + y * cos };
31
+ }
32
+ export function lensOutline(lens, steps = 28) {
33
+ const hw = Math.max(0, lens.halfWidth);
34
+ const k = Math.max(0.5, lens.roundness);
35
+ const power = 2 / k;
36
+ const sin = Math.sin(lens.tilt);
37
+ const cos = Math.cos(lens.tilt);
38
+ const points = [];
39
+ // Right, over the top, to left; then left, under the bottom, back to right.
40
+ // Parametrised by angle rather than by x so the samples stay evenly spread
41
+ // around the curve instead of bunching at the ends.
42
+ const half = (extent, from, to) => {
43
+ for (let i = 0; i <= steps; i++) {
44
+ const t = from + (to - from) * (i / steps);
45
+ const c = Math.cos(t);
46
+ const s = Math.sin(t);
47
+ const x = hw * Math.sign(c) * Math.pow(Math.abs(c), power);
48
+ const y = extent * Math.sign(s) * Math.pow(Math.abs(s), power);
49
+ points.push(rotate(x, y, sin, cos));
50
+ }
51
+ };
52
+ // Over the top with `upper`, then under the bottom with `lower`. On the lower
53
+ // sweep sin(t) is negative, so the formula already flips the sign -- passing
54
+ // `lower` unnegated is what makes a POSITIVE lower dip below the baseline and
55
+ // a negative one bow above it.
56
+ half(lens.upper, 0, Math.PI);
57
+ half(lens.lower, Math.PI, Math.PI * 2);
58
+ return points;
59
+ }
60
+ /**
61
+ * Peak vertical extent, used to offset gaze so the pupil moves within the eye
62
+ * rather than by a fraction of nothing when the lid is nearly shut.
63
+ */
64
+ export function lensHeight(lens) {
65
+ return Math.max(Math.abs(lens.upper), Math.abs(lens.lower));
66
+ }
67
+ //# sourceMappingURL=lens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lens.js","sourceRoot":"","sources":["../../src/core/lens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAgBH,SAAS,MAAM,CAAC,CAAS,EAAE,CAAS,EAAE,GAAW,EAAE,GAAW;IAC5D,OAAO,EAAE,CAAC,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,EAAE,CAAA;AACvD,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAe,EAAE,KAAK,GAAG,EAAE;IACrD,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IACtC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IACvC,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,CAAA;IACnB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAE/B,MAAM,MAAM,GAAY,EAAE,CAAA;IAC1B,4EAA4E;IAC5E,2EAA2E;IAC3E,oDAAoD;IACpD,MAAM,IAAI,GAAG,CAAC,MAAc,EAAE,IAAY,EAAE,EAAU,EAAQ,EAAE;QAC9D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YAChC,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAA;YAC1C,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;YACrB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;YACrB,MAAM,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;YAC1D,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;YAC9D,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;QACrC,CAAC;IACH,CAAC,CAAA;IACD,8EAA8E;IAC9E,6EAA6E;IAC7E,8EAA8E;IAC9E,+BAA+B;IAC/B,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;IAC5B,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;IACtC,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAe;IACxC,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;AAC7D,CAAC"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * How each emotion deforms the neutral face.
3
+ *
4
+ * Every Emotion gets an entry, enforced by `Record<Emotion, Look>`. Letting one
5
+ * fall through to neutral would make `caps.presetExpressions` a lie: the
6
+ * backend would claim to support an expression it renders identically to no
7
+ * expression at all, and the caller has no way to discover that.
8
+ *
9
+ * Values are multipliers on the tuned base geometry rather than absolute sizes,
10
+ * so retuning the face in the tuner does not invalidate the table.
11
+ */
12
+ import { type Emotion } from './vocabulary.js';
13
+ export interface Look {
14
+ /** Multiplies the eye's upper arc. */
15
+ readonly eyeUpper: number;
16
+ /** Multiplies the eye's lower arc. NEGATIVE turns the eye into a crescent -- see lens.ts. */
17
+ readonly eyeLower: number;
18
+ readonly eyeWidth: number;
19
+ /** Radians, mirrored per side. Positive brings the inner corners down. */
20
+ readonly eyeTilt: number;
21
+ readonly mouthUpper: number;
22
+ readonly mouthLower: number;
23
+ readonly mouthWidth: number;
24
+ /**
25
+ * 0..1 opacity of the mouth. 0 removes it entirely.
26
+ *
27
+ * Only `sleepy` uses it, and only for the resting pose: a sleeping mochi is a
28
+ * shape with two closed eyes, and a mouth left on it reads as awake-but-quiet
29
+ * rather than asleep.
30
+ *
31
+ * This does NOT weaken the layering rule, which says no layer above the
32
+ * mouth may overwrite it, because a gesture holding her jaw shut while audio
33
+ * is playing reads as broken. `paintMouth` therefore takes the LOUDER of this
34
+ * and whatever is actually driving the mouth -- so anything making a sound
35
+ * wins, and the expression can only hide a mouth that is already still.
36
+ */
37
+ readonly mouthAlpha: number;
38
+ /** 0..1 extra cheek colour, on top of the resting amount. */
39
+ readonly cheek: number;
40
+ /**
41
+ * 0..1 opacity of the catchlight -- the "her eyes lit up" block.
42
+ *
43
+ * A property of the EMOTION rather than a timer, which is what makes "her
44
+ * eyes lit up" mean something: it appears when she is delighted or startled
45
+ * and is absent the rest of the time, so seeing it carries information. A
46
+ * highlight that were simply always on would be a shading detail, and one
47
+ * that blinked on a schedule would be noise.
48
+ *
49
+ * It blends with intensity like everything else here, so the waking perk --
50
+ * `surprised` at 0.6 for 1.4 seconds, in `face.ts` -- shows a partial glint
51
+ * rather than a full one.
52
+ *
53
+ * **It does not fade.** This entry used to say it did, and it was wrong
54
+ * twice over: nothing set the perk at all, and only `squash` runs through a
55
+ * spring -- every other channel of a `Look` is applied on the frame it is
56
+ * set. A hold ENDS an expression; it does not ease it out. Anyone wanting a
57
+ * fade has to interpolate the look itself, which is a change to this whole
58
+ * pipeline rather than a value in this table.
59
+ */
60
+ readonly sparkle: number;
61
+ /** -1..1 gaze bias, added to whatever the cursor is doing. Thinking looks up and away. */
62
+ readonly gazeX: number;
63
+ readonly gazeY: number;
64
+ /** Postural squash. Surprise stretches her up; sleepy lets her spread. */
65
+ readonly squash: number;
66
+ readonly lean: number;
67
+ }
68
+ export declare const NEUTRAL: Look;
69
+ export declare const LOOKS: Readonly<Record<Emotion, Look>>;
70
+ /**
71
+ * Neutral blended toward the named emotion by intensity.
72
+ *
73
+ * Intensity 0 is exactly neutral for every emotion, which is what lets a signal
74
+ * decay smoothly rather than snapping when it expires.
75
+ */
76
+ export declare function blendLook(emotion: Emotion, intensity: number): Look;
77
+ //# sourceMappingURL=looks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"looks.d.ts","sourceRoot":"","sources":["../../src/core/looks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAEvD,MAAM,WAAW,IAAI;IACnB,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,6FAA6F;IAC7F,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,0FAA0F;IAC1F,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,eAAO,MAAM,OAAO,EAAE,IAerB,CAAA;AAED,eAAO,MAAM,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAsHjD,CAAA;AAID;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAWnE"}