@heyramzi/ui 2.11.0 → 2.11.2

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 (267) hide show
  1. package/dist/agents/helpers.js +38 -38
  2. package/dist/agents/page-state.js +49 -50
  3. package/dist/agents/vendor/color.js +146 -127
  4. package/dist/agents/vendor/draw.js +617 -508
  5. package/dist/agents/vendor/engine.js +698 -599
  6. package/dist/agents/vendor/mesh.js +653 -683
  7. package/dist/agents/vendor/plastic.js +3913 -3112
  8. package/dist/agents/vendor/presets.js +163 -164
  9. package/dist/agents/vendor/shapes.js +21 -20
  10. package/dist/agents/vendor/ticker.js +41 -42
  11. package/dist/agents/vendor/types.js +1 -0
  12. package/dist/agents/vendor/wear.js +1421 -1448
  13. package/dist/ai/confirmation/confirmation-context.svelte.js +6 -6
  14. package/dist/ai/confirmation/index.js +3 -1
  15. package/dist/ai/index.js +33 -0
  16. package/dist/ai/prompt-input/index.js +3 -1
  17. package/dist/ai/reasoning/index.js +3 -1
  18. package/dist/ai/reasoning/reasoning-context.svelte.js +6 -6
  19. package/dist/ai/suggestion/index.js +3 -1
  20. package/dist/ai/typing-dots/index.js +3 -1
  21. package/dist/animations/text-swap.js +24 -24
  22. package/dist/brand/index.js +54 -48
  23. package/dist/canvas/card/resize.js +5 -3
  24. package/dist/canvas/edges/edge-path.js +255 -242
  25. package/dist/canvas/editor/context-menu.js +39 -36
  26. package/dist/canvas/editor/dispatch.svelte.js +154 -151
  27. package/dist/canvas/editor/editor-context.js +7 -2
  28. package/dist/canvas/editor/editor-mode.js +1 -0
  29. package/dist/canvas/editor/frame.js +73 -76
  30. package/dist/canvas/editor/hand-context.js +5 -2
  31. package/dist/canvas/editor/history-core.js +163 -191
  32. package/dist/canvas/editor/keymap.js +95 -169
  33. package/dist/canvas/editor/layout-actions.js +143 -144
  34. package/dist/canvas/editor/lock.js +52 -51
  35. package/dist/canvas/editor/rebase.js +217 -240
  36. package/dist/canvas/editor/style.js +132 -137
  37. package/dist/canvas/editor/text-field-context.js +7 -2
  38. package/dist/canvas/editor/z-order.js +65 -70
  39. package/dist/canvas/headless/alignment.js +22 -16
  40. package/dist/canvas/headless/break-apart-find.js +286 -279
  41. package/dist/canvas/headless/break-apart-pieces.js +62 -58
  42. package/dist/canvas/headless/break-apart-plan.js +299 -325
  43. package/dist/canvas/headless/break-apart-style.js +75 -70
  44. package/dist/canvas/headless/break-apart.js +188 -181
  45. package/dist/canvas/headless/canvas-cleanup.js +78 -58
  46. package/dist/canvas/headless/edge-anchors.js +115 -116
  47. package/dist/canvas/headless/edge-memo.js +24 -22
  48. package/dist/canvas/headless/edge-serialization.js +17 -27
  49. package/dist/canvas/headless/figure-geometry.js +232 -264
  50. package/dist/canvas/headless/fit-viewport.js +84 -67
  51. package/dist/canvas/headless/grid.js +88 -86
  52. package/dist/canvas/headless/icon-node.js +44 -36
  53. package/dist/canvas/headless/index.js +1 -0
  54. package/dist/canvas/headless/linkify.js +17 -19
  55. package/dist/canvas/headless/pen-marks.js +101 -95
  56. package/dist/canvas/headless/sketch.js +231 -209
  57. package/dist/canvas/headless/snap-guides.js +99 -83
  58. package/dist/canvas/headless/svg-figure.js +133 -117
  59. package/dist/canvas/headless/table-layout.js +146 -107
  60. package/dist/canvas/index.js +2 -0
  61. package/dist/canvas/lod/lod-mode.svelte.js +20 -18
  62. package/dist/canvas/lod/render-all.svelte.js +36 -39
  63. package/dist/canvas/ops/arrange.js +78 -71
  64. package/dist/canvas/ops/ops.js +260 -255
  65. package/dist/canvas/pen/colors.js +37 -35
  66. package/dist/canvas/pen/eraser.js +28 -27
  67. package/dist/canvas/pen/geometry.js +120 -130
  68. package/dist/canvas/pen/index.js +1 -0
  69. package/dist/canvas/pen/ops.js +11 -11
  70. package/dist/canvas/pen/pen-session.js +150 -159
  71. package/dist/canvas/pen/plugin.js +107 -104
  72. package/dist/canvas/pen/shapes.js +73 -68
  73. package/dist/canvas/pen/stroke-path.js +32 -30
  74. package/dist/canvas/pen/types.js +21 -21
  75. package/dist/canvas/pen/ui.js +10 -8
  76. package/dist/canvas/plugin.js +46 -46
  77. package/dist/canvas/screen/glyphs.js +12 -11
  78. package/dist/canvas/screen/labels.js +22 -22
  79. package/dist/canvas/screen/parts.js +98 -111
  80. package/dist/canvas/screen/plugin.js +47 -56
  81. package/dist/canvas/screen/types.js +30 -35
  82. package/dist/canvas/screen/ui.js +7 -7
  83. package/dist/canvas/test-utils.js +14 -14
  84. package/dist/canvas/types.js +1 -0
  85. package/dist/canvas/ui/forward-wheel.js +24 -25
  86. package/dist/canvas/ui/icons.js +64 -131
  87. package/dist/canvas/ui/place-panel.js +58 -49
  88. package/dist/canvas/ui/shortcut-tip.js +14 -12
  89. package/dist/canvas/ui/tools.js +300 -385
  90. package/dist/charts/dashboard/chart.js +4 -0
  91. package/dist/charts/dashboard/data-table.js +11 -0
  92. package/dist/charts/dashboard/windows.js +8 -3
  93. package/dist/charts/edge-fade.js +10 -5
  94. package/dist/charts/hover-band.js +16 -11
  95. package/dist/charts/index.js +0 -1
  96. package/dist/charts/motion.js +11 -7
  97. package/dist/charts/primitives/chart-utils.js +12 -12
  98. package/dist/components/copy/clipboard.js +32 -32
  99. package/dist/components/copy/copied.svelte.js +31 -23
  100. package/dist/components/data-table/data-table.js +39 -39
  101. package/dist/components/docs/index.js +4 -0
  102. package/dist/components/docs/scroll-spy.svelte.js +73 -68
  103. package/dist/components/list-page/filter-section.js +4 -1
  104. package/dist/components/list-page/index.js +2 -0
  105. package/dist/components/list-page/list-table.js +7 -2
  106. package/dist/components/list-page/queue-rail.js +14 -0
  107. package/dist/components/list-page/section-host.js +4 -2
  108. package/dist/components/list-page/step.js +4 -3
  109. package/dist/components/list-page/types.js +1 -0
  110. package/dist/components/picker/trigger.js +48 -30
  111. package/dist/components/picker/types.js +1 -0
  112. package/dist/components/segmented/collapse.js +38 -27
  113. package/dist/components/segmented/index.js +8 -0
  114. package/dist/components/segmented/segmented.js +67 -42
  115. package/dist/components/star-rating-input/index.js +3 -1
  116. package/dist/components/table-view/types.js +1 -0
  117. package/dist/components/tags-input/tags.js +81 -69
  118. package/dist/components/theme/index.js +4 -0
  119. package/dist/components/toolbar/index.js +4 -0
  120. package/dist/components/toolbar/toolbar-trigger.js +14 -1
  121. package/dist/editor/anchors.js +17 -13
  122. package/dist/editor/block-id.js +52 -51
  123. package/dist/editor/block-ops.js +108 -80
  124. package/dist/editor/doc-blocks.js +155 -166
  125. package/dist/editor/extensions.js +139 -180
  126. package/dist/editor/index.js +4 -0
  127. package/dist/editor/markdown-test-helpers.js +4 -2
  128. package/dist/editor/markdown.js +420 -408
  129. package/dist/editor/serializer.js +204 -200
  130. package/dist/editor/slash-items.js +89 -101
  131. package/dist/editor/youtube-url.js +25 -20
  132. package/dist/effects/animated-beam/path-calculator.svelte.js +43 -38
  133. package/dist/effects/animated-beam/types.js +1 -0
  134. package/dist/effects/border-beam/css.js +92 -78
  135. package/dist/effects/border-beam/presets.js +1705 -1720
  136. package/dist/effects/border-beam/register-properties.js +43 -33
  137. package/dist/effects/border-beam/types.js +1 -0
  138. package/dist/effects/image-comparison/compare.js +3 -2
  139. package/dist/effects/image-reveal/types.js +1 -0
  140. package/dist/effects/magnetic/magnetic.js +58 -52
  141. package/dist/effects/reveal/observe.js +50 -40
  142. package/dist/effects/reveal/types.js +1 -0
  143. package/dist/effects/shader-gradient/color.js +36 -29
  144. package/dist/effects/shader-gradient/renderer.js +384 -295
  145. package/dist/effects/shader-gradient/shaders.js +31 -0
  146. package/dist/effects/shader-gradient/types.js +1 -0
  147. package/dist/icons/apple-symbol-exports.js +99 -124
  148. package/dist/icons/sf-symbols.js +357 -461
  149. package/dist/icons/trades.js +43 -28
  150. package/dist/inbox/index.js +1 -0
  151. package/dist/inbox/logic.js +126 -122
  152. package/dist/inbox/types.js +43 -43
  153. package/dist/lib/arrive.js +19 -12
  154. package/dist/lib/icon-types.js +1 -0
  155. package/dist/lib/initials.js +16 -10
  156. package/dist/lib/layout.js +5 -0
  157. package/dist/lib/panel.js +13 -0
  158. package/dist/lib/result.js +22 -22
  159. package/dist/lib/sliding-pill.js +59 -44
  160. package/dist/lib/theme-transition.js +43 -43
  161. package/dist/lib/tone.js +31 -24
  162. package/dist/lib/utils.js +3 -1
  163. package/dist/onboarding/define-tours.js +24 -23
  164. package/dist/onboarding/onboarding.svelte.js +16 -12
  165. package/dist/onboarding/pick.js +10 -6
  166. package/dist/onboarding/stores.js +40 -41
  167. package/dist/onboarding/wait.js +24 -23
  168. package/dist/panel/side-panel-types.js +1 -0
  169. package/dist/record/demo-data.js +459 -486
  170. package/dist/record/fields.js +129 -131
  171. package/dist/record/format.js +70 -82
  172. package/dist/record/gutter.js +1 -0
  173. package/dist/record/kinds.js +95 -105
  174. package/dist/record/logic.js +238 -219
  175. package/dist/record/types.js +49 -48
  176. package/dist/shadcn/accordion/index.js +3 -1
  177. package/dist/shadcn/alert/index.js +3 -1
  178. package/dist/shadcn/alert-dialog/index.js +3 -1
  179. package/dist/shadcn/attachment/index.js +3 -1
  180. package/dist/shadcn/avatar/index.js +3 -1
  181. package/dist/shadcn/breadcrumb/index.js +3 -1
  182. package/dist/shadcn/bubble/index.js +3 -1
  183. package/dist/shadcn/button/index.js +3 -1
  184. package/dist/shadcn/calendar/index.js +3 -1
  185. package/dist/shadcn/card/index.js +3 -1
  186. package/dist/shadcn/checkbox/index.js +3 -1
  187. package/dist/shadcn/code-block/index.js +3 -1
  188. package/dist/shadcn/collapsible/index.js +3 -1
  189. package/dist/shadcn/command/index.js +4 -1
  190. package/dist/shadcn/context-menu/index.js +3 -1
  191. package/dist/shadcn/dialog/index.js +3 -1
  192. package/dist/shadcn/dropdown-menu/menu-item.js +40 -20
  193. package/dist/shadcn/empty/index.js +3 -1
  194. package/dist/shadcn/field/index.js +3 -1
  195. package/dist/shadcn/hooks/highlighter.js +77 -75
  196. package/dist/shadcn/hooks/is-mobile.svelte.js +3 -3
  197. package/dist/shadcn/hooks/use-highlighted-code.svelte.js +62 -62
  198. package/dist/shadcn/input/index.js +3 -1
  199. package/dist/shadcn/kbd/index.js +3 -1
  200. package/dist/shadcn/label/index.js +3 -1
  201. package/dist/shadcn/marker/index.js +3 -1
  202. package/dist/shadcn/message/index.js +3 -1
  203. package/dist/shadcn/message-scroller/index.js +3 -1
  204. package/dist/shadcn/popover/index.js +3 -1
  205. package/dist/shadcn/progress/index.js +3 -1
  206. package/dist/shadcn/progress/tone.js +8 -7
  207. package/dist/shadcn/radio-group/index.js +3 -1
  208. package/dist/shadcn/scroll-area/index.js +3 -1
  209. package/dist/shadcn/select/index.js +3 -1
  210. package/dist/shadcn/separator/index.js +3 -1
  211. package/dist/shadcn/sheet/index.js +3 -1
  212. package/dist/shadcn/sidebar/context.svelte.js +42 -28
  213. package/dist/shadcn/sidebar/index.js +3 -1
  214. package/dist/shadcn/skeleton/index.js +3 -1
  215. package/dist/shadcn/slider/index.js +3 -1
  216. package/dist/shadcn/switch/index.js +3 -1
  217. package/dist/shadcn/table/index.js +3 -1
  218. package/dist/shadcn/tabs/index.js +3 -1
  219. package/dist/shadcn/textarea/index.js +3 -1
  220. package/dist/shadcn/toggle/index.js +3 -1
  221. package/dist/shadcn/toggle-group/index.js +3 -1
  222. package/dist/shadcn/tooltip/context.js +7 -2
  223. package/dist/shadcn/tooltip/index.js +3 -1
  224. package/dist/shadcn/trace/index.js +3 -1
  225. package/dist/slides/bridges.js +65 -59
  226. package/dist/slides/deck-controller.svelte.js +210 -207
  227. package/dist/slides/export/capture.js +66 -71
  228. package/dist/slides/export/ffmpeg.js +76 -72
  229. package/dist/slides/export/index.js +3 -0
  230. package/dist/slides/export/launch.js +9 -6
  231. package/dist/slides/export/pdf.js +60 -54
  232. package/dist/slides/export/types.js +1 -0
  233. package/dist/slides/labels.js +24 -24
  234. package/dist/slides/layout.js +39 -41
  235. package/dist/slides/model.js +9 -7
  236. package/dist/slides/navigation.js +78 -73
  237. package/dist/slides/types.js +2 -2
  238. package/dist/status/define-statuses.js +18 -14
  239. package/dist/status/status-types.js +1 -0
  240. package/dist/test-setup.js +24 -20
  241. package/dist/thread/logic.js +73 -77
  242. package/dist/thread/types.js +21 -21
  243. package/dist/work/board-order.js +94 -90
  244. package/dist/work/dates.js +16 -9
  245. package/dist/work/detail/detail-types.js +18 -18
  246. package/dist/work/docs/edited.js +32 -29
  247. package/dist/work/docs/index.js +4 -0
  248. package/dist/work/docs/labels.js +34 -34
  249. package/dist/work/docs/menu.js +113 -106
  250. package/dist/work/docs/outline.js +39 -29
  251. package/dist/work/gantt/logic.js +211 -235
  252. package/dist/work/inbox/inbox-logic.js +71 -75
  253. package/dist/work/inbox/inbox-statuses.js +75 -72
  254. package/dist/work/inbox/inbox-types.js +1 -0
  255. package/dist/work/kanban-drag.js +27 -22
  256. package/dist/work/kanban-types.js +1 -0
  257. package/dist/work/presets/sales.js +36 -32
  258. package/dist/work/reorder.js +9 -8
  259. package/dist/work/task-types.js +1 -0
  260. package/dist/work/time/duration.js +51 -46
  261. package/dist/work/time/time-types.js +23 -23
  262. package/dist/work/use-kanban-dnd.svelte.js +126 -125
  263. package/package.json +3 -1
  264. package/src/components/list-page/master-detail.svelte +3 -1
  265. package/src/components/list-page/rail-row.svelte +4 -4
  266. package/src/components/tags-input/tags-input.svelte +2 -0
  267. package/src/editor/markdown.ts +30 -35
