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,157 @@
1
+ /**
2
+ * The mochi's silhouette, as arithmetic.
3
+ *
4
+ * Deliberately canvas-free: these functions return points, and the backend
5
+ * turns points into a Path2D. That is what makes the shape testable without a
6
+ * rasteriser, and it is also what makes hit testing exact -- the polygon that
7
+ * is filled and the polygon that is hit-tested are the same array.
8
+ *
9
+ * Local space throughout: origin at the point where she meets the surface,
10
+ * +y UP. The renderer flips. Heights read as positive numbers here, which is
11
+ * worth the one transform at the boundary.
12
+ *
13
+ * The shape is an OVOID, not a dome, and that came from measuring
14
+ * `rig/__fixtures__/mochi-icon.png` rather than from taste. The first version of this file
15
+ * was a single superellipse planted on a flat base, which put the widest point
16
+ * at the bottom -- and the icon's widest point is at 0.295 of its height, with
17
+ * a rounded underside. Those are different creatures, and the difference is
18
+ * exactly what makes one read as a dumpling and the other as a mochi.
19
+ */
20
+ // Imported rather than declared: the window has to be sized for the widest
21
+ // frame she can reach, so main needs this number too. Two copies would let the
22
+ // window and the shape disagree about how far she leans.
23
+ //
24
+ // This was a RELATIVE, extension-suffixed import for a specific reason:
25
+ // `scripts/make-icons.ts` imported this module under bare `node`, which resolves
26
+ // no path aliases -- and switching to `@shared` once broke `pnpm icons` while
27
+ // every other gate stayed green, because nothing in `pnpm verify` ran the
28
+ // generator.
29
+ //
30
+ // That generator went to the archive with the v1 rig, so the constraint is gone
31
+ // and the alias is correct again. If an icon generator returns and runs outside
32
+ // the bundler, this line is the one that breaks first -- and it will break
33
+ // silently, so give it a gate that runs the generator rather than restoring the
34
+ // relative path and hoping.
35
+ import { SHEAR_GAIN } from './layout.js';
36
+ import { clamp01 } from './vocabulary.js';
37
+ /**
38
+ * Squash, preserving area.
39
+ *
40
+ * `halfWidth * factor` and `height / factor`: the ovoid's area is
41
+ * proportional to the product for fixed exponents, so this is exactly
42
+ * volume-preserving. That is the property that makes it read as dough being
43
+ * compressed rather than as a picture being scaled.
44
+ *
45
+ * The factor is floored well above zero. A squash of -1 would otherwise divide
46
+ * by zero and produce an infinitely tall mochi.
47
+ */
48
+ export function squashed(shape, amount) {
49
+ const safe = Number.isFinite(amount) ? Math.max(-0.9, Math.min(0.9, amount)) : 0;
50
+ const factor = 1 + safe;
51
+ return { ...shape, halfWidth: shape.halfWidth * factor, height: shape.height / factor };
52
+ }
53
+ /**
54
+ * Horizontal displacement at a given height above the base.
55
+ *
56
+ * Zero at the base and maximal at the apex, which is what distinguishes a lean
57
+ * from a translation: her contact point stays where it was.
58
+ */
59
+ export function shearAt(shape, heightAboveBase) {
60
+ if (shape.height <= 0)
61
+ return 0;
62
+ const t = Math.max(0, Math.min(1, heightAboveBase / shape.height));
63
+ return shape.lean * shape.halfWidth * SHEAR_GAIN * t;
64
+ }
65
+ /** Superellipse half-width at normalised distance `t` from the waist, 0..1. */
66
+ function profile(t, exponent) {
67
+ const n = Math.max(0.2, exponent);
68
+ return Math.pow(Math.max(0, 1 - Math.pow(Math.min(1, Math.max(0, t)), n)), 1 / n);
69
+ }
70
+ /**
71
+ * Half-width at a height, as a fraction of halfWidth.
72
+ *
73
+ * Exported because it is the cheapest way to compare this shape against the
74
+ * measured icon: the test walks heights and checks the profile, which pins the
75
+ * silhouette to the artwork rather than to whatever looked right on the day.
76
+ */
77
+ export function widthAt(shape, heightFraction) {
78
+ const t = Math.min(1, Math.max(0, heightFraction));
79
+ const waist = Math.min(0.95, Math.max(0, shape.waist));
80
+ if (t >= waist) {
81
+ // No `waist >= 1` guard: the clamp above caps it at 0.95, so the divisor
82
+ // is at least 0.05 by construction. The guard implied a division by zero
83
+ // that this function has already made impossible.
84
+ return profile((t - waist) / (1 - waist), shape.upperShoulder);
85
+ }
86
+ const below = waist <= 0 ? 1 : (waist - t) / waist;
87
+ return profile(below, shape.lowerShoulder);
88
+ }
89
+ /**
90
+ * The closed outline, from the contact point up the right side and back down
91
+ * the left.
92
+ *
93
+ * The base sits exactly on y = 0 and the body occupies y > 0, so "resting on a
94
+ * surface" is a property of the coordinates rather than something the caller
95
+ * has to arrange. Squash therefore grows upward, which is how something resting
96
+ * on a table actually deforms.
97
+ */
98
+ export function domeOutline(shape, steps = 96) {
99
+ const a = Math.max(1e-6, shape.halfWidth);
100
+ const b = Math.max(1e-6, shape.height);
101
+ const waist = Math.min(0.95, Math.max(0, shape.waist));
102
+ // Delegated rather than restated. This was a second copy of the lean formula
103
+ // with the same three factors in the same order, and two copies of an
104
+ // expression are two things that can be tuned apart. The guarded dimensions
105
+ // are what differed, so they go into the shape instead of into the formula.
106
+ const guarded = { ...shape, halfWidth: a, height: b };
107
+ const shear = (y) => shearAt(guarded, y);
108
+ // The right-hand profile, bottom to top. Sampled separately above and below
109
+ // the waist so neither half is starved of points when the waist sits low --
110
+ // and it does sit low, at 0.295.
111
+ const right = [];
112
+ const lowerSteps = Math.max(8, Math.round(steps * waist));
113
+ const upperSteps = Math.max(8, steps - lowerSteps);
114
+ for (let i = 0; i <= lowerSteps; i++) {
115
+ const t = waist * (i / lowerSteps);
116
+ const y = t * b;
117
+ right.push({ x: a * widthAt(shape, t) + shear(y), y });
118
+ }
119
+ for (let i = 1; i <= upperSteps; i++) {
120
+ const t = waist + (1 - waist) * (i / upperSteps);
121
+ const y = t * b;
122
+ right.push({ x: a * widthAt(shape, t) + shear(y), y });
123
+ }
124
+ // Mirror back down the left. Both endpoints are dropped: they sit on the
125
+ // axis, so keeping them would duplicate the apex and the contact point and
126
+ // leave a zero-length segment at each -- which some rasterisers render as a
127
+ // stray join.
128
+ const left = right
129
+ .slice(1, -1)
130
+ .reverse()
131
+ .map((point) => ({ x: -(point.x - shear(point.y)) + shear(point.y), y: point.y }));
132
+ return [...right, ...left];
133
+ }
134
+ /**
135
+ * Where a feature sits, in body-local space.
136
+ *
137
+ * `grip` is the point of this function. At 1 the face deforms exactly as much
138
+ * as the body and looks printed on; at 0 it floats, unaffected. Real features
139
+ * on something soft resist deformation without escaping it, so the placement
140
+ * interpolates between the squashed and unsquashed frames. That single number
141
+ * is most of what makes it read as a face ON something rather than a decal.
142
+ *
143
+ * The lean is measured in that SAME interpolated frame. It used to be taken
144
+ * from the squashed one unconditionally, which quietly broke the contract: a
145
+ * zero-grip feature was placed at its undeformed width and then displaced by a
146
+ * shear computed from the wider, shorter squashed body -- so it moved
147
+ * horizontally during a squash it was supposed to be ignoring. At grip 1 the
148
+ * interpolated frame IS the squashed frame, so nothing about her changes.
149
+ */
150
+ export function placeFeature(base, squashedShape, normalisedX, normalisedY, gripX, gripY) {
151
+ const width = base.halfWidth + (squashedShape.halfWidth - base.halfWidth) * clamp01(gripX);
152
+ const height = base.height + (squashedShape.height - base.height) * clamp01(gripY);
153
+ const y = normalisedY * height;
154
+ const frame = { ...squashedShape, halfWidth: width, height };
155
+ return { x: normalisedX * width + shearAt(frame, y), y };
156
+ }
157
+ //# sourceMappingURL=geometry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.js","sourceRoot":"","sources":["../../src/core/geometry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,2EAA2E;AAC3E,+EAA+E;AAC/E,yDAAyD;AACzD,EAAE;AACF,wEAAwE;AACxE,iFAAiF;AACjF,8EAA8E;AAC9E,0EAA0E;AAC1E,aAAa;AACb,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,2EAA2E;AAC3E,gFAAgF;AAChF,4BAA4B;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAoCzC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAgB,EAAE,MAAc;IACvD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAChF,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,GAAG,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,EAAE,CAAA;AACzF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,OAAO,CAAC,KAAgB,EAAE,eAAuB;IAC/D,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,CAAC,CAAA;IAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,eAAe,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAA;IAClE,OAAO,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,SAAS,GAAG,UAAU,GAAG,CAAC,CAAA;AACtD,CAAC;AAED,+EAA+E;AAC/E,SAAS,OAAO,CAAC,CAAS,EAAE,QAAgB;IAC1C,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IACjC,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;AACnF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,KAAgB,EAAE,cAAsB;IAC9D,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,CAAA;IAClD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAA;IACtD,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC;QACf,yEAAyE;QACzE,yEAAyE;QACzE,kDAAkD;QAClD,OAAO,OAAO,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,CAAA;IAChE,CAAC;IACD,MAAM,KAAK,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,KAAK,CAAA;IAClD,OAAO,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAA;AAC5C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,KAAgB,EAAE,KAAK,GAAG,EAAE;IACtD,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,CAAA;IACzC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IACtC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAA;IACtD,6EAA6E;IAC7E,sEAAsE;IACtE,4EAA4E;IAC5E,4EAA4E;IAC5E,MAAM,OAAO,GAAc,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAA;IAChE,MAAM,KAAK,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;IAExD,4EAA4E;IAC5E,4EAA4E;IAC5E,iCAAiC;IACjC,MAAM,KAAK,GAAY,EAAE,CAAA;IACzB,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAA;IACzD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,UAAU,CAAC,CAAA;IAElD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,GAAG,UAAU,CAAC,CAAA;QAClC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACf,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;IACxD,CAAC;IACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,UAAU,CAAC,CAAA;QAChD,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACf,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;IACxD,CAAC;IAED,yEAAyE;IACzE,2EAA2E;IAC3E,4EAA4E;IAC5E,cAAc;IACd,MAAM,IAAI,GAAG,KAAK;SACf,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;SACZ,OAAO,EAAE;SACT,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAEpF,OAAO,CAAC,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC,CAAA;AAC5B,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAe,EACf,aAAwB,EACxB,WAAmB,EACnB,WAAmB,EACnB,KAAa,EACb,KAAa;IAEb,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,GAAG,CAAC,aAAa,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;IAC1F,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,aAAa,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;IAClF,MAAM,CAAC,GAAG,WAAW,GAAG,MAAM,CAAA;IAC9B,MAAM,KAAK,GAAc,EAAE,GAAG,aAAa,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,CAAA;IACvE,OAAO,EAAE,CAAC,EAAE,WAAW,GAAG,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;AAC1D,CAAC"}
@@ -0,0 +1,163 @@
1
+ /**
2
+ * The idle layer: breathing, blinking, and the small motion that separates
3
+ * "alive" from "a picture that sometimes moves".
4
+ *
5
+ * Pure functions of time. The backend asks what the idle layer wants at a
6
+ * given instant and gets an answer; nothing here holds a frame loop, a clock,
7
+ * or a random number generator it did not receive. That is what makes a blink
8
+ * schedule testable at all -- the alternative is a test that waits.
9
+ *
10
+ * Ported unchanged from mochi. The asymmetric blink and the Poisson gap are
11
+ * both experiment, not preference, and re-deriving them would mean re-watching
12
+ * a metronome blink for thirty seconds to rediscover why it looks wrong.
13
+ */
14
+ import type { MonotonicMs } from './vocabulary.js';
15
+ export declare const BREATH_PERIOD_MS = 3400;
16
+ /**
17
+ * Blink gap bounds.
18
+ *
19
+ * Drawn from an exponential distribution rather than picked uniformly: real
20
+ * blinks are a Poisson process, and a uniform gap reads as a metronome within
21
+ * about thirty seconds of watching. The bounds clamp the tail -- an exponential
22
+ * will happily produce a four-second gap or a ninety-second one, and both look
23
+ * wrong.
24
+ */
25
+ export declare const BLINK_MIN_GAP_MS = 2200;
26
+ export declare const BLINK_MAX_GAP_MS = 6200;
27
+ /** How long the lid takes to close and open again. */
28
+ export declare const BLINK_DURATION_MS = 130;
29
+ export interface IdlePose {
30
+ /** 0..1, where 1 is fully shut. */
31
+ readonly blink: number;
32
+ /**
33
+ * 0..1, where 0 is resting and 1 is fully inhaled. Never negative.
34
+ *
35
+ * ONE-SIDED, and the sign is the whole of it -- see `breathAt`. The resting
36
+ * silhouette is a floor she returns to rather than a midpoint she oscillates
37
+ * about.
38
+ */
39
+ readonly breath: number;
40
+ }
41
+ /**
42
+ * How far the drift may take her, in fractions of her body.
43
+ *
44
+ * ## Small on purpose, and this is the whole point of it
45
+ *
46
+ * A companion who is alive is not a companion who goes anywhere. These are a
47
+ * few pixels at her drawn size -- `shift` is about 2.6px on a 94px body -- and
48
+ * that is the intended reading: somebody standing still, shifting their weight,
49
+ * rather than somebody pacing.
50
+ *
51
+ * The clips are the loud half of the vocabulary and they were tuned louder
52
+ * still after a first complaint that read as "too small". It was not: the
53
+ * motions were coarse because they were the ONLY thing moving, so she was a
54
+ * still image punctuated by gestures. Continuous micro-motion is what a still
55
+ * image is missing, and once it exists the gestures can be quiet again.
56
+ */
57
+ export declare const DRIFT: {
58
+ readonly lean: 0.02;
59
+ readonly shift: 0.026;
60
+ readonly lift: 0.009;
61
+ };
62
+ /** Where in her small orbit she is, at `now`. */
63
+ export interface Drift {
64
+ readonly lean: number;
65
+ readonly shift: number;
66
+ readonly lift: number;
67
+ }
68
+ /**
69
+ * The continuous, going-nowhere motion of somebody who is simply present.
70
+ *
71
+ * `scale` is how much of it applies -- 1 awake, less asleep. Sleep keeps a
72
+ * trace rather than none for the reason the breath is kept: a companion who
73
+ * stops moving altogether reads as a crash, which is the one thing the resting
74
+ * state must not look like.
75
+ */
76
+ export declare function driftAt(now: MonotonicMs, scale?: number): Drift;
77
+ /**
78
+ * The breath, as a fraction of a full inhale. 0 at rest, 1 fully inhaled.
79
+ *
80
+ * Raised cosines rather than ramps: the turnaround at each end is gradual, and
81
+ * a linear ramp gives the body a visible tick at the top. One cycle per period,
82
+ * starting and ending at rest, peaking at `BREATH_IN_FRACTION` of the way
83
+ * through.
84
+ *
85
+ * TWO half-cosines rather than one whole one, because the halves are different
86
+ * lengths -- see `BREATH_IN_FRACTION`. Each meets the other with zero slope, at
87
+ * the peak and at rest alike, so neither join is visible; at a fraction of 0.5
88
+ * the pair collapses exactly to the single cosine this replaced.
89
+ *
90
+ * ## ONE-SIDED, and that is the point of it
91
+ *
92
+ * This was a plain sine over -1..1, and the negative half was the defect. The
93
+ * breath is summed into `squash` (see `mochi.ts`), which is an area-preserving
94
+ * trade: positive spreads her, negative stretches her taller and NARROWER. So
95
+ * for half of every cycle she wore a fraction of the same deformation `sad`
96
+ * (-0.11) and `surprised` (-0.16) are made of -- measured at 42% of `sad`'s
97
+ * crown-sharpening at the extreme, on the same axis.
98
+ *
99
+ * That reads as her head going pointy on a 3.4-second loop, and it corresponds
100
+ * to nothing a body does: inhaling expands, exhaling returns to rest. Nothing
101
+ * makes you taller and thinner than resting.
102
+ *
103
+ * The crown is where it showed first because it sharpens FASTER than the body
104
+ * lengthens -- width a fixed distance below the apex scales as
105
+ * `f^(1 + 1/upperShoulder)`, about `f^1.54` at the tuned 1.86, against `f^-1`
106
+ * for the height. Halving the amplitude alone would therefore not have fixed
107
+ * it, only slowed it down; removing the negative half does, by construction.
108
+ *
109
+ * The consequence worth stating: the silhouette measured against
110
+ * `rig/__fixtures__/mochi-icon.png` is now a FLOOR she returns to once per
111
+ * cycle, rather than a midpoint she is above half the time.
112
+ */
113
+ export declare function breathAt(now: MonotonicMs, periodMs?: number): number;
114
+ /**
115
+ * Lid closure during a blink that began at `startedAt`.
116
+ *
117
+ * Returns 0 outside the blink, so a caller can ask unconditionally. The
118
+ * envelope is continuous at every boundary -- at elapsed 0 the rising ramp is
119
+ * already 0, at `closeMs` both branches give 1, at `durationMs` the falling
120
+ * ramp has reached 0 -- so a blink cannot clip at its edges.
121
+ */
122
+ export declare function blinkAt(now: MonotonicMs, startedAt: MonotonicMs, durationMs?: number): number;
123
+ /**
124
+ * The gap until the next blink.
125
+ *
126
+ * `random` is injected rather than taken from Math.random so a test can pin the
127
+ * schedule. An exponential mapped through the clamp: most gaps land in the
128
+ * first half of the range, which is what makes the rhythm feel unplanned.
129
+ */
130
+ export declare function nextBlinkGap(random: () => number): number;
131
+ /**
132
+ * The idle layer, holding only the blink schedule.
133
+ *
134
+ * Deliberately not a frame loop: it is asked what it wants and answers. The
135
+ * backend owns time, which is the only way the same code can run in a render
136
+ * loop and in a test that advances a number.
137
+ */
138
+ export declare class IdleLayer {
139
+ private readonly random;
140
+ private nextBlinkAt;
141
+ private blinkStartedAt;
142
+ constructor(startedAt: MonotonicMs, random?: () => number);
143
+ /**
144
+ * `periodMs` is a PARAMETER rather than a constant read from this module.
145
+ *
146
+ * `FaceSpec.breathMs` is a validated, bounded, tuner-exposed field, and for a
147
+ * while nothing read it: this method called `breathAt(now)` and took the
148
+ * module default. The two happened to agree at 3400, so every gate stayed
149
+ * green and the only symptom was a slider that did nothing -- which presents
150
+ * to a designer as "I set this and the app ignored it", the same failure the
151
+ * bounds table exists to prevent.
152
+ */
153
+ pose(now: MonotonicMs, periodMs?: number): IdlePose;
154
+ /**
155
+ * Abandon any blink in progress and re-arm.
156
+ *
157
+ * Called when idle is switched off and on again. Without it a blink frozen
158
+ * half-closed at the moment idle stopped would resume from the middle, which
159
+ * looks like a twitch.
160
+ */
161
+ reset(now: MonotonicMs): void;
162
+ }
163
+ //# sourceMappingURL=idle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idle.d.ts","sourceRoot":"","sources":["../../src/core/idle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAElD,eAAO,MAAM,gBAAgB,OAAO,CAAA;AAEpC;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAA;AACpC,eAAO,MAAM,gBAAgB,OAAO,CAAA;AAEpC,sDAAsD;AACtD,eAAO,MAAM,iBAAiB,MAAM,CAAA;AA0BpC,MAAM,WAAW,QAAQ;IACvB,mCAAmC;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,KAAK;aAAK,IAAI,EAAE,IAAI;aAAE,KAAK,EAAE,KAAK;aAAE,IAAI,EAAE,KAAK;CAAW,CAAA;AAuBvE,iDAAiD;AACjD,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAaD;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,SAAI,GAAG,KAAK,CAU1D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,WAAW,EAAE,QAAQ,GAAE,MAAyB,GAAG,MAAM,CAYtF;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CACrB,GAAG,EAAE,WAAW,EAChB,SAAS,EAAE,WAAW,EACtB,UAAU,GAAE,MAA0B,GACrC,MAAM,CAQR;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,MAAM,GAAG,MAAM,CAMzD;AAED;;;;;;GAMG;AACH,qBAAa,SAAS;IAMlB,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,cAAc,CAA2B;IAEjD,YACE,SAAS,EAAE,WAAW,EACL,MAAM,GAAE,MAAM,MAAoB,EAMpD;IAED;;;;;;;;;OASG;IACH,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,QAAQ,GAAE,MAAyB,GAAG,QAAQ,CAsBpE;IAED;;;;;;OAMG;IACH,KAAK,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAG5B;CACF"}
@@ -0,0 +1,262 @@
1
+ /**
2
+ * The idle layer: breathing, blinking, and the small motion that separates
3
+ * "alive" from "a picture that sometimes moves".
4
+ *
5
+ * Pure functions of time. The backend asks what the idle layer wants at a
6
+ * given instant and gets an answer; nothing here holds a frame loop, a clock,
7
+ * or a random number generator it did not receive. That is what makes a blink
8
+ * schedule testable at all -- the alternative is a test that waits.
9
+ *
10
+ * Ported unchanged from mochi. The asymmetric blink and the Poisson gap are
11
+ * both experiment, not preference, and re-deriving them would mean re-watching
12
+ * a metronome blink for thirty seconds to rediscover why it looks wrong.
13
+ */
14
+ export const BREATH_PERIOD_MS = 3400;
15
+ /**
16
+ * Blink gap bounds.
17
+ *
18
+ * Drawn from an exponential distribution rather than picked uniformly: real
19
+ * blinks are a Poisson process, and a uniform gap reads as a metronome within
20
+ * about thirty seconds of watching. The bounds clamp the tail -- an exponential
21
+ * will happily produce a four-second gap or a ninety-second one, and both look
22
+ * wrong.
23
+ */
24
+ export const BLINK_MIN_GAP_MS = 2200;
25
+ export const BLINK_MAX_GAP_MS = 6200;
26
+ /** How long the lid takes to close and open again. */
27
+ export const BLINK_DURATION_MS = 130;
28
+ /**
29
+ * Fraction of the blink spent closing, the rest opening.
30
+ *
31
+ * Not symmetric: a real eyelid shuts faster than it opens, and matching that
32
+ * is most of what makes a blink read as one rather than as a flicker.
33
+ */
34
+ const BLINK_CLOSE_FRACTION = 0.35;
35
+ /**
36
+ * Fraction of a breath spent inhaling, the rest exhaling.
37
+ *
38
+ * Not symmetric, for the reason `BLINK_CLOSE_FRACTION` is not, and the
39
+ * mechanism is the same shape: inspiration is muscular and expiration is
40
+ * elastic recoil, so a resting breath goes in faster than it comes out. Around
41
+ * 1:1.5 at rest, which is this number.
42
+ *
43
+ * The gentle end of the plausible range, deliberately. A blink is over in 130ms
44
+ * and has to be sharp to read at all; a breath runs for 3.4 SECONDS, so the
45
+ * asymmetry has all the time it needs, and pushing it further makes the inhale
46
+ * read as a gasp -- an event, which is exactly what an idle layer must not
47
+ * produce.
48
+ */
49
+ const BREATH_IN_FRACTION = 0.4;
50
+ /**
51
+ * How far the drift may take her, in fractions of her body.
52
+ *
53
+ * ## Small on purpose, and this is the whole point of it
54
+ *
55
+ * A companion who is alive is not a companion who goes anywhere. These are a
56
+ * few pixels at her drawn size -- `shift` is about 2.6px on a 94px body -- and
57
+ * that is the intended reading: somebody standing still, shifting their weight,
58
+ * rather than somebody pacing.
59
+ *
60
+ * The clips are the loud half of the vocabulary and they were tuned louder
61
+ * still after a first complaint that read as "too small". It was not: the
62
+ * motions were coarse because they were the ONLY thing moving, so she was a
63
+ * still image punctuated by gestures. Continuous micro-motion is what a still
64
+ * image is missing, and once it exists the gestures can be quiet again.
65
+ */
66
+ export const DRIFT = { lean: 0.02, shift: 0.026, lift: 0.009 };
67
+ /**
68
+ * Periods that do not divide each other, in milliseconds.
69
+ *
70
+ * Three sines summed per channel, at spans chosen to be mutually incommensurate
71
+ * -- their least common multiple is hours, so the pattern never visibly
72
+ * repeats. One sine per channel would be a metronome, which is the same defect
73
+ * `BLINK_MIN_GAP_MS` exists to avoid and reads the same way: after about half a
74
+ * minute the eye locks onto the period and she stops looking alive.
75
+ *
76
+ * Deliberately NOT random. A random walk needs state, cannot be asked "where
77
+ * are you at time t", and makes a frame a function of every frame before it --
78
+ * so a throttled window comes back somewhere else entirely. Everything in this
79
+ * file is a pure function of the clock, and that is what makes it testable
80
+ * without waiting.
81
+ */
82
+ const DRIFT_MS = {
83
+ lean: [11_300, 6_700, 19_900],
84
+ shift: [13_700, 8_300, 23_100],
85
+ lift: [5_900, 9_700, 17_300],
86
+ };
87
+ function wobble(now, periods) {
88
+ // Weighted so the longest span carries the movement and the shortest only
89
+ // roughens it. Equal weights read as a tremor rather than as a body.
90
+ const weights = [0.6, 0.28, 0.12];
91
+ let sum = 0;
92
+ for (const [index, period] of periods.entries()) {
93
+ sum += (weights[index] ?? 0) * Math.sin((2 * Math.PI * now) / period);
94
+ }
95
+ return sum;
96
+ }
97
+ /**
98
+ * The continuous, going-nowhere motion of somebody who is simply present.
99
+ *
100
+ * `scale` is how much of it applies -- 1 awake, less asleep. Sleep keeps a
101
+ * trace rather than none for the reason the breath is kept: a companion who
102
+ * stops moving altogether reads as a crash, which is the one thing the resting
103
+ * state must not look like.
104
+ */
105
+ export function driftAt(now, scale = 1) {
106
+ if (!Number.isFinite(now) || !Number.isFinite(scale))
107
+ return { lean: 0, shift: 0, lift: 0 };
108
+ const k = Math.max(0, Math.min(1, scale));
109
+ return {
110
+ lean: DRIFT.lean * k * wobble(now, DRIFT_MS.lean),
111
+ shift: DRIFT.shift * k * wobble(now, DRIFT_MS.shift),
112
+ // Never below the ground: a bob that dipped would put her feet through the
113
+ // surface she is standing on. Half above the line, oscillating about it.
114
+ lift: Math.max(0, DRIFT.lift * k * (0.5 + 0.5 * wobble(now, DRIFT_MS.lift))),
115
+ };
116
+ }
117
+ /**
118
+ * The breath, as a fraction of a full inhale. 0 at rest, 1 fully inhaled.
119
+ *
120
+ * Raised cosines rather than ramps: the turnaround at each end is gradual, and
121
+ * a linear ramp gives the body a visible tick at the top. One cycle per period,
122
+ * starting and ending at rest, peaking at `BREATH_IN_FRACTION` of the way
123
+ * through.
124
+ *
125
+ * TWO half-cosines rather than one whole one, because the halves are different
126
+ * lengths -- see `BREATH_IN_FRACTION`. Each meets the other with zero slope, at
127
+ * the peak and at rest alike, so neither join is visible; at a fraction of 0.5
128
+ * the pair collapses exactly to the single cosine this replaced.
129
+ *
130
+ * ## ONE-SIDED, and that is the point of it
131
+ *
132
+ * This was a plain sine over -1..1, and the negative half was the defect. The
133
+ * breath is summed into `squash` (see `mochi.ts`), which is an area-preserving
134
+ * trade: positive spreads her, negative stretches her taller and NARROWER. So
135
+ * for half of every cycle she wore a fraction of the same deformation `sad`
136
+ * (-0.11) and `surprised` (-0.16) are made of -- measured at 42% of `sad`'s
137
+ * crown-sharpening at the extreme, on the same axis.
138
+ *
139
+ * That reads as her head going pointy on a 3.4-second loop, and it corresponds
140
+ * to nothing a body does: inhaling expands, exhaling returns to rest. Nothing
141
+ * makes you taller and thinner than resting.
142
+ *
143
+ * The crown is where it showed first because it sharpens FASTER than the body
144
+ * lengthens -- width a fixed distance below the apex scales as
145
+ * `f^(1 + 1/upperShoulder)`, about `f^1.54` at the tuned 1.86, against `f^-1`
146
+ * for the height. Halving the amplitude alone would therefore not have fixed
147
+ * it, only slowed it down; removing the negative half does, by construction.
148
+ *
149
+ * The consequence worth stating: the silhouette measured against
150
+ * `rig/__fixtures__/mochi-icon.png` is now a FLOOR she returns to once per
151
+ * cycle, rather than a midpoint she is above half the time.
152
+ */
153
+ export function breathAt(now, periodMs = BREATH_PERIOD_MS) {
154
+ if (!Number.isFinite(now) || !(periodMs > 0))
155
+ return 0;
156
+ // Wrapped into 0..1 rather than fed straight to a trig function, because the
157
+ // curve is piecewise now and the branch is chosen by phase. `%` keeps the
158
+ // sign of its left operand in JS, so the second modulo is what stops a
159
+ // negative timestamp landing in the wrong half.
160
+ const phase = (((now % periodMs) + periodMs) % periodMs) / periodMs;
161
+ const rising = phase < BREATH_IN_FRACTION;
162
+ const t = rising
163
+ ? phase / BREATH_IN_FRACTION
164
+ : (phase - BREATH_IN_FRACTION) / (1 - BREATH_IN_FRACTION);
165
+ return rising ? (1 - Math.cos(Math.PI * t)) / 2 : (1 + Math.cos(Math.PI * t)) / 2;
166
+ }
167
+ /**
168
+ * Lid closure during a blink that began at `startedAt`.
169
+ *
170
+ * Returns 0 outside the blink, so a caller can ask unconditionally. The
171
+ * envelope is continuous at every boundary -- at elapsed 0 the rising ramp is
172
+ * already 0, at `closeMs` both branches give 1, at `durationMs` the falling
173
+ * ramp has reached 0 -- so a blink cannot clip at its edges.
174
+ */
175
+ export function blinkAt(now, startedAt, durationMs = BLINK_DURATION_MS) {
176
+ if (!Number.isFinite(now) || !Number.isFinite(startedAt) || !(durationMs > 0))
177
+ return 0;
178
+ const elapsed = now - startedAt;
179
+ if (elapsed < 0 || elapsed > durationMs)
180
+ return 0;
181
+ const closeMs = durationMs * BLINK_CLOSE_FRACTION;
182
+ if (elapsed <= closeMs)
183
+ return elapsed / closeMs;
184
+ return 1 - (elapsed - closeMs) / (durationMs - closeMs);
185
+ }
186
+ /**
187
+ * The gap until the next blink.
188
+ *
189
+ * `random` is injected rather than taken from Math.random so a test can pin the
190
+ * schedule. An exponential mapped through the clamp: most gaps land in the
191
+ * first half of the range, which is what makes the rhythm feel unplanned.
192
+ */
193
+ export function nextBlinkGap(random) {
194
+ const uniform = random();
195
+ const bounded = Number.isFinite(uniform) ? Math.min(1, Math.max(0, uniform)) : 0.5;
196
+ const exponential = -Math.log(1 - bounded * 0.99);
197
+ const span = BLINK_MAX_GAP_MS - BLINK_MIN_GAP_MS;
198
+ return BLINK_MIN_GAP_MS + Math.min(span, (exponential / 4.6) * span);
199
+ }
200
+ /**
201
+ * The idle layer, holding only the blink schedule.
202
+ *
203
+ * Deliberately not a frame loop: it is asked what it wants and answers. The
204
+ * backend owns time, which is the only way the same code can run in a render
205
+ * loop and in a test that advances a number.
206
+ */
207
+ export class IdleLayer {
208
+ random;
209
+ nextBlinkAt;
210
+ blinkStartedAt = null;
211
+ constructor(startedAt, random = Math.random) {
212
+ this.random = random;
213
+ // Scheduled from the caller's clock, not from zero: seeding from 0 would
214
+ // put the first blink in the past whenever startup takes longer than the
215
+ // minimum gap, and she would blink the instant she appeared.
216
+ this.nextBlinkAt = startedAt + nextBlinkGap(random);
217
+ }
218
+ /**
219
+ * `periodMs` is a PARAMETER rather than a constant read from this module.
220
+ *
221
+ * `FaceSpec.breathMs` is a validated, bounded, tuner-exposed field, and for a
222
+ * while nothing read it: this method called `breathAt(now)` and took the
223
+ * module default. The two happened to agree at 3400, so every gate stayed
224
+ * green and the only symptom was a slider that did nothing -- which presents
225
+ * to a designer as "I set this and the app ignored it", the same failure the
226
+ * bounds table exists to prevent.
227
+ */
228
+ pose(now, periodMs = BREATH_PERIOD_MS) {
229
+ if (!Number.isFinite(now))
230
+ return { blink: 0, breath: 0 };
231
+ if (this.blinkStartedAt === null && now >= this.nextBlinkAt) {
232
+ // From the SCHEDULED time, not from now. With sparse frames -- a
233
+ // backgrounded window renders at 1fps -- starting from `now` slides every
234
+ // blink later by however long the gap between frames was, and the rhythm
235
+ // drifts. Starting from the schedule means a blink whose whole duration
236
+ // fell between two frames is simply not seen, which is correct: you
237
+ // cannot render what you had no frames for.
238
+ this.blinkStartedAt = this.nextBlinkAt;
239
+ }
240
+ let blink = 0;
241
+ if (this.blinkStartedAt !== null) {
242
+ blink = blinkAt(now, this.blinkStartedAt);
243
+ if (now - this.blinkStartedAt > BLINK_DURATION_MS) {
244
+ this.blinkStartedAt = null;
245
+ this.nextBlinkAt = now + nextBlinkGap(this.random);
246
+ }
247
+ }
248
+ return { blink, breath: breathAt(now, periodMs) };
249
+ }
250
+ /**
251
+ * Abandon any blink in progress and re-arm.
252
+ *
253
+ * Called when idle is switched off and on again. Without it a blink frozen
254
+ * half-closed at the moment idle stopped would resume from the middle, which
255
+ * looks like a twitch.
256
+ */
257
+ reset(now) {
258
+ this.blinkStartedAt = null;
259
+ this.nextBlinkAt = (Number.isFinite(now) ? now : 0) + nextBlinkGap(this.random);
260
+ }
261
+ }
262
+ //# sourceMappingURL=idle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idle.js","sourceRoot":"","sources":["../../src/core/idle.ts"],"names":[],"mappings":"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"}