@@ -1,632 +1,731 @@
1
1
  export const STATES = [
2
- "default",
3
- "working",
4
- "sleeping"
2
+ "default",
3
+ "working",
4
+ "sleeping"
5
5
  ];
6
- const HOP_T = 0.68;
6
+ /* the working state's hop: its period, and the height of the spinning one */
7
+ const HOP_T = .68;
7
8
  const HOP_SPIN_H = 26;
8
- const FLIP_PRE = 0.2;
9
- const FLIP_POST = 0.66;
9
+ /* a flip is that spinning hop on its own: a crouch before it (the squash
10
+ that, in the working state, the previous landing leaves), the hop, and
11
+ the landing's recovery after — measured in hop periods */
12
+ const FLIP_PRE = .2;
13
+ const FLIP_POST = .66;
10
14
  export const JUMP_DEFAULTS = {
11
- height: HOP_SPIN_H,
12
- time: HOP_T,
13
- stretch: 1,
14
- squash: 1.15,
15
- squashTime: 0.37,
16
- squashEase: "pulse",
17
- groundTime: 0.11,
18
- groundEase: "pulse",
19
- riseTime: 0.33,
20
- riseEase: "pulse",
21
- clickSquashTime: 0.24,
22
- spin: 1,
23
- lean: 6,
24
- every: 8,
25
- land: 0
15
+ height: HOP_SPIN_H,
16
+ time: HOP_T,
17
+ stretch: 1,
18
+ squash: 1.15,
19
+ squashTime: .37,
20
+ squashEase: "pulse",
21
+ groundTime: .11,
22
+ groundEase: "pulse",
23
+ riseTime: .33,
24
+ riseEase: "pulse",
25
+ clickSquashTime: .24,
26
+ spin: 1,
27
+ lean: 6,
28
+ every: 8,
29
+ land: 0
26
30
  };
27
- const flipPre = (j, poked)=>poked ? j.clickSquashTime : FLIP_PRE * j.time;
28
- const flipDuration = (j, poked)=>flipPre(j, poked) + j.time + SQUASH_PEAK[j.squashEase] * (poked ? j.clickSquashTime : j.squashTime) + Math.max(0, j.groundTime) + j.riseTime + Math.max(0, j.land) + 0.05;
31
+ /* a click's jump crouches for its squash time before take-off and lands
32
+ with a squash of the same length; an idle jump's crouch is the working
33
+ hop's short one */
34
+ const flipPre = (j, poked) => poked ? j.clickSquashTime : FLIP_PRE * j.time;
35
+ const flipDuration = (j, poked) => flipPre(j, poked) + j.time + SQUASH_PEAK[j.squashEase] * (poked ? j.clickSquashTime : j.squashTime) + Math.max(0, j.groundTime) + j.riseTime + Math.max(0, j.land) + .05;
36
+ /* where in its time each easing reaches the deepest squash */
29
37
  const SQUASH_PEAK = {
30
- sharp: 0,
31
- pulse: 2 / 7,
32
- soft: 0.5,
33
- bouncy: 0.144
38
+ sharp: 0,
39
+ pulse: 2 / 7,
40
+ soft: .5,
41
+ bouncy: .144
34
42
  };
35
- const risePulse = (v, ease)=>{
36
- if (v <= 0) return 1;
37
- if (v >= 1) return 0;
38
- switch(ease){
39
- case "sharp":
40
- return (1 - v) * (1 - v);
41
- case "soft":
42
- return 0.5 + 0.5 * Math.cos(Math.PI * v);
43
- case "bouncy":
44
- return Math.exp(-3.2 * v) * Math.cos(5.4 * v) - v * v * v * 0.026;
45
- default:
46
- {
47
- const k = 4.2 * v;
48
- return (1 + k) * Math.exp(-k) - v * v * v * 0.078;
49
- }
50
- }
43
+ /* the rise: how the body comes back from its deepest squash to its own
44
+ shape, 1 → 0 over the rise time. `sharp` lets go at once and eases in
45
+ to rest, `pulse` leaves quickly with a long settle, `soft` eases out of
46
+ the squash and into rest, `bouncy` passes rest into a slight stretch
47
+ and settles back. */
48
+ const risePulse = (v, ease) => {
49
+ if (v <= 0) return 1;
50
+ if (v >= 1) return 0;
51
+ switch (ease) {
52
+ case "sharp": return (1 - v) * (1 - v);
53
+ case "soft": return .5 + .5 * Math.cos(Math.PI * v);
54
+ case "bouncy": return Math.exp(-3.2 * v) * Math.cos(5.4 * v) - v * v * v * .026;
55
+ default: {
56
+ const k = 4.2 * v;
57
+ return (1 + k) * Math.exp(-k) - v * v * v * .078;
58
+ }
59
+ }
51
60
  };
52
- const groundShape = (v, ease)=>1 + 0.25 * squashPulse(v, ease);
53
- const squashPulse = (u, ease)=>{
54
- if (u <= 0 || u >= 1) return 0;
55
- let x;
56
- switch(ease){
57
- case "sharp":
58
- x = (1 - u) * (1 - u);
59
- break;
60
- case "soft":
61
- x = Math.sin(Math.PI * u) ** 2;
62
- break;
63
- case "bouncy":
64
- x = Math.exp(-3.15 * u) * Math.sin(8.43 * u) / 0.596;
65
- break;
66
- default:
67
- {
68
- const k = 7 * u;
69
- x = k * k * Math.exp(2 - k) / 4;
70
- }
71
- }
72
- const tail = u > 0.85 ? 1 - (u - 0.85) / 0.15 : 1;
73
- return x * tail * tail * (3 - 2 * tail);
61
+ /* the weight settling through the hold, as a gain on the deepest squash:
62
+ the same four curves as the landing squash, pressing up to a quarter
63
+ deeper and back to the held depth, so the hold's ends never step */
64
+ const groundShape = (v, ease) => 1 + .25 * squashPulse(v, ease);
65
+ /* the landing squash over its time, 0–1 → its depth, 0–1 at the peak:
66
+ sharp is all there at contact and eases off; pulse presses quickly and
67
+ recovers without a wobble (a critically damped spring under a short
68
+ push); soft eases in and out; bouncy overshoots into a stretch and
69
+ settles. Each is faded out over the last part so it ends at nothing. */
70
+ const squashPulse = (u, ease) => {
71
+ if (u <= 0 || u >= 1) return 0;
72
+ let x;
73
+ switch (ease) {
74
+ case "sharp":
75
+ x = (1 - u) * (1 - u);
76
+ break;
77
+ case "soft":
78
+ x = Math.sin(Math.PI * u) ** 2;
79
+ break;
80
+ case "bouncy":
81
+ x = Math.exp(-3.15 * u) * Math.sin(8.43 * u) / .596;
82
+ break;
83
+ default: {
84
+ const k = 7 * u;
85
+ x = k * k * Math.exp(2 - k) / 4;
86
+ }
87
+ }
88
+ const tail = u > .85 ? 1 - (u - .85) / .15 : 1;
89
+ return x * tail * tail * (3 - 2 * tail);
74
90
  };
75
- const hopSquash = (a)=>Math.exp(-Math.pow(Math.min(Math.abs(a), Math.abs(a - 1)) / 0.11, 2));
91
+ /* the hop's shaping: squash on the ground, stretch at the top */
92
+ const hopSquash = (a) => Math.exp(-Math.pow(Math.min(Math.abs(a), Math.abs(a - 1)) / .11, 2));
76
93
  const DEG = Math.PI / 180;
77
94
  const TAU = Math.PI * 2;
95
+ /* how long a state change takes: dozing off is slow, waking a little
96
+ quicker, the rest brisk */
97
+ /* How long a switch takes. Settling down takes longer than getting to
98
+ work: coming back to idle the body has to let go of the hops, so it
99
+ eases out over more than a second. */
78
100
  const SWITCH_TO = {
79
- default: 1.2,
80
- working: 0.7,
81
- sleeping: 1.4
101
+ default: 1.2,
102
+ working: .7,
103
+ sleeping: 1.4
82
104
  };
83
105
  const SWITCH_FROM_SLEEP = 1;
106
+ /* Deterministic per-instance randomness (mulberry32). */
84
107
  function rng(seed) {
85
- let a = seed * 0x9e3779b1 >>> 0 || 1;
86
- return ()=>{
87
- a = a + 0x6d2b79f5 >>> 0;
88
- let t = a;
89
- t = Math.imul(t ^ t >>> 15, t | 1);
90
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
91
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
92
- };
108
+ let a = seed * 2654435761 >>> 0 || 1;
109
+ return () => {
110
+ a = a + 1831565813 >>> 0;
111
+ let t = a;
112
+ t = Math.imul(t ^ t >>> 15, t | 1);
113
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
114
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
115
+ };
93
116
  }
117
+ /** Exponential approach: `rate` per second, frame-rate independent. */
94
118
  function approach(cur, target, rate, dt) {
95
- return cur + (target - cur) * (1 - Math.exp(-rate * dt));
119
+ return cur + (target - cur) * (1 - Math.exp(-rate * dt));
96
120
  }
97
- const easeInOut = (p)=>p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
98
- const easeSine = (p)=>0.5 - 0.5 * Math.cos(Math.PI * p);
121
+ const easeInOut = (p) => p < .5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
122
+ /* gentler at both ends than the cubic: for drifting off and waking */
123
+ const easeSine = (p) => .5 - .5 * Math.cos(Math.PI * p);
124
+ /* A value that drifts: picks a new target inside its range every hold,
125
+ and eases toward it. Ranges change with the state; the value never
126
+ jumps. */
99
127
  class Wander {
100
- rand;
101
- amp;
102
- holdMin;
103
- holdMax;
104
- rate;
105
- spring;
106
- value = 0;
107
- vel = 0;
108
- target = 0;
109
- next = 0;
110
- constructor(rand, amp, holdMin, holdMax, rate, spring = false){
111
- this.rand = rand;
112
- this.amp = amp;
113
- this.holdMin = holdMin;
114
- this.holdMax = holdMax;
115
- this.rate = rate;
116
- this.spring = spring;
117
- }
118
- update(t, dt) {
119
- if (t >= this.next) {
120
- this.target = (this.rand() * 2 - 1) * this.amp;
121
- this.next = t + this.holdMin + this.rand() * (this.holdMax - this.holdMin);
122
- }
123
- if (this.spring) {
124
- const w = this.rate * 1.6, z = 0.9;
125
- this.vel += (w * w * (this.target - this.value) - 2 * z * w * this.vel) * dt;
126
- this.value += this.vel * dt;
127
- } else this.value = approach(this.value, this.target, this.rate, dt);
128
- }
129
- aim(v) {
130
- this.target = v;
131
- this.next = Infinity;
132
- }
133
- set(amp, holdMin, holdMax, rate) {
134
- this.amp = amp;
135
- this.holdMin = holdMin;
136
- this.holdMax = holdMax;
137
- this.rate = rate;
138
- this.next = 0;
139
- }
128
+ rand;
129
+ amp;
130
+ holdMin;
131
+ holdMax;
132
+ rate;
133
+ spring;
134
+ value = 0;
135
+ vel = 0;
136
+ target = 0;
137
+ next = 0;
138
+ /* A channel that picks a new target now and then and moves to it. The
139
+ head channels move as a lightly damped spring — the turn starts and
140
+ ends softly, the way a head does — while the eyes dart with an
141
+ exponential approach, the way eyes do. */
142
+ constructor(rand, amp, holdMin, holdMax, rate, spring = false) {
143
+ this.rand = rand;
144
+ this.amp = amp;
145
+ this.holdMin = holdMin;
146
+ this.holdMax = holdMax;
147
+ this.rate = rate;
148
+ this.spring = spring;
149
+ }
150
+ update(t, dt) {
151
+ if (t >= this.next) {
152
+ this.target = (this.rand() * 2 - 1) * this.amp;
153
+ this.next = t + this.holdMin + this.rand() * (this.holdMax - this.holdMin);
154
+ }
155
+ if (this.spring) {
156
+ const w = this.rate * 1.6, z = .9;
157
+ this.vel += (w * w * (this.target - this.value) - 2 * z * w * this.vel) * dt;
158
+ this.value += this.vel * dt;
159
+ } else this.value = approach(this.value, this.target, this.rate, dt);
160
+ }
161
+ /** aim at a value and stay there: the rig picks the next one */
162
+ aim(v) {
163
+ this.target = v;
164
+ this.next = Infinity;
165
+ }
166
+ set(amp, holdMin, holdMax, rate) {
167
+ this.amp = amp;
168
+ this.holdMin = holdMin;
169
+ this.holdMax = holdMax;
170
+ this.rate = rate;
171
+ this.next = 0;
172
+ }
140
173
  }
174
+ /* A one-shot event: progress 0 → 1 over its duration, then idle at -1. */
141
175
  class Event {
142
- duration;
143
- p = -1;
144
- constructor(duration){
145
- this.duration = duration;
146
- }
147
- fire() {
148
- this.p = 0;
149
- }
150
- get active() {
151
- return this.p >= 0;
152
- }
153
- update(dt) {
154
- if (this.p < 0) return;
155
- this.p += dt / this.duration;
156
- if (this.p >= 1) this.p = -1;
157
- }
176
+ duration;
177
+ p = -1;
178
+ constructor(duration) {
179
+ this.duration = duration;
180
+ }
181
+ fire() {
182
+ this.p = 0;
183
+ }
184
+ get active() {
185
+ return this.p >= 0;
186
+ }
187
+ update(dt) {
188
+ if (this.p < 0) return;
189
+ this.p += dt / this.duration;
190
+ if (this.p >= 1) this.p = -1;
191
+ }
158
192
  }
193
+ /* The idle gaze: how far the head swings, and how long it stays. The
194
+ sideways reach is the big one — a look to a corner reads as a turn of
195
+ the head, with the rise and a slight tilt going along. */
159
196
  const GAZE_YAW = 35 * DEG;
160
197
  const GAZE_PITCH = 14 * DEG;
161
198
  const GAZE_ROLL = 3.2 * DEG;
162
199
  const GAZE_HOLD_MIN = 2.6;
163
200
  const GAZE_HOLD_MAX = 4.4;
164
201
  const REST = {
165
- default: {
166
- pitch: 0,
167
- roll: 0,
168
- y: 0,
169
- lookX: 0,
170
- lookY: 0
171
- },
172
- working: {
173
- pitch: 5 * DEG,
174
- roll: 0,
175
- y: 0,
176
- lookX: 0,
177
- lookY: 0
178
- },
179
- sleeping: {
180
- pitch: -16 * DEG,
181
- roll: 6 * DEG,
182
- y: 3,
183
- lookX: 0,
184
- lookY: 1
185
- }
202
+ default: {
203
+ pitch: 0,
204
+ roll: 0,
205
+ y: 0,
206
+ lookX: 0,
207
+ lookY: 0
208
+ },
209
+ working: {
210
+ pitch: 5 * DEG,
211
+ roll: 0,
212
+ y: 0,
213
+ lookX: 0,
214
+ lookY: 0
215
+ },
216
+ sleeping: {
217
+ pitch: -16 * DEG,
218
+ roll: 6 * DEG,
219
+ y: 3,
220
+ lookX: 0,
221
+ lookY: 1
222
+ }
186
223
  };
187
224
  export class Sim {
188
- pose = {
189
- yaw: 0,
190
- pitch: 0,
191
- roll: 0,
192
- x: 0,
193
- y: 0,
194
- sx: 1,
195
- sy: 1,
196
- eyeOpen: 1,
197
- blinkL: 0,
198
- blinkR: 0,
199
- lookX: 0,
200
- lookY: 0,
201
- breath: 0,
202
- laugh: 0,
203
- whirl: 0,
204
- whirlAngle: 0,
205
- w: [
206
- 1,
207
- 0,
208
- 0
209
- ]
210
- };
211
- state = "default";
212
- rand;
213
- t = 0;
214
- wFrom = [
215
- 1,
216
- 0,
217
- 0
218
- ];
219
- tr = 1;
220
- trDuration = 1.2;
221
- yawW;
222
- pitchW;
223
- rollW;
224
- lookXW;
225
- lookYW;
226
- blink = new Event(0.17);
227
- blinkAt;
228
- blinkAgain = false;
229
- dart = new Event(0.12);
230
- dartAt;
231
- dartX = 0;
232
- dartY = 0;
233
- flip = new Event(flipDuration(JUMP_DEFAULTS, false));
234
- flipPoked = false;
235
- jump = {
236
- ...JUMP_DEFAULTS
237
- };
238
- flipAt;
239
- flipSide = 1;
240
- nod = new Event(1.7);
241
- nodAt;
242
- hopPhase = 0;
243
- hopCount = 0;
244
- hopGain = 0;
245
- laughEv = new Event(0.8);
246
- laughAt;
247
- prevYaw = 0;
248
- jelly = 0;
249
- jellyV = 0;
250
- gazeLead = 0;
251
- gazeDir = [
252
- 0,
253
- 0
254
- ];
255
- gazeAt = 0;
256
- turnK = 1;
257
- breathPhase = 0;
258
- ptrX = 0;
259
- ptrY = 0;
260
- ptrS = 0;
261
- ptrTargetX = 0;
262
- ptrTargetY = 0;
263
- ptrTargetS = 0;
264
- baseYaw = 0;
265
- constructor(seed, state = "default"){
266
- this.rand = rng(Math.floor(seed * 1e6) + 1);
267
- const r = this.rand;
268
- this.yawW = new Wander(r, 36 * DEG, 1.1, 2.6, 3, true);
269
- this.pitchW = new Wander(r, 10 * DEG, 1.1, 2.6, 2.6, true);
270
- this.rollW = new Wander(r, 5 * DEG, 1.6, 3.2, 2, true);
271
- this.lookXW = new Wander(r, 3.6, 0.5, 2, 14);
272
- this.lookYW = new Wander(r, 2.4, 0.5, 2, 14);
273
- this.t = r() * 10;
274
- this.hopPhase = r();
275
- this.breathPhase = r();
276
- this.blinkAt = this.t + 1 + r() * 3;
277
- this.flipAt = this.nextFlip(this.t, 1);
278
- this.nodAt = this.t + 3 + r() * 4;
279
- this.dartAt = this.t + 1 + r() * 2;
280
- this.laughAt = this.t + 0.6 + r() * 1.5;
281
- this.setState(state, true);
282
- }
283
- setState(next, immediate = false) {
284
- if (next === this.state && !immediate) return;
285
- const from = this.state;
286
- this.state = next;
287
- const w = this.pose.w;
288
- if (immediate) {
289
- for(let i = 0; i < 3; i++)w[i] = STATES[i] === next ? 1 : 0;
290
- this.tr = 1;
291
- } else {
292
- this.wFrom = [
293
- w[0],
294
- w[1],
295
- w[2]
296
- ];
297
- this.tr = 0;
298
- this.trDuration = from === "sleeping" ? SWITCH_FROM_SLEEP : SWITCH_TO[next];
299
- }
300
- switch(next){
301
- case "default":
302
- this.yawW.set(GAZE_YAW, 2.6, 5.4, 2);
303
- this.pitchW.set(GAZE_PITCH, 2.8, 5.8, 1.8);
304
- this.rollW.set(GAZE_ROLL, 3.4, 6.6, 1.5);
305
- this.gazeAt = 0;
306
- this.gazeDir = [
307
- 0,
308
- 0
309
- ];
310
- this.lookXW.set(3.6, 0.6, 2.2, 13);
311
- this.lookYW.set(2.4, 0.6, 2.2, 13);
312
- this.flipAt = this.nextFlip(this.t, 0.6);
313
- break;
314
- case "working":
315
- this.yawW.set(16 * DEG, 0.9, 1.8, 4);
316
- this.pitchW.set(3 * DEG, 1.2, 2.4, 3);
317
- this.rollW.set(0, 1, 2, 3);
318
- this.lookXW.set(2, 0.5, 1.2, 12);
319
- this.lookYW.set(1, 0.5, 1.2, 12);
320
- this.hopPhase = 0;
321
- this.hopCount = 0;
322
- this.laughAt = this.t + 0.5 + this.rand() * 1.2;
323
- break;
324
- case "sleeping":
325
- this.yawW.set(7 * DEG, 3, 6, 0.7);
326
- this.pitchW.set(3 * DEG, 3, 6, 0.7);
327
- this.rollW.set(2 * DEG, 3, 6, 0.6);
328
- this.lookXW.set(0, 2, 4, 2);
329
- this.lookYW.set(0, 2, 4, 2);
330
- this.nodAt = this.t + 2.5 + this.rand() * 4;
331
- break;
332
- }
333
- }
334
- setPointer(x, y, strength) {
335
- this.ptrTargetX = Math.max(-1.2, Math.min(1.2, x));
336
- this.ptrTargetY = Math.max(-1.2, Math.min(1.2, y));
337
- this.ptrTargetS = Math.max(0, Math.min(1, strength));
338
- }
339
- poke() {
340
- if (this.flip.active && this.flip.p < 0.6) return;
341
- this.flipPoked = true;
342
- this.flip.duration = flipDuration(this.jump, true);
343
- this.flipSide = this.rand() < 0.5 ? -1 : 1;
344
- this.flip.fire();
345
- this.flipAt = this.nextFlip(this.t, 1.1);
346
- }
347
- setTurn(k) {
348
- const next = Math.max(0, k);
349
- if (next === this.turnK) return;
350
- this.turnK = next;
351
- if (this.state === "default") this.gazeAt = 0;
352
- }
353
- setJump(j) {
354
- const every = this.jump.every;
355
- Object.assign(this.jump, j);
356
- if (j.every !== undefined && j.every !== every) this.flipAt = this.nextFlip(this.t, 1);
357
- }
358
- nextGaze() {
359
- const r = this.rand;
360
- const [px, py] = this.gazeDir;
361
- if (px !== 0 || py !== 0) {
362
- const p = r();
363
- if (p < 0.66) return [
364
- -px,
365
- -py
366
- ];
367
- if (p < 0.85) return [
368
- -px,
369
- py
370
- ];
371
- return [
372
- 0,
373
- 0
374
- ];
375
- }
376
- const corners = [
377
- [
378
- 1,
379
- -1
380
- ],
381
- [
382
- -1,
383
- 1
384
- ],
385
- [
386
- -1,
387
- -1
388
- ],
389
- [
390
- 1,
391
- 1
392
- ]
393
- ];
394
- return corners[Math.floor(r() * corners.length)];
395
- }
396
- nextFlip(t, k) {
397
- const every = this.jump.every;
398
- return every > 0 ? t + every * k * (0.625 + this.rand() * 0.75) : Infinity;
399
- }
400
- update(dt) {
401
- dt = Math.min(dt, 0.05);
402
- this.t += dt;
403
- const t = this.t;
404
- const p = this.pose;
405
- const w = p.w;
406
- if (this.tr < 1) {
407
- this.tr = Math.min(1, this.tr + dt / this.trDuration);
408
- const e = easeSine(this.tr);
409
- for(let i = 0; i < 3; i++){
410
- const target = STATES[i] === this.state ? 1 : 0;
411
- w[i] = this.wFrom[i] + (target - this.wFrom[i]) * e;
412
- }
413
- }
414
- const [wd, ww, ws] = w;
415
- const rest = {
416
- pitch: 0,
417
- roll: 0,
418
- y: 0,
419
- lookX: 0,
420
- lookY: 0
421
- };
422
- for(let i = 0; i < 3; i++){
423
- const r = REST[STATES[i]];
424
- rest.pitch += r.pitch * w[i];
425
- rest.roll += r.roll * w[i];
426
- rest.y += r.y * w[i];
427
- rest.lookX += r.lookX * w[i];
428
- rest.lookY += r.lookY * w[i];
429
- }
430
- if (this.state === "default" && t >= this.gazeAt) {
431
- const [gx, gy] = this.nextGaze();
432
- this.gazeDir = [
433
- gx,
434
- gy
435
- ];
436
- const reach = 0.84 + this.rand() * 0.16;
437
- this.yawW.aim(gx * GAZE_YAW * reach * this.turnK);
438
- this.pitchW.aim(gy * GAZE_PITCH * reach);
439
- this.rollW.aim(gx * GAZE_ROLL * reach * this.turnK);
440
- this.gazeAt = t + GAZE_HOLD_MIN + this.rand() * (GAZE_HOLD_MAX - GAZE_HOLD_MIN);
441
- }
442
- this.yawW.update(t, dt);
443
- this.pitchW.update(t, dt);
444
- this.rollW.update(t, dt);
445
- this.lookXW.update(t, dt);
446
- this.lookYW.update(t, dt);
447
- this.ptrS = approach(this.ptrS, this.ptrTargetS, 8, dt);
448
- this.ptrX = approach(this.ptrX, this.ptrTargetX, 14, dt);
449
- this.ptrY = approach(this.ptrY, this.ptrTargetY, 14, dt);
450
- const ps = this.ptrS;
451
- const quiet = 1 - 0.75 * ps;
452
- this.baseYaw = approach(this.baseYaw, this.yawW.value * quiet + 22 * DEG * this.ptrX * ps, 5, dt);
453
- const basePitch = rest.pitch + this.pitchW.value * quiet - 12 * DEG * this.ptrY * ps;
454
- const baseRoll = rest.roll + this.rollW.value * quiet;
455
- const baseY = rest.y;
456
- const baseLookX = rest.lookX + this.lookXW.value * quiet + 4.5 * this.ptrX * ps;
457
- const baseLookY = rest.lookY + this.lookYW.value * quiet + 3 * this.ptrY * ps;
458
- let spin = 0, hopY = 0, sx = 1, sy = 1, pitchAdd = 0, rollAdd = 0, blinkClose = 0, lookXAdd = 0, lookYAdd = 0, laugh = 0;
459
- let whirl = 0, whirlAngle = 0;
460
- const smooth = (a, b, v)=>{
461
- const x = Math.min(1, Math.max(0, (v - a) / (b - a)));
462
- return x * x * (3 - 2 * x);
463
- };
464
- const envelope = (q)=>smooth(0.1, 0.26, q) * (1 - smooth(0.66, 0.9, q));
465
- const ringAngle = (q)=>TAU * (1.5 * q + 0.9 * easeInOut(q));
466
- if (t >= this.blinkAt && !this.blink.active && wd + ww > 0.5) {
467
- this.blink.fire();
468
- this.blinkAgain = !this.blinkAgain && this.rand() < 0.22;
469
- this.blinkAt = t + (this.blinkAgain ? 0.28 : 2.2 + this.rand() * 2.6);
470
- }
471
- this.blink.update(dt);
472
- if (this.blink.active) blinkClose = Math.sin(Math.PI * this.blink.p);
473
- if (t >= this.dartAt && !this.dart.active && wd + ww > 0.5) {
474
- this.dart.fire();
475
- this.dartX = (this.rand() * 2 - 1) * 4;
476
- this.dartY = (this.rand() * 2 - 1) * 2;
477
- this.dart.duration = 0.25 + this.rand() * 0.45;
478
- this.dartAt = t + 1.2 + this.rand() * 2.6;
479
- }
480
- this.dart.update(dt);
481
- if (this.dart.active) {
482
- const q = this.dart.p;
483
- const hold = q < 0.15 ? q / 0.15 : q > 0.8 ? (1 - q) / 0.2 : 1;
484
- lookXAdd += this.dartX * hold * (wd + ww);
485
- lookYAdd += this.dartY * hold * (wd + ww);
486
- }
487
- if (this.state === "default" && t >= this.flipAt && !this.flip.active) {
488
- this.flipPoked = false;
489
- this.flip.duration = flipDuration(this.jump, false);
490
- this.flipSide = this.rand() < 0.5 ? -1 : 1;
491
- this.flip.fire();
492
- this.flipAt = this.nextFlip(t, 1);
493
- }
494
- this.flip.update(dt);
495
- if (this.flip.active) {
496
- const J = this.jump;
497
- const preS = flipPre(J, this.flipPoked);
498
- const a = (this.flip.p * this.flip.duration - preS) / J.time;
499
- const q = Math.min(1, Math.max(0, a));
500
- const arc = Math.sin(Math.PI * q);
501
- spin += TAU * J.spin * easeInOut(q);
502
- hopY -= J.height * arc;
503
- const tl = (a - 1) * J.time - J.land;
504
- const squashTime = this.flipPoked ? J.clickSquashTime : J.squashTime;
505
- const crouch = (u)=>this.flipPoked ? u * u * (3 - 2 * u) : hopSquash(u - 1);
506
- const hold = Math.max(0, J.groundTime);
507
- const peakT = SQUASH_PEAK[J.squashEase] * squashTime;
508
- const rise = tl - peakT - hold;
509
- const depth = tl <= peakT ? squashPulse(tl / squashTime, J.squashEase) : rise <= 0 ? groundShape((tl - peakT) / hold, J.groundEase) : risePulse(rise / J.riseTime, J.riseEase);
510
- const land = (a < 0 ? crouch(Math.max(0, 1 + a * J.time / preS)) : tl > 0 ? depth : a < 0.2 ? hopSquash(a) : 0) * J.squash;
511
- sx += 0.16 * land - 0.06 * arc * J.stretch;
512
- sy += -0.18 * land + 0.09 * arc * J.stretch;
513
- rollAdd += this.flipSide * J.lean * DEG * arc;
514
- if (J.spin > 0) {
515
- laugh = Math.max(laugh, arc);
516
- whirl = Math.max(whirl, envelope(q));
517
- whirlAngle = ringAngle(q);
518
- }
519
- }
520
- const J = this.jump;
521
- const exitHold = Math.max(0, J.groundTime);
522
- const exitFor = exitHold + J.riseTime;
523
- if (this.state === "working") this.hopGain = ww;
524
- if (this.hopGain > 0.02 && (this.state === "working" || this.hopPhase > 0)) {
525
- this.hopPhase += dt / HOP_T;
526
- if (this.hopPhase >= 1) {
527
- if (this.state === "working") {
528
- this.hopPhase -= 1;
529
- this.hopCount += 1;
530
- } else if ((this.hopPhase - 1) * HOP_T >= exitFor) {
531
- this.hopPhase = 0;
532
- this.hopGain = 0;
533
- }
534
- }
535
- const g = this.hopGain;
536
- const q = Math.min(1, this.hopPhase);
537
- const arc = Math.sin(Math.PI * q);
538
- const spinning = this.hopCount % 3 === 2;
539
- const h = spinning ? HOP_SPIN_H : 18;
540
- hopY -= h * arc * g;
541
- const exitT = this.state !== "working" && this.hopPhase > 1 ? (this.hopPhase - 1) * HOP_T : -1;
542
- const land = exitT < 0 ? hopSquash(this.hopPhase) : exitT < exitHold ? groundShape(exitT / exitHold, J.groundEase) : risePulse((exitT - exitHold) / J.riseTime, J.riseEase);
543
- sx += (0.16 * land - 0.06 * arc) * g;
544
- sy += (-0.18 * land + 0.09 * arc) * g;
545
- if (spinning) {
546
- spin += TAU * easeInOut(q) * g;
547
- laugh = Math.max(laugh, arc * g);
548
- if (envelope(q) * g > whirl) {
549
- whirl = envelope(q) * g;
550
- whirlAngle = ringAngle(q);
551
- }
552
- }
553
- rollAdd += (this.hopCount % 2 === 0 ? 1 : -1) * 6 * DEG * arc * g;
554
- }
555
- if (this.state === "working" && t >= this.laughAt && !this.laughEv.active) {
556
- this.laughEv.fire();
557
- this.laughEv.duration = 0.6 + this.rand() * 0.5;
558
- this.laughAt = t + 1.6 + this.rand() * 2.2;
559
- }
560
- this.laughEv.update(dt);
561
- if (this.laughEv.active) {
562
- const q = this.laughEv.p;
563
- laugh = Math.max(laugh, q < 0.18 ? q / 0.18 : q > 0.78 ? (1 - q) / 0.22 : 1);
564
- }
565
- if (this.state === "sleeping" && t >= this.nodAt && !this.nod.active) {
566
- this.nod.fire();
567
- this.nodAt = t + 4 + this.rand() * 4;
568
- }
569
- this.nod.update(dt);
570
- if (this.nod.active) {
571
- const q = this.nod.p;
572
- const dip = q < 0.72 ? easeInOut(q / 0.72) : 1 - easeInOut((q - 0.72) / 0.28);
573
- pitchAdd -= 13 * DEG * dip * ws;
574
- }
575
- this.breathPhase += dt / (3.6 + 1.2 * ws);
576
- const breath = Math.sin(this.breathPhase * TAU);
577
- p.breath = breath;
578
- sx += breath * (0.008 + 0.014 * ws);
579
- sy += breath * (0.012 + 0.02 * ws);
580
- const bob = Math.sin(t * TAU / 3.4) * 2 * (1 - ws);
581
- p.yaw = this.baseYaw + spin;
582
- let dyaw = this.baseYaw - this.prevYaw;
583
- dyaw = ((dyaw + Math.PI) % TAU + TAU) % TAU - Math.PI;
584
- this.prevYaw = this.baseYaw;
585
- const rate = dt > 0 ? Math.abs(dyaw) / dt : 0;
586
- const leadTarget = dt > 0 ? Math.max(-2.2, Math.min(2.2, dyaw / dt * 2.4)) : 0;
587
- this.gazeLead = approach(this.gazeLead, leadTarget, 9, dt);
588
- const jellyTarget = Math.min(0.22, 0.055 * rate);
589
- const omega = 16, zeta = 0.45;
590
- this.jellyV += (omega * omega * (jellyTarget - this.jelly) - 2 * zeta * omega * this.jellyV) * dt;
591
- this.jelly += this.jellyV * dt;
592
- const jelly = Math.max(-0.08, Math.min(0.28, this.jelly)) * 0.6;
593
- sx *= 1 + jelly;
594
- sy *= 1 - 0.55 * jelly;
595
- p.pitch = basePitch + pitchAdd;
596
- p.roll = baseRoll + rollAdd;
597
- p.x = 0;
598
- p.y = baseY + hopY + bob;
599
- p.sx = sx;
600
- p.sy = sy;
601
- p.eyeOpen = 1;
602
- p.laugh = approach(p.laugh, laugh, 30, dt);
603
- p.blinkL = blinkClose;
604
- p.blinkR = blinkClose;
605
- p.lookX = baseLookX + lookXAdd + this.gazeLead;
606
- p.lookY = baseLookY + lookYAdd;
607
- p.whirl = whirl;
608
- p.whirlAngle = whirlAngle;
609
- }
225
+ pose = {
226
+ yaw: 0,
227
+ pitch: 0,
228
+ roll: 0,
229
+ x: 0,
230
+ y: 0,
231
+ sx: 1,
232
+ sy: 1,
233
+ eyeOpen: 1,
234
+ blinkL: 0,
235
+ blinkR: 0,
236
+ lookX: 0,
237
+ lookY: 0,
238
+ breath: 0,
239
+ laugh: 0,
240
+ whirl: 0,
241
+ whirlAngle: 0,
242
+ w: [
243
+ 1,
244
+ 0,
245
+ 0
246
+ ]
247
+ };
248
+ state = "default";
249
+ rand;
250
+ t = 0;
251
+ /* a state change: the weights it started from and its progress */
252
+ wFrom = [
253
+ 1,
254
+ 0,
255
+ 0
256
+ ];
257
+ tr = 1;
258
+ trDuration = 1.2;
259
+ yawW;
260
+ pitchW;
261
+ rollW;
262
+ lookXW;
263
+ lookYW;
264
+ blink = new Event(.17);
265
+ blinkAt;
266
+ blinkAgain = false;
267
+ /* −1 left eye only, 1 right eye only, 0 both */
268
+ dart = new Event(.12);
269
+ dartAt;
270
+ dartX = 0;
271
+ dartY = 0;
272
+ flip = new Event(flipDuration(JUMP_DEFAULTS, false));
273
+ flipPoked = false;
274
+ jump = { ...JUMP_DEFAULTS };
275
+ flipAt;
276
+ flipSide = 1;
277
+ nod = new Event(1.7);
278
+ nodAt;
279
+ hopPhase = 0;
280
+ hopCount = 0;
281
+ /** the hops' gain: the working weight while in the state, then held so
282
+ a hop under way finishes whole when the state is left */
283
+ hopGain = 0;
284
+ laughEv = new Event(.8);
285
+ laughAt;
286
+ /* the jelly: a damped spring driven by how fast the head turns, so a
287
+ sweep stretches the body and it wobbles back */
288
+ prevYaw = 0;
289
+ jelly = 0;
290
+ jellyV = 0;
291
+ /** how far the eyes run ahead of a head turn */
292
+ gazeLead = 0;
293
+ /** the idle gaze: where the head is looking, and when it moves on */
294
+ gazeDir = [0, 0];
295
+ gazeAt = 0;
296
+ /** how far it turns to the side, 1 as the gaze has it */
297
+ turnK = 1;
298
+ /** the breathing cycle's phase, in turns */
299
+ breathPhase = 0;
300
+ /* the pointer, as an offset from the head in head-widths, and how much
301
+ to follow it — both smoothed */
302
+ ptrX = 0;
303
+ ptrY = 0;
304
+ ptrS = 0;
305
+ ptrTargetX = 0;
306
+ ptrTargetY = 0;
307
+ ptrTargetS = 0;
308
+ /* the smoothed yaw the head is turning to on its own */
309
+ baseYaw = 0;
310
+ constructor(seed, state = "default") {
311
+ this.rand = rng(Math.floor(seed * 1e6) + 1);
312
+ const r = this.rand;
313
+ this.yawW = new Wander(r, 36 * DEG, 1.1, 2.6, 3, true);
314
+ this.pitchW = new Wander(r, 10 * DEG, 1.1, 2.6, 2.6, true);
315
+ this.rollW = new Wander(r, 5 * DEG, 1.6, 3.2, 2, true);
316
+ this.lookXW = new Wander(r, 3.6, .5, 2, 14);
317
+ this.lookYW = new Wander(r, 2.4, .5, 2, 14);
318
+ /* every instance starts somewhere else in its loops */
319
+ this.t = r() * 10;
320
+ this.hopPhase = r();
321
+ this.breathPhase = r();
322
+ /* event timers count from that start, so nothing fires on the first tick */
323
+ this.blinkAt = this.t + 1 + r() * 3;
324
+ this.flipAt = this.nextFlip(this.t, 1);
325
+ this.nodAt = this.t + 3 + r() * 4;
326
+ this.dartAt = this.t + 1 + r() * 2;
327
+ this.laughAt = this.t + .6 + r() * 1.5;
328
+ this.setState(state, true);
329
+ }
330
+ setState(next, immediate = false) {
331
+ if (next === this.state && !immediate) return;
332
+ const from = this.state;
333
+ this.state = next;
334
+ const w = this.pose.w;
335
+ if (immediate) {
336
+ for (let i = 0; i < 3; i++) w[i] = STATES[i] === next ? 1 : 0;
337
+ this.tr = 1;
338
+ } else {
339
+ this.wFrom = [
340
+ w[0],
341
+ w[1],
342
+ w[2]
343
+ ];
344
+ this.tr = 0;
345
+ this.trDuration = from === "sleeping" ? SWITCH_FROM_SLEEP : SWITCH_TO[next];
346
+ }
347
+ switch (next) {
348
+ case "default":
349
+ /* the head takes its time: it holds a place, then swings to the
350
+ next over about a second — no short quick jabs. The gaze picks
351
+ where, so these carry the motion and their amplitude is unused. */
352
+ this.yawW.set(GAZE_YAW, 2.6, 5.4, 2);
353
+ this.pitchW.set(GAZE_PITCH, 2.8, 5.8, 1.8);
354
+ this.rollW.set(GAZE_ROLL, 3.4, 6.6, 1.5);
355
+ this.gazeAt = 0;
356
+ this.gazeDir = [0, 0];
357
+ /* the eyes keep their quickness: they dart, the head follows */
358
+ this.lookXW.set(3.6, .6, 2.2, 13);
359
+ this.lookYW.set(2.4, .6, 2.2, 13);
360
+ this.flipAt = this.nextFlip(this.t, .6);
361
+ break;
362
+ case "working":
363
+ this.yawW.set(16 * DEG, .9, 1.8, 4);
364
+ this.pitchW.set(3 * DEG, 1.2, 2.4, 3);
365
+ this.rollW.set(0, 1, 2, 3);
366
+ this.lookXW.set(2, .5, 1.2, 12);
367
+ this.lookYW.set(1, .5, 1.2, 12);
368
+ this.hopPhase = 0;
369
+ this.hopCount = 0;
370
+ this.laughAt = this.t + .5 + this.rand() * 1.2;
371
+ break;
372
+ case "sleeping":
373
+ this.yawW.set(7 * DEG, 3, 6, .7);
374
+ this.pitchW.set(3 * DEG, 3, 6, .7);
375
+ this.rollW.set(2 * DEG, 3, 6, .6);
376
+ this.lookXW.set(0, 2, 4, 2);
377
+ this.lookYW.set(0, 2, 4, 2);
378
+ this.nodAt = this.t + 2.5 + this.rand() * 4;
379
+ break;
380
+ }
381
+ }
382
+ /** Where the pointer is, relative to the head (−1 … 1 across a head
383
+ width), and how strongly to follow it (0 lets go). */
384
+ setPointer(x, y, strength) {
385
+ this.ptrTargetX = Math.max(-1.2, Math.min(1.2, x));
386
+ this.ptrTargetY = Math.max(-1.2, Math.min(1.2, y));
387
+ this.ptrTargetS = Math.max(0, Math.min(1, strength));
388
+ }
389
+ /** A hop and a full turn, right now, whatever the state. */
390
+ poke() {
391
+ if (this.flip.active && this.flip.p < .6) return;
392
+ this.flipPoked = true;
393
+ this.flip.duration = flipDuration(this.jump, true);
394
+ this.flipSide = this.rand() < .5 ? -1 : 1;
395
+ this.flip.fire();
396
+ this.flipAt = this.nextFlip(this.t, 1.1);
397
+ }
398
+ /** How far the head turns to the side while idle: 1 as the gaze has
399
+ it, 0 keeps it facing forward. */
400
+ setTurn(k) {
401
+ const next = Math.max(0, k);
402
+ if (next === this.turnK) return;
403
+ this.turnK = next;
404
+ /* take the new reach at once rather than at the next place */
405
+ if (this.state === "default") this.gazeAt = 0;
406
+ }
407
+ /** The jump's numbers; any subset. */
408
+ setJump(j) {
409
+ const every = this.jump.every;
410
+ Object.assign(this.jump, j);
411
+ /* a new interval takes effect at once, so `every: 0` stops the next
412
+ jump that was already due and a first value starts the clock */
413
+ if (j.every !== undefined && j.every !== every) this.flipAt = this.nextFlip(this.t, 1);
414
+ }
415
+ /* Where the head looks next. From a corner it mostly swings straight
416
+ across to the opposite one — top right, stay, bottom left — now and
417
+ then only sideways, or back to the middle for a beat. */
418
+ nextGaze() {
419
+ const r = this.rand;
420
+ const [px, py] = this.gazeDir;
421
+ if (px !== 0 || py !== 0) {
422
+ const p = r();
423
+ if (p < .66) return [-px, -py];
424
+ if (p < .85) return [-px, py];
425
+ return [0, 0];
426
+ }
427
+ const corners = [
428
+ [1, -1],
429
+ [-1, 1],
430
+ [-1, -1],
431
+ [1, 1]
432
+ ];
433
+ return corners[Math.floor(r() * corners.length)];
434
+ }
435
+ /** when the next idle jump is due: `every` seconds, give or take 40 % */
436
+ nextFlip(t, k) {
437
+ const every = this.jump.every;
438
+ return every > 0 ? t + every * k * (.625 + this.rand() * .75) : Infinity;
439
+ }
440
+ /** Advance by `dt` seconds (already scaled by the speed). */
441
+ update(dt) {
442
+ dt = Math.min(dt, .05);
443
+ this.t += dt;
444
+ const t = this.t;
445
+ const p = this.pose;
446
+ const w = p.w;
447
+ /* the state change eases from the weights it started with to the new
448
+ state's on an S-curve: soft start, soft finish, no creeping tail */
449
+ if (this.tr < 1) {
450
+ this.tr = Math.min(1, this.tr + dt / this.trDuration);
451
+ /* a sine ease: no kick at either end of a switch */
452
+ const e = easeSine(this.tr);
453
+ for (let i = 0; i < 3; i++) {
454
+ const target = STATES[i] === this.state ? 1 : 0;
455
+ w[i] = this.wFrom[i] + (target - this.wFrom[i]) * e;
456
+ }
457
+ }
458
+ const [wd, ww, ws] = w;
459
+ /* rest targets, blended */
460
+ const rest = {
461
+ pitch: 0,
462
+ roll: 0,
463
+ y: 0,
464
+ lookX: 0,
465
+ lookY: 0
466
+ };
467
+ for (let i = 0; i < 3; i++) {
468
+ const r = REST[STATES[i]];
469
+ rest.pitch += r.pitch * w[i];
470
+ rest.roll += r.roll * w[i];
471
+ rest.y += r.y * w[i];
472
+ rest.lookX += r.lookX * w[i];
473
+ rest.lookY += r.lookY * w[i];
474
+ }
475
+ /* the idle gaze: a place to look, a while to stay, then the swing to
476
+ the next — the wanderers carry it on their own springs */
477
+ if (this.state === "default" && t >= this.gazeAt) {
478
+ const [gx, gy] = this.nextGaze();
479
+ this.gazeDir = [gx, gy];
480
+ const reach = .84 + this.rand() * .16;
481
+ this.yawW.aim(gx * GAZE_YAW * reach * this.turnK);
482
+ this.pitchW.aim(gy * GAZE_PITCH * reach);
483
+ /* the tilt goes with the turn, so it follows it */
484
+ this.rollW.aim(gx * GAZE_ROLL * reach * this.turnK);
485
+ this.gazeAt = t + GAZE_HOLD_MIN + this.rand() * (GAZE_HOLD_MAX - GAZE_HOLD_MIN);
486
+ }
487
+ /* wander */
488
+ this.yawW.update(t, dt);
489
+ this.pitchW.update(t, dt);
490
+ this.rollW.update(t, dt);
491
+ this.lookXW.update(t, dt);
492
+ this.lookYW.update(t, dt);
493
+ /* following the pointer: the eyes lead, the head turns after them,
494
+ and the wander quietens while it lasts */
495
+ this.ptrS = approach(this.ptrS, this.ptrTargetS, 8, dt);
496
+ this.ptrX = approach(this.ptrX, this.ptrTargetX, 14, dt);
497
+ this.ptrY = approach(this.ptrY, this.ptrTargetY, 14, dt);
498
+ const ps = this.ptrS;
499
+ const quiet = 1 - .75 * ps;
500
+ /* the base pose: the blended rest plus the smoothed wander and the
501
+ pointer's pull — every term is already continuous */
502
+ this.baseYaw = approach(this.baseYaw, this.yawW.value * quiet + 22 * DEG * this.ptrX * ps, 5, dt);
503
+ const basePitch = rest.pitch + this.pitchW.value * quiet - 12 * DEG * this.ptrY * ps;
504
+ const baseRoll = rest.roll + this.rollW.value * quiet;
505
+ const baseY = rest.y;
506
+ const baseLookX = rest.lookX + this.lookXW.value * quiet + 4.5 * this.ptrX * ps;
507
+ const baseLookY = rest.lookY + this.lookYW.value * quiet + 3 * this.ptrY * ps;
508
+ /* ── events ── */
509
+ let spin = 0, hopY = 0, sx = 1, sy = 1, pitchAdd = 0, rollAdd = 0, blinkClose = 0, lookXAdd = 0, lookYAdd = 0, laugh = 0;
510
+ let whirl = 0, whirlAngle = 0;
511
+ /* the whirl: not there until the turn is visibly under way, gone
512
+ before the landing */
513
+ const smooth = (a, b, v) => {
514
+ const x = Math.min(1, Math.max(0, (v - a) / (b - a)));
515
+ return x * x * (3 - 2 * x);
516
+ };
517
+ const envelope = (q) => smooth(.1, .26, q) * (1 - smooth(.66, .9, q));
518
+ /* the ring's own travel: fast and steady from the first frame, with a
519
+ little of the body's own easing on top, so it is moving the moment
520
+ it shows */
521
+ const ringAngle = (q) => TAU * (1.5 * q + .9 * easeInOut(q));
522
+ /* blinks: idle and working blink; a double blink now and then */
523
+ if (t >= this.blinkAt && !this.blink.active && wd + ww > .5) {
524
+ this.blink.fire();
525
+ this.blinkAgain = !this.blinkAgain && this.rand() < .22;
526
+ this.blinkAt = t + (this.blinkAgain ? .28 : 2.2 + this.rand() * 2.6);
527
+ }
528
+ this.blink.update(dt);
529
+ if (this.blink.active) blinkClose = Math.sin(Math.PI * this.blink.p);
530
+ /* eye darts: a quick glance to the side and back, between the slower
531
+ looks — the eyes have a life of their own */
532
+ if (t >= this.dartAt && !this.dart.active && wd + ww > .5) {
533
+ this.dart.fire();
534
+ this.dartX = (this.rand() * 2 - 1) * 4;
535
+ this.dartY = (this.rand() * 2 - 1) * 2;
536
+ this.dart.duration = .25 + this.rand() * .45;
537
+ this.dartAt = t + 1.2 + this.rand() * 2.6;
538
+ }
539
+ this.dart.update(dt);
540
+ if (this.dart.active) {
541
+ const q = this.dart.p;
542
+ /* snap out, hold, snap back */
543
+ const hold = q < .15 ? q / .15 : q > .8 ? (1 - q) / .2 : 1;
544
+ lookXAdd += this.dartX * hold * (wd + ww);
545
+ lookYAdd += this.dartY * hold * (wd + ww);
546
+ }
547
+ /* idle: a full turn now and then, with a jump */
548
+ if (this.state === "default" && t >= this.flipAt && !this.flip.active) {
549
+ this.flipPoked = false;
550
+ this.flip.duration = flipDuration(this.jump, false);
551
+ this.flipSide = this.rand() < .5 ? -1 : 1;
552
+ this.flip.fire();
553
+ this.flipAt = this.nextFlip(t, 1);
554
+ }
555
+ this.flip.update(dt);
556
+ if (this.flip.active) {
557
+ const J = this.jump;
558
+ /* the hop's own phase: below 0 the crouch, above 1 the recovery */
559
+ const preS = flipPre(J, this.flipPoked);
560
+ const a = (this.flip.p * this.flip.duration - preS) / J.time;
561
+ const q = Math.min(1, Math.max(0, a));
562
+ const arc = Math.sin(Math.PI * q);
563
+ /* whole turns, eased in and out, no overshoot to snap back from */
564
+ spin += TAU * J.spin * easeInOut(q);
565
+ hopY -= J.height * arc;
566
+ /* the crouch before take-off; then the landing squash from `land`
567
+ seconds round the moment of contact — before it, the body braces
568
+ for the ground; after it, it gives late */
569
+ const tl = (a - 1) * J.time - J.land;
570
+ const squashTime = this.flipPoked ? J.clickSquashTime : J.squashTime;
571
+ /* the crouch: a click's builds over its squash time, an idle jump's
572
+ is the hop's short bump; then the hop, then the landing squash */
573
+ const crouch = (u) => this.flipPoked ? u * u * (3 - 2 * u) : hopSquash(u - 1);
574
+ /* the landing in three parts: the press down to the deepest squash
575
+ in the squash easing's own shape, the hold on the ground, then
576
+ the rise back to the body's shape — each with its own time and
577
+ easing, and all three meeting at the same depth, so nothing steps */
578
+ const hold = Math.max(0, J.groundTime);
579
+ const peakT = SQUASH_PEAK[J.squashEase] * squashTime;
580
+ const rise = tl - peakT - hold;
581
+ const depth = tl <= peakT ? squashPulse(tl / squashTime, J.squashEase) : rise <= 0 ? groundShape((tl - peakT) / hold, J.groundEase) : risePulse(rise / J.riseTime, J.riseEase);
582
+ const land = (a < 0 ? crouch(Math.max(0, 1 + a * J.time / preS)) : tl > 0 ? depth : a < .2 ? hopSquash(a) : 0) * J.squash;
583
+ sx += .16 * land - .06 * arc * J.stretch;
584
+ sy += -.18 * land + .09 * arc * J.stretch;
585
+ /* lean into it; eyes shut for a turn */
586
+ rollAdd += this.flipSide * J.lean * DEG * arc;
587
+ if (J.spin > 0) {
588
+ laugh = Math.max(laugh, arc);
589
+ whirl = Math.max(whirl, envelope(q));
590
+ whirlAngle = ringAngle(q);
591
+ }
592
+ }
593
+ /* working: hops all the time; every third one spins. Leaving the
594
+ state, the hop under way keeps the strength it had — it flies its
595
+ whole arc, finishes its turn and lands — and that last landing is
596
+ not the rhythm's quick bump but a jump's proper one: the hold on
597
+ the ground and the rise back, from the depth the hop reached. */
598
+ const J = this.jump;
599
+ const exitHold = Math.max(0, J.groundTime);
600
+ const exitFor = exitHold + J.riseTime;
601
+ if (this.state === "working") this.hopGain = ww;
602
+ if (this.hopGain > .02 && (this.state === "working" || this.hopPhase > 0)) {
603
+ this.hopPhase += dt / HOP_T;
604
+ if (this.hopPhase >= 1) {
605
+ if (this.state === "working") {
606
+ this.hopPhase -= 1;
607
+ this.hopCount += 1;
608
+ } else if ((this.hopPhase - 1) * HOP_T >= exitFor) {
609
+ /* the last hop has landed and settled */
610
+ this.hopPhase = 0;
611
+ this.hopGain = 0;
612
+ }
613
+ }
614
+ const g = this.hopGain;
615
+ /* past 1 the body is down and only the landing is still playing */
616
+ const q = Math.min(1, this.hopPhase);
617
+ const arc = Math.sin(Math.PI * q);
618
+ const spinning = this.hopCount % 3 === 2;
619
+ const h = spinning ? HOP_SPIN_H : 18;
620
+ hopY -= h * arc * g;
621
+ /* squash on landing, stretch at the top */
622
+ const exitT = this.state !== "working" && this.hopPhase > 1 ? (this.hopPhase - 1) * HOP_T : -1;
623
+ const land = exitT < 0 ? hopSquash(this.hopPhase) : exitT < exitHold ? groundShape(exitT / exitHold, J.groundEase) : risePulse((exitT - exitHold) / J.riseTime, J.riseEase);
624
+ sx += (.16 * land - .06 * arc) * g;
625
+ sy += (-.18 * land + .09 * arc) * g;
626
+ if (spinning) {
627
+ spin += TAU * easeInOut(q) * g;
628
+ /* eyes shut for the spin */
629
+ laugh = Math.max(laugh, arc * g);
630
+ if (envelope(q) * g > whirl) {
631
+ whirl = envelope(q) * g;
632
+ whirlAngle = ringAngle(q);
633
+ }
634
+ }
635
+ /* lean into each hop, alternating sides */
636
+ rollAdd += (this.hopCount % 2 === 0 ? 1 : -1) * 6 * DEG * arc * g;
637
+ }
638
+ /* working: now and then a laugh shuts the eyes into arcs, then they
639
+ open again */
640
+ if (this.state === "working" && t >= this.laughAt && !this.laughEv.active) {
641
+ this.laughEv.fire();
642
+ this.laughEv.duration = .6 + this.rand() * .5;
643
+ this.laughAt = t + 1.6 + this.rand() * 2.2;
644
+ }
645
+ this.laughEv.update(dt);
646
+ if (this.laughEv.active) {
647
+ const q = this.laughEv.p;
648
+ /* quick shut, hold, quick open */
649
+ laugh = Math.max(laugh, q < .18 ? q / .18 : q > .78 ? (1 - q) / .22 : 1);
650
+ }
651
+ /* sleeping: the head drops, then jerks back up */
652
+ if (this.state === "sleeping" && t >= this.nodAt && !this.nod.active) {
653
+ this.nod.fire();
654
+ this.nodAt = t + 4 + this.rand() * 4;
655
+ }
656
+ this.nod.update(dt);
657
+ if (this.nod.active) {
658
+ const q = this.nod.p;
659
+ /* slow slide down, quick recovery */
660
+ const dip = q < .72 ? easeInOut(q / .72) : 1 - easeInOut((q - .72) / .28);
661
+ pitchAdd -= 13 * DEG * dip * ws;
662
+ }
663
+ /* breathing, always, deeper and slower asleep. The phase is integrated
664
+ so the period can change with the state without the cycle jumping */
665
+ this.breathPhase += dt / (3.6 + 1.2 * ws);
666
+ const breath = Math.sin(this.breathPhase * TAU);
667
+ p.breath = breath;
668
+ sx += breath * (.008 + .014 * ws);
669
+ sy += breath * (.012 + .02 * ws);
670
+ const bob = Math.sin(t * TAU / 3.4) * 2 * (1 - ws);
671
+ /* ── compose ── */
672
+ p.yaw = this.baseYaw + spin;
673
+ /* the jelly: the faster the head turns, the more the body stretches
674
+ along the turn, on a spring that overshoots and settles. A flip's
675
+ spin is left out: that is a jump, and the landing spring handles it. */
676
+ let dyaw = this.baseYaw - this.prevYaw;
677
+ dyaw = ((dyaw + Math.PI) % TAU + TAU) % TAU - Math.PI;
678
+ this.prevYaw = this.baseYaw;
679
+ const rate = dt > 0 ? Math.abs(dyaw) / dt : 0;
680
+ /* the eyes lead a head turn, the way a gaze shift goes: they run
681
+ ahead of it and come back as the head settles */
682
+ const leadTarget = dt > 0 ? Math.max(-2.2, Math.min(2.2, dyaw / dt * 2.4)) : 0;
683
+ this.gazeLead = approach(this.gazeLead, leadTarget, 9, dt);
684
+ const jellyTarget = Math.min(.22, .055 * rate);
685
+ const omega = 16, zeta = .45;
686
+ this.jellyV += (omega * omega * (jellyTarget - this.jelly) - 2 * zeta * omega * this.jellyV) * dt;
687
+ this.jelly += this.jellyV * dt;
688
+ const jelly = Math.max(-.08, Math.min(.28, this.jelly)) * .6;
689
+ sx *= 1 + jelly;
690
+ sy *= 1 - .55 * jelly;
691
+ p.pitch = basePitch + pitchAdd;
692
+ p.roll = baseRoll + rollAdd;
693
+ p.x = 0;
694
+ p.y = baseY + hopY + bob;
695
+ p.sx = sx;
696
+ p.sy = sy;
697
+ /* the lids: the sleeping state closes them through its own weight in
698
+ the renderer, so here the eyes stay open apart from blinks */
699
+ p.eyeOpen = 1;
700
+ p.laugh = approach(p.laugh, laugh, 30, dt);
701
+ p.blinkL = blinkClose;
702
+ p.blinkR = blinkClose;
703
+ p.lookX = baseLookX + lookXAdd + this.gazeLead;
704
+ p.lookY = baseLookY + lookYAdd;
705
+ p.whirl = whirl;
706
+ p.whirlAngle = whirlAngle;
707
+ }
610
708
  }
709
+ /** The still pose of a state, for reduced motion and the first paint. */
611
710
  export function restPose(state) {
612
- const r = REST[state];
613
- return {
614
- yaw: 0,
615
- pitch: r.pitch,
616
- roll: r.roll,
617
- x: 0,
618
- y: r.y,
619
- sx: 1,
620
- sy: 1,
621
- eyeOpen: 1,
622
- blinkL: 0,
623
- blinkR: 0,
624
- lookX: r.lookX,
625
- lookY: r.lookY,
626
- breath: 0,
627
- laugh: 0,
628
- whirl: 0,
629
- whirlAngle: 0,
630
- w: STATES.map((s)=>s === state ? 1 : 0)
631
- };
711
+ const r = REST[state];
712
+ return {
713
+ yaw: 0,
714
+ pitch: r.pitch,
715
+ roll: r.roll,
716
+ x: 0,
717
+ y: r.y,
718
+ sx: 1,
719
+ sy: 1,
720
+ eyeOpen: 1,
721
+ blinkL: 0,
722
+ blinkR: 0,
723
+ lookX: r.lookX,
724
+ lookY: r.lookY,
725
+ breath: 0,
726
+ laugh: 0,
727
+ whirl: 0,
728
+ whirlAngle: 0,
729
+ w: STATES.map((s) => s === state ? 1 : 0)
730
+ };
632
731
  }