@heyramzi/ui 0.0.0-stage → 1.53.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 (564) hide show
  1. package/COMPONENTS.md +133 -0
  2. package/README.md +136 -2
  3. package/package.json +217 -3
  4. package/scripts/lib/sync-core.mjs +113 -0
  5. package/scripts/relative-imports.mjs +39 -0
  6. package/scripts/sf-symbol.mjs +93 -0
  7. package/scripts/sync-shadcn.mjs +354 -0
  8. package/scripts/sync-transitions.mjs +146 -0
  9. package/scripts/validate-apple-symbol-exports.mjs +45 -0
  10. package/src/agents/agent-avatar.svelte +592 -0
  11. package/src/agents/agent-cast.svelte +197 -0
  12. package/src/agents/demo.svelte +125 -0
  13. package/src/agents/helpers.ts +58 -0
  14. package/src/agents/index.ts +11 -0
  15. package/src/agents/page-state.ts +70 -0
  16. package/src/agents/types.ts +74 -0
  17. package/src/agents/vendor/LICENSE +21 -0
  18. package/src/agents/vendor/color.ts +155 -0
  19. package/src/agents/vendor/draw.ts +840 -0
  20. package/src/agents/vendor/engine.ts +845 -0
  21. package/src/agents/vendor/mesh.ts +838 -0
  22. package/src/agents/vendor/plastic.ts +4958 -0
  23. package/src/agents/vendor/presets.ts +103 -0
  24. package/src/agents/vendor/shapes.ts +41 -0
  25. package/src/agents/vendor/ticker.ts +67 -0
  26. package/src/agents/vendor/types.ts +261 -0
  27. package/src/agents/vendor/wear.ts +1699 -0
  28. package/src/ai/ai-button/AiButton.svelte +86 -0
  29. package/src/ai/ai-button/index.ts +4 -0
  30. package/src/ai/confirmation/Confirmation.svelte +54 -0
  31. package/src/ai/confirmation/ConfirmationAccepted.svelte +23 -0
  32. package/src/ai/confirmation/ConfirmationAction.svelte +53 -0
  33. package/src/ai/confirmation/ConfirmationActions.svelte +24 -0
  34. package/src/ai/confirmation/ConfirmationFailed.svelte +23 -0
  35. package/src/ai/confirmation/ConfirmationRejected.svelte +23 -0
  36. package/src/ai/confirmation/ConfirmationRequest.svelte +25 -0
  37. package/src/ai/confirmation/ConfirmationTitle.svelte +25 -0
  38. package/src/ai/confirmation/confirmation-context.svelte.ts +22 -0
  39. package/src/ai/confirmation/index.ts +50 -0
  40. package/src/ai/generation-progress/GenerationProgress.svelte +354 -0
  41. package/src/ai/generation-progress/index.ts +7 -0
  42. package/src/ai/index.ts +111 -0
  43. package/src/ai/loading-state/PixelLoader.svelte +138 -0
  44. package/src/ai/loading-state/index.ts +5 -0
  45. package/src/ai/prompt-input/PromptInput.svelte +56 -0
  46. package/src/ai/prompt-input/PromptInputBody.svelte +19 -0
  47. package/src/ai/prompt-input/PromptInputSubmit.svelte +79 -0
  48. package/src/ai/prompt-input/PromptInputTextarea.svelte +74 -0
  49. package/src/ai/prompt-input/PromptInputToolbar.svelte +19 -0
  50. package/src/ai/prompt-input/index.ts +34 -0
  51. package/src/ai/reasoning/Reasoning.svelte +50 -0
  52. package/src/ai/reasoning/ReasoningContent.svelte +27 -0
  53. package/src/ai/reasoning/ReasoningTrigger.svelte +48 -0
  54. package/src/ai/reasoning/index.ts +22 -0
  55. package/src/ai/reasoning/reasoning-context.svelte.ts +20 -0
  56. package/src/ai/response/Response.css +32 -0
  57. package/src/ai/response/Response.svelte +130 -0
  58. package/src/ai/response/index.ts +5 -0
  59. package/src/ai/suggestion/Suggestion.svelte +43 -0
  60. package/src/ai/suggestion/Suggestions.svelte +25 -0
  61. package/src/ai/suggestion/index.ts +14 -0
  62. package/src/ai/thinking-dots/ThinkingDots.svelte +42 -0
  63. package/src/ai/thinking-dots/index.ts +5 -0
  64. package/src/ai/typing-dots/TypingDots.css +39 -0
  65. package/src/ai/typing-dots/TypingDots.svelte +27 -0
  66. package/src/ai/typing-dots/index.ts +10 -0
  67. package/src/ai/voice-waveform/VoiceWaveform.css +40 -0
  68. package/src/ai/voice-waveform/VoiceWaveform.svelte +46 -0
  69. package/src/ai/voice-waveform/index.ts +5 -0
  70. package/src/ambient.d.ts +4 -0
  71. package/src/animations/README.md +84 -0
  72. package/src/animations/avatar-group-hover.css +47 -0
  73. package/src/animations/card-tilt.css +74 -0
  74. package/src/animations/digit-pop-in.css +78 -0
  75. package/src/animations/dropdown.css +121 -0
  76. package/src/animations/index.css +23 -0
  77. package/src/animations/input-clear-dissolve.css +86 -0
  78. package/src/animations/input-shake.css +114 -0
  79. package/src/animations/learn-more-hover.css +48 -0
  80. package/src/animations/like-button.css +93 -0
  81. package/src/animations/modal.css +75 -0
  82. package/src/animations/page-side-by-side.css +63 -0
  83. package/src/animations/plus-menu-morph.css +97 -0
  84. package/src/animations/resize.css +20 -0
  85. package/src/animations/shimmer-text.css +72 -0
  86. package/src/animations/spinning-counter.css +52 -0
  87. package/src/animations/success-check.css +120 -0
  88. package/src/animations/text-swap.css +40 -0
  89. package/src/animations/text-swap.ts +40 -0
  90. package/src/animations/thinking-states.css +65 -0
  91. package/src/animations/upstream-coverage.test.ts +65 -0
  92. package/src/components/copy/clipboard.test.ts +206 -0
  93. package/src/components/copy/clipboard.ts +49 -0
  94. package/src/components/copy/copied.svelte.ts +39 -0
  95. package/src/components/copy/copy-button.svelte +175 -0
  96. package/src/components/copy/copy-button.test.ts +110 -0
  97. package/src/components/copy/index.ts +3 -0
  98. package/src/components/data-table/data-table.test.ts +134 -0
  99. package/src/components/data-table/data-table.ts +77 -0
  100. package/src/components/data-table/index.ts +20 -0
  101. package/src/components/data-table/infinite-scroll.svelte +69 -0
  102. package/src/components/data-table/pagination.svelte +62 -0
  103. package/src/components/data-table/row-arrow.svelte +28 -0
  104. package/src/components/data-table/row-context-menu.svelte +106 -0
  105. package/src/components/data-table/row-context-menu.test.ts +18 -0
  106. package/src/components/data-table/sortable-head.svelte +56 -0
  107. package/src/components/data-table/table-skeleton.svelte +83 -0
  108. package/src/components/hold-to-confirm/hold-to-confirm.svelte +232 -0
  109. package/src/components/hold-to-confirm/index.ts +1 -0
  110. package/src/components/icon/icon.svelte +23 -0
  111. package/src/components/icon/index.ts +1 -0
  112. package/src/components/language-flag/index.ts +1 -0
  113. package/src/components/language-flag/language-flag.svelte +60 -0
  114. package/src/components/list-page/detail-empty.svelte +50 -0
  115. package/src/components/list-page/detail-header.svelte +107 -0
  116. package/src/components/list-page/detail-panel.svelte +90 -0
  117. package/src/components/list-page/detail-row.svelte +36 -0
  118. package/src/components/list-page/detail-stack.svelte +33 -0
  119. package/src/components/list-page/empty-state.svelte +91 -0
  120. package/src/components/list-page/filter-item.svelte +49 -0
  121. package/src/components/list-page/filter-popover.svelte +88 -0
  122. package/src/components/list-page/filter-section.ts +12 -0
  123. package/src/components/list-page/grouped-list.svelte +116 -0
  124. package/src/components/list-page/index.ts +28 -0
  125. package/src/components/list-page/list-page-shell.svelte +195 -0
  126. package/src/components/list-page/list-table.ts +19 -0
  127. package/src/components/list-page/master-detail.svelte +206 -0
  128. package/src/components/list-page/master-detail.test.ts +37 -0
  129. package/src/components/list-page/notice-bar.svelte +68 -0
  130. package/src/components/list-page/prev-next.svelte +104 -0
  131. package/src/components/list-page/queue-rail.ts +25 -0
  132. package/src/components/list-page/rail-body.svelte +29 -0
  133. package/src/components/list-page/rail-empty.svelte +51 -0
  134. package/src/components/list-page/rail-head.svelte +49 -0
  135. package/src/components/list-page/rail-row.svelte +163 -0
  136. package/src/components/list-page/section-host.ts +24 -0
  137. package/src/components/list-page/section-label.svelte +39 -0
  138. package/src/components/list-page/step.test.ts +21 -0
  139. package/src/components/list-page/step.ts +10 -0
  140. package/src/components/list-page/toolbar-search.svelte +63 -0
  141. package/src/components/list-page/toolbar-trigger.ts +23 -0
  142. package/src/components/list-page/types.ts +1 -0
  143. package/src/components/locale-switch/index.ts +1 -0
  144. package/src/components/locale-switch/locale-switch.svelte +59 -0
  145. package/src/components/mark/index.ts +1 -0
  146. package/src/components/mark/mark.svelte +70 -0
  147. package/src/components/open-in/index.ts +6 -0
  148. package/src/components/open-in/open-in.svelte +106 -0
  149. package/src/components/picker/index.ts +4 -0
  150. package/src/components/picker/mark-picker.svelte +84 -0
  151. package/src/components/picker/picker.svelte +143 -0
  152. package/src/components/picker/trigger.ts +42 -0
  153. package/src/components/picker/types.ts +26 -0
  154. package/src/components/segmented/animated-tabs.svelte +175 -0
  155. package/src/components/segmented/collapse.test.ts +49 -0
  156. package/src/components/segmented/collapse.ts +39 -0
  157. package/src/components/segmented/index.ts +41 -0
  158. package/src/components/segmented/section-runs.svelte +79 -0
  159. package/src/components/segmented/segment.svelte +106 -0
  160. package/src/components/segmented/segmented-filter.svelte +125 -0
  161. package/src/components/segmented/segmented-nav.svelte +82 -0
  162. package/src/components/segmented/segmented-overflow.svelte +85 -0
  163. package/src/components/segmented/segmented-track.svelte +181 -0
  164. package/src/components/segmented/segmented.test.ts +20 -0
  165. package/src/components/segmented/segmented.ts +80 -0
  166. package/src/components/star-rating-input/index.ts +9 -0
  167. package/src/components/star-rating-input/star-rating-input.svelte +256 -0
  168. package/src/components/svg/index.ts +1 -0
  169. package/src/components/svg/svg.svelte +9 -0
  170. package/src/design/index.css +270 -0
  171. package/src/design/polish.css +159 -0
  172. package/src/effects/animated-beam/AnimatedBeam.svelte +132 -0
  173. package/src/effects/animated-beam/diagrams/BeamSvg.svelte +91 -0
  174. package/src/effects/animated-beam/index.ts +2 -0
  175. package/src/effects/animated-beam/path-calculator.svelte.ts +70 -0
  176. package/src/effects/animated-beam/types.ts +23 -0
  177. package/src/effects/border-beam/BorderBeam.svelte +223 -0
  178. package/src/effects/border-beam/css.test.ts +46 -0
  179. package/src/effects/border-beam/css.ts +574 -0
  180. package/src/effects/border-beam/presets.ts +699 -0
  181. package/src/effects/border-beam/register-properties.test.ts +74 -0
  182. package/src/effects/border-beam/register-properties.ts +73 -0
  183. package/src/effects/border-beam/types.ts +38 -0
  184. package/src/effects/image-comparison/ImageComparison.css +100 -0
  185. package/src/effects/image-comparison/ImageComparison.svelte +209 -0
  186. package/src/effects/image-comparison/compare.test.ts +17 -0
  187. package/src/effects/image-comparison/compare.ts +5 -0
  188. package/src/effects/image-reveal/ImageReveal.css +10 -0
  189. package/src/effects/image-reveal/ImageReveal.svelte +357 -0
  190. package/src/effects/image-reveal/index.ts +2 -0
  191. package/src/effects/image-reveal/types.ts +35 -0
  192. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +338 -0
  193. package/src/effects/magic-card/MagicCard.css +47 -0
  194. package/src/effects/magic-card/MagicCard.svelte +57 -0
  195. package/src/effects/magnetic/magnetic.ts +81 -0
  196. package/src/effects/marquee/Marquee.css +78 -0
  197. package/src/effects/marquee/Marquee.svelte +160 -0
  198. package/src/effects/progressive-blur/ProgressiveBlur.css +9 -0
  199. package/src/effects/progressive-blur/ProgressiveBlur.svelte +63 -0
  200. package/src/effects/reveal/AnimatedGroup.svelte +101 -0
  201. package/src/effects/reveal/InView.svelte +77 -0
  202. package/src/effects/reveal/TextEffect.svelte +142 -0
  203. package/src/effects/reveal/index.ts +11 -0
  204. package/src/effects/reveal/observe.test.ts +60 -0
  205. package/src/effects/reveal/observe.ts +65 -0
  206. package/src/effects/reveal/reveal.css +141 -0
  207. package/src/effects/reveal/types.ts +32 -0
  208. package/src/effects/shader-gradient/ShaderGradient.svelte +227 -0
  209. package/src/effects/shader-gradient/color.ts +44 -0
  210. package/src/effects/shader-gradient/index.ts +5 -0
  211. package/src/effects/shader-gradient/renderer.ts +456 -0
  212. package/src/effects/shader-gradient/shaders.ts +324 -0
  213. package/src/effects/shader-gradient/types.ts +68 -0
  214. package/src/effects/spotlight/Spotlight.css +14 -0
  215. package/src/effects/spotlight/Spotlight.svelte +61 -0
  216. package/src/effects/theme-toggle/ThemeToggle.css +214 -0
  217. package/src/effects/theme-toggle/ThemeToggle.svelte +126 -0
  218. package/src/gallery.test.ts +52 -0
  219. package/src/icons/TradeIcon.svelte +16 -0
  220. package/src/icons/apple-symbol-exports.ts +135 -0
  221. package/src/icons/sf-symbols.ts +527 -0
  222. package/src/icons/trades.ts +89 -0
  223. package/src/index.ts +53 -0
  224. package/src/lib/arrive.ts +26 -0
  225. package/src/lib/icon-types.ts +17 -0
  226. package/src/lib/layout.ts +8 -0
  227. package/src/lib/panel.ts +21 -0
  228. package/src/lib/sliding-pill.ts +68 -0
  229. package/src/lib/theme-transition.ts +79 -0
  230. package/src/lib/tone.ts +48 -0
  231. package/src/shadcn/accordion/accordion-content.svelte +22 -0
  232. package/src/shadcn/accordion/accordion-item.svelte +17 -0
  233. package/src/shadcn/accordion/accordion-trigger.svelte +44 -0
  234. package/src/shadcn/accordion/accordion.svelte +19 -0
  235. package/src/shadcn/accordion/index.ts +16 -0
  236. package/src/shadcn/alert/alert-description.svelte +23 -0
  237. package/src/shadcn/alert/alert-title.svelte +20 -0
  238. package/src/shadcn/alert/alert.svelte +46 -0
  239. package/src/shadcn/alert/index.ts +14 -0
  240. package/src/shadcn/alert-dialog/alert-dialog-action.svelte +18 -0
  241. package/src/shadcn/alert-dialog/alert-dialog-cancel.svelte +18 -0
  242. package/src/shadcn/alert-dialog/alert-dialog-content.svelte +29 -0
  243. package/src/shadcn/alert-dialog/alert-dialog-description.svelte +17 -0
  244. package/src/shadcn/alert-dialog/alert-dialog-footer.svelte +20 -0
  245. package/src/shadcn/alert-dialog/alert-dialog-header.svelte +20 -0
  246. package/src/shadcn/alert-dialog/alert-dialog-overlay.svelte +17 -0
  247. package/src/shadcn/alert-dialog/alert-dialog-portal.svelte +7 -0
  248. package/src/shadcn/alert-dialog/alert-dialog-title.svelte +17 -0
  249. package/src/shadcn/alert-dialog/alert-dialog-trigger.svelte +7 -0
  250. package/src/shadcn/alert-dialog/alert-dialog.svelte +7 -0
  251. package/src/shadcn/alert-dialog/index.ts +37 -0
  252. package/src/shadcn/attachment/attachment-action.svelte +23 -0
  253. package/src/shadcn/attachment/attachment-actions.svelte +23 -0
  254. package/src/shadcn/attachment/attachment-content.svelte +23 -0
  255. package/src/shadcn/attachment/attachment-description.svelte +23 -0
  256. package/src/shadcn/attachment/attachment-group.svelte +23 -0
  257. package/src/shadcn/attachment/attachment-media.svelte +44 -0
  258. package/src/shadcn/attachment/attachment-title.svelte +23 -0
  259. package/src/shadcn/attachment/attachment-trigger.svelte +29 -0
  260. package/src/shadcn/attachment/attachment.svelte +55 -0
  261. package/src/shadcn/attachment/index.ts +40 -0
  262. package/src/shadcn/avatar/avatar-badge.svelte +26 -0
  263. package/src/shadcn/avatar/avatar-fallback.svelte +20 -0
  264. package/src/shadcn/avatar/avatar-group-count.svelte +23 -0
  265. package/src/shadcn/avatar/avatar-group.svelte +23 -0
  266. package/src/shadcn/avatar/avatar-image.svelte +17 -0
  267. package/src/shadcn/avatar/avatar.svelte +26 -0
  268. package/src/shadcn/avatar/index.ts +22 -0
  269. package/src/shadcn/badge/badge.svelte +54 -0
  270. package/src/shadcn/badge/index.ts +2 -0
  271. package/src/shadcn/breadcrumb/breadcrumb-ellipsis.svelte +23 -0
  272. package/src/shadcn/breadcrumb/breadcrumb-item.svelte +20 -0
  273. package/src/shadcn/breadcrumb/breadcrumb-link.svelte +31 -0
  274. package/src/shadcn/breadcrumb/breadcrumb-list.svelte +23 -0
  275. package/src/shadcn/breadcrumb/breadcrumb-page.svelte +23 -0
  276. package/src/shadcn/breadcrumb/breadcrumb-separator.svelte +27 -0
  277. package/src/shadcn/breadcrumb/breadcrumb.svelte +21 -0
  278. package/src/shadcn/breadcrumb/index.ts +25 -0
  279. package/src/shadcn/bubble/bubble-content.svelte +31 -0
  280. package/src/shadcn/bubble/bubble-group.svelte +20 -0
  281. package/src/shadcn/bubble/bubble-reactions.svelte +52 -0
  282. package/src/shadcn/bubble/bubble.svelte +58 -0
  283. package/src/shadcn/bubble/index.ts +25 -0
  284. package/src/shadcn/button/button.svelte +121 -0
  285. package/src/shadcn/button/index.ts +17 -0
  286. package/src/shadcn/calendar/calendar-caption.svelte +76 -0
  287. package/src/shadcn/calendar/calendar-cell.svelte +19 -0
  288. package/src/shadcn/calendar/calendar-day.svelte +35 -0
  289. package/src/shadcn/calendar/calendar-grid-body.svelte +12 -0
  290. package/src/shadcn/calendar/calendar-grid-head.svelte +12 -0
  291. package/src/shadcn/calendar/calendar-grid-row.svelte +12 -0
  292. package/src/shadcn/calendar/calendar-grid.svelte +16 -0
  293. package/src/shadcn/calendar/calendar-head-cell.svelte +19 -0
  294. package/src/shadcn/calendar/calendar-header.svelte +19 -0
  295. package/src/shadcn/calendar/calendar-heading.svelte +16 -0
  296. package/src/shadcn/calendar/calendar-month-select.svelte +44 -0
  297. package/src/shadcn/calendar/calendar-month.svelte +15 -0
  298. package/src/shadcn/calendar/calendar-months.svelte +19 -0
  299. package/src/shadcn/calendar/calendar-nav.svelte +19 -0
  300. package/src/shadcn/calendar/calendar-next-button.svelte +31 -0
  301. package/src/shadcn/calendar/calendar-prev-button.svelte +31 -0
  302. package/src/shadcn/calendar/calendar-year-select.svelte +43 -0
  303. package/src/shadcn/calendar/calendar.svelte +124 -0
  304. package/src/shadcn/calendar/index.ts +40 -0
  305. package/src/shadcn/card/card-action.svelte +23 -0
  306. package/src/shadcn/card/card-content.svelte +20 -0
  307. package/src/shadcn/card/card-description.svelte +20 -0
  308. package/src/shadcn/card/card-footer.svelte +23 -0
  309. package/src/shadcn/card/card-header.svelte +23 -0
  310. package/src/shadcn/card/card-title.svelte +29 -0
  311. package/src/shadcn/card/card.svelte +25 -0
  312. package/src/shadcn/card/index.ts +25 -0
  313. package/src/shadcn/checkbox/checkbox.svelte +38 -0
  314. package/src/shadcn/checkbox/index.ts +6 -0
  315. package/src/shadcn/code-block/code-block-actions.svelte +23 -0
  316. package/src/shadcn/code-block/code-block-content.svelte +55 -0
  317. package/src/shadcn/code-block/code-block-copy-button.svelte +10 -0
  318. package/src/shadcn/code-block/code-block-footer.svelte +20 -0
  319. package/src/shadcn/code-block/code-block-header.svelte +23 -0
  320. package/src/shadcn/code-block/code-block-line.svelte +71 -0
  321. package/src/shadcn/code-block/code-block-title.svelte +23 -0
  322. package/src/shadcn/code-block/code-block.svelte +47 -0
  323. package/src/shadcn/code-block/index.ts +42 -0
  324. package/src/shadcn/collapsible/collapsible-content.svelte +7 -0
  325. package/src/shadcn/collapsible/collapsible-trigger.svelte +7 -0
  326. package/src/shadcn/collapsible/collapsible.svelte +11 -0
  327. package/src/shadcn/collapsible/index.ts +13 -0
  328. package/src/shadcn/command/command-dialog.svelte +66 -0
  329. package/src/shadcn/command/command-empty.svelte +17 -0
  330. package/src/shadcn/command/command-group.svelte +40 -0
  331. package/src/shadcn/command/command-input.svelte +64 -0
  332. package/src/shadcn/command/command-item.svelte +37 -0
  333. package/src/shadcn/command/command-link-item.svelte +20 -0
  334. package/src/shadcn/command/command-list.svelte +20 -0
  335. package/src/shadcn/command/command-loading.svelte +7 -0
  336. package/src/shadcn/command/command-separator.svelte +17 -0
  337. package/src/shadcn/command/command-shortcut.svelte +23 -0
  338. package/src/shadcn/command/command.svelte +28 -0
  339. package/src/shadcn/command/index.ts +40 -0
  340. package/src/shadcn/context-menu/context-menu-checkbox-item.svelte +41 -0
  341. package/src/shadcn/context-menu/context-menu-content.svelte +28 -0
  342. package/src/shadcn/context-menu/context-menu-group-heading.svelte +21 -0
  343. package/src/shadcn/context-menu/context-menu-group.svelte +7 -0
  344. package/src/shadcn/context-menu/context-menu-item.svelte +27 -0
  345. package/src/shadcn/context-menu/context-menu-label.svelte +27 -0
  346. package/src/shadcn/context-menu/context-menu-portal.svelte +7 -0
  347. package/src/shadcn/context-menu/context-menu-radio-group.svelte +16 -0
  348. package/src/shadcn/context-menu/context-menu-radio-item.svelte +35 -0
  349. package/src/shadcn/context-menu/context-menu-separator.svelte +17 -0
  350. package/src/shadcn/context-menu/context-menu-shortcut.svelte +23 -0
  351. package/src/shadcn/context-menu/context-menu-sub-content.svelte +25 -0
  352. package/src/shadcn/context-menu/context-menu-sub-trigger.svelte +29 -0
  353. package/src/shadcn/context-menu/context-menu-sub.svelte +7 -0
  354. package/src/shadcn/context-menu/context-menu-trigger.svelte +17 -0
  355. package/src/shadcn/context-menu/context-menu.svelte +7 -0
  356. package/src/shadcn/context-menu/index.ts +52 -0
  357. package/src/shadcn/dialog/dialog-close.svelte +11 -0
  358. package/src/shadcn/dialog/dialog-content.svelte +57 -0
  359. package/src/shadcn/dialog/dialog-description.svelte +20 -0
  360. package/src/shadcn/dialog/dialog-footer.svelte +32 -0
  361. package/src/shadcn/dialog/dialog-header.svelte +20 -0
  362. package/src/shadcn/dialog/dialog-overlay.svelte +20 -0
  363. package/src/shadcn/dialog/dialog-portal.svelte +7 -0
  364. package/src/shadcn/dialog/dialog-title.svelte +17 -0
  365. package/src/shadcn/dialog/dialog-trigger.svelte +11 -0
  366. package/src/shadcn/dialog/dialog.svelte +7 -0
  367. package/src/shadcn/dialog/index.ts +34 -0
  368. package/src/shadcn/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
  369. package/src/shadcn/dropdown-menu/dropdown-menu-checkbox-item.svelte +43 -0
  370. package/src/shadcn/dropdown-menu/dropdown-menu-content.svelte +31 -0
  371. package/src/shadcn/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
  372. package/src/shadcn/dropdown-menu/dropdown-menu-group.svelte +7 -0
  373. package/src/shadcn/dropdown-menu/dropdown-menu-item.svelte +29 -0
  374. package/src/shadcn/dropdown-menu/dropdown-menu-label.svelte +27 -0
  375. package/src/shadcn/dropdown-menu/dropdown-menu-portal.svelte +7 -0
  376. package/src/shadcn/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
  377. package/src/shadcn/dropdown-menu/dropdown-menu-radio-item.svelte +34 -0
  378. package/src/shadcn/dropdown-menu/dropdown-menu-separator.svelte +17 -0
  379. package/src/shadcn/dropdown-menu/dropdown-menu-shortcut.svelte +23 -0
  380. package/src/shadcn/dropdown-menu/dropdown-menu-sub-content.svelte +20 -0
  381. package/src/shadcn/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
  382. package/src/shadcn/dropdown-menu/dropdown-menu-sub.svelte +7 -0
  383. package/src/shadcn/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
  384. package/src/shadcn/dropdown-menu/dropdown-menu.svelte +7 -0
  385. package/src/shadcn/dropdown-menu/index.ts +56 -0
  386. package/src/shadcn/dropdown-menu/menu-item.ts +43 -0
  387. package/src/shadcn/empty/empty-content.svelte +23 -0
  388. package/src/shadcn/empty/empty-description.svelte +23 -0
  389. package/src/shadcn/empty/empty-header.svelte +20 -0
  390. package/src/shadcn/empty/empty-media.svelte +41 -0
  391. package/src/shadcn/empty/empty-title.svelte +20 -0
  392. package/src/shadcn/empty/empty.svelte +23 -0
  393. package/src/shadcn/empty/index.ts +22 -0
  394. package/src/shadcn/field/field-content.svelte +20 -0
  395. package/src/shadcn/field/field-description.svelte +25 -0
  396. package/src/shadcn/field/field-error.svelte +58 -0
  397. package/src/shadcn/field/field-group.svelte +23 -0
  398. package/src/shadcn/field/field-label.svelte +26 -0
  399. package/src/shadcn/field/field-legend.svelte +29 -0
  400. package/src/shadcn/field/field-separator.svelte +35 -0
  401. package/src/shadcn/field/field-set.svelte +24 -0
  402. package/src/shadcn/field/field-title.svelte +23 -0
  403. package/src/shadcn/field/field.svelte +53 -0
  404. package/src/shadcn/field/index.ts +36 -0
  405. package/src/shadcn/field/simple-field.svelte +71 -0
  406. package/src/shadcn/field/simple-field.test.ts +47 -0
  407. package/src/shadcn/hooks/highlighter.ts +132 -0
  408. package/src/shadcn/hooks/is-mobile.svelte.ts +9 -0
  409. package/src/shadcn/hooks/use-highlighted-code.svelte.ts +98 -0
  410. package/src/shadcn/hover-card/hover-card-content.svelte +31 -0
  411. package/src/shadcn/hover-card/hover-card-portal.svelte +7 -0
  412. package/src/shadcn/hover-card/hover-card-trigger.svelte +7 -0
  413. package/src/shadcn/hover-card/hover-card.svelte +7 -0
  414. package/src/shadcn/hover-card/index.ts +15 -0
  415. package/src/shadcn/input/index.ts +9 -0
  416. package/src/shadcn/input/input.svelte +48 -0
  417. package/src/shadcn/input/secret-input.svelte +75 -0
  418. package/src/shadcn/input-otp/index.ts +15 -0
  419. package/src/shadcn/input-otp/input-otp-group.svelte +20 -0
  420. package/src/shadcn/input-otp/input-otp-separator.svelte +25 -0
  421. package/src/shadcn/input-otp/input-otp-slot.svelte +26 -0
  422. package/src/shadcn/input-otp/input-otp.svelte +22 -0
  423. package/src/shadcn/kbd/index.ts +10 -0
  424. package/src/shadcn/kbd/kbd-group.svelte +20 -0
  425. package/src/shadcn/kbd/kbd.svelte +23 -0
  426. package/src/shadcn/label/index.ts +7 -0
  427. package/src/shadcn/label/label.svelte +20 -0
  428. package/src/shadcn/marker/index.ts +15 -0
  429. package/src/shadcn/marker/marker-content.svelte +23 -0
  430. package/src/shadcn/marker/marker-icon.svelte +21 -0
  431. package/src/shadcn/marker/marker.svelte +52 -0
  432. package/src/shadcn/message/index.ts +22 -0
  433. package/src/shadcn/message/message-avatar.svelte +23 -0
  434. package/src/shadcn/message/message-content.svelte +23 -0
  435. package/src/shadcn/message/message-footer.svelte +23 -0
  436. package/src/shadcn/message/message-group.svelte +20 -0
  437. package/src/shadcn/message/message-header.svelte +23 -0
  438. package/src/shadcn/message/message.svelte +27 -0
  439. package/src/shadcn/message-scroller/index.ts +28 -0
  440. package/src/shadcn/message-scroller/message-scroller-button.svelte +52 -0
  441. package/src/shadcn/message-scroller/message-scroller-content.svelte +17 -0
  442. package/src/shadcn/message-scroller/message-scroller-item.svelte +22 -0
  443. package/src/shadcn/message-scroller/message-scroller-provider.svelte +7 -0
  444. package/src/shadcn/message-scroller/message-scroller-viewport.svelte +20 -0
  445. package/src/shadcn/message-scroller/message-scroller.svelte +20 -0
  446. package/src/shadcn/popover/index.ts +28 -0
  447. package/src/shadcn/popover/popover-close.svelte +7 -0
  448. package/src/shadcn/popover/popover-content.svelte +38 -0
  449. package/src/shadcn/popover/popover-description.svelte +20 -0
  450. package/src/shadcn/popover/popover-header.svelte +20 -0
  451. package/src/shadcn/popover/popover-portal.svelte +7 -0
  452. package/src/shadcn/popover/popover-title.svelte +15 -0
  453. package/src/shadcn/popover/popover-trigger.svelte +17 -0
  454. package/src/shadcn/popover/popover.svelte +7 -0
  455. package/src/shadcn/progress/index.ts +7 -0
  456. package/src/shadcn/progress/progress.svelte +30 -0
  457. package/src/shadcn/radio-group/index.ts +10 -0
  458. package/src/shadcn/radio-group/radio-group-item.svelte +32 -0
  459. package/src/shadcn/radio-group/radio-group.svelte +19 -0
  460. package/src/shadcn/scroll-area/index.ts +10 -0
  461. package/src/shadcn/scroll-area/scroll-area-scrollbar.svelte +30 -0
  462. package/src/shadcn/scroll-area/scroll-area.svelte +43 -0
  463. package/src/shadcn/select/index.ts +37 -0
  464. package/src/shadcn/select/select-content.svelte +45 -0
  465. package/src/shadcn/select/select-group-heading.svelte +21 -0
  466. package/src/shadcn/select/select-group.svelte +17 -0
  467. package/src/shadcn/select/select-item.svelte +38 -0
  468. package/src/shadcn/select/select-label.svelte +20 -0
  469. package/src/shadcn/select/select-portal.svelte +7 -0
  470. package/src/shadcn/select/select-scroll-down-button.svelte +23 -0
  471. package/src/shadcn/select/select-scroll-up-button.svelte +23 -0
  472. package/src/shadcn/select/select-separator.svelte +18 -0
  473. package/src/shadcn/select/select-trigger.svelte +33 -0
  474. package/src/shadcn/select/select.svelte +11 -0
  475. package/src/shadcn/separator/index.ts +7 -0
  476. package/src/shadcn/separator/separator.svelte +23 -0
  477. package/src/shadcn/sheet/index.ts +34 -0
  478. package/src/shadcn/sheet/sheet-close.svelte +7 -0
  479. package/src/shadcn/sheet/sheet-content.svelte +73 -0
  480. package/src/shadcn/sheet/sheet-description.svelte +17 -0
  481. package/src/shadcn/sheet/sheet-footer.svelte +20 -0
  482. package/src/shadcn/sheet/sheet-header.svelte +20 -0
  483. package/src/shadcn/sheet/sheet-overlay.svelte +20 -0
  484. package/src/shadcn/sheet/sheet-portal.svelte +7 -0
  485. package/src/shadcn/sheet/sheet-title.svelte +17 -0
  486. package/src/shadcn/sheet/sheet-trigger.svelte +7 -0
  487. package/src/shadcn/sheet/sheet.css +48 -0
  488. package/src/shadcn/sheet/sheet.svelte +7 -0
  489. package/src/shadcn/sidebar/constants.ts +6 -0
  490. package/src/shadcn/sidebar/context.svelte.ts +81 -0
  491. package/src/shadcn/sidebar/index.ts +75 -0
  492. package/src/shadcn/sidebar/sidebar-content.svelte +24 -0
  493. package/src/shadcn/sidebar/sidebar-footer.svelte +21 -0
  494. package/src/shadcn/sidebar/sidebar-group-action.svelte +33 -0
  495. package/src/shadcn/sidebar/sidebar-group-content.svelte +21 -0
  496. package/src/shadcn/sidebar/sidebar-group-label.svelte +33 -0
  497. package/src/shadcn/sidebar/sidebar-group.svelte +21 -0
  498. package/src/shadcn/sidebar/sidebar-header.svelte +21 -0
  499. package/src/shadcn/sidebar/sidebar-input.svelte +21 -0
  500. package/src/shadcn/sidebar/sidebar-inset.svelte +23 -0
  501. package/src/shadcn/sidebar/sidebar-menu-action.svelte +37 -0
  502. package/src/shadcn/sidebar/sidebar-menu-badge.svelte +24 -0
  503. package/src/shadcn/sidebar/sidebar-menu-button.svelte +103 -0
  504. package/src/shadcn/sidebar/sidebar-menu-item.svelte +21 -0
  505. package/src/shadcn/sidebar/sidebar-menu-skeleton.svelte +36 -0
  506. package/src/shadcn/sidebar/sidebar-menu-sub-button.svelte +39 -0
  507. package/src/shadcn/sidebar/sidebar-menu-sub-item.svelte +21 -0
  508. package/src/shadcn/sidebar/sidebar-menu-sub.svelte +24 -0
  509. package/src/shadcn/sidebar/sidebar-menu.svelte +21 -0
  510. package/src/shadcn/sidebar/sidebar-provider.svelte +62 -0
  511. package/src/shadcn/sidebar/sidebar-rail.svelte +36 -0
  512. package/src/shadcn/sidebar/sidebar-separator.svelte +19 -0
  513. package/src/shadcn/sidebar/sidebar-trigger.svelte +37 -0
  514. package/src/shadcn/sidebar/sidebar.svelte +109 -0
  515. package/src/shadcn/skeleton/index.ts +7 -0
  516. package/src/shadcn/skeleton/skeleton.svelte +17 -0
  517. package/src/shadcn/slider/index.ts +7 -0
  518. package/src/shadcn/slider/slider.svelte +52 -0
  519. package/src/shadcn/sonner/index.ts +1 -0
  520. package/src/shadcn/sonner/sonner.svelte +31 -0
  521. package/src/shadcn/spinner/index.ts +1 -0
  522. package/src/shadcn/spinner/spinner.svelte +26 -0
  523. package/src/shadcn/switch/index.ts +7 -0
  524. package/src/shadcn/switch/switch.svelte +31 -0
  525. package/src/shadcn/table/index.ts +28 -0
  526. package/src/shadcn/table/table-body.svelte +20 -0
  527. package/src/shadcn/table/table-caption.svelte +20 -0
  528. package/src/shadcn/table/table-cell.svelte +20 -0
  529. package/src/shadcn/table/table-footer.svelte +20 -0
  530. package/src/shadcn/table/table-head.svelte +23 -0
  531. package/src/shadcn/table/table-header.svelte +20 -0
  532. package/src/shadcn/table/table-row.svelte +23 -0
  533. package/src/shadcn/table/table.svelte +25 -0
  534. package/src/shadcn/tabs/index.ts +16 -0
  535. package/src/shadcn/tabs/tabs-content.svelte +17 -0
  536. package/src/shadcn/tabs/tabs-list.svelte +36 -0
  537. package/src/shadcn/tabs/tabs-trigger.svelte +20 -0
  538. package/src/shadcn/tabs/tabs.svelte +19 -0
  539. package/src/shadcn/textarea/index.ts +7 -0
  540. package/src/shadcn/textarea/textarea.svelte +23 -0
  541. package/src/shadcn/toggle/index.ts +13 -0
  542. package/src/shadcn/toggle/toggle.svelte +53 -0
  543. package/src/shadcn/toggle-group/index.ts +10 -0
  544. package/src/shadcn/toggle-group/toggle-group-item.svelte +35 -0
  545. package/src/shadcn/toggle-group/toggle-group.svelte +66 -0
  546. package/src/shadcn/tooltip/context.ts +16 -0
  547. package/src/shadcn/tooltip/index.ts +19 -0
  548. package/src/shadcn/tooltip/tooltip-content.svelte +38 -0
  549. package/src/shadcn/tooltip/tooltip-portal.svelte +7 -0
  550. package/src/shadcn/tooltip/tooltip-provider.svelte +16 -0
  551. package/src/shadcn/tooltip/tooltip-trigger.svelte +7 -0
  552. package/src/shadcn/tooltip/tooltip.svelte +19 -0
  553. package/src/shadcn/trace/index.ts +29 -0
  554. package/src/shadcn/trace/trace-collapsible-content.svelte +29 -0
  555. package/src/shadcn/trace/trace-collapsible-trigger.svelte +29 -0
  556. package/src/shadcn/trace/trace-collapsible.svelte +20 -0
  557. package/src/shadcn/trace/trace-description.svelte +20 -0
  558. package/src/shadcn/trace/trace-icon.svelte +24 -0
  559. package/src/shadcn/trace/trace-line.svelte +31 -0
  560. package/src/shadcn/trace/trace-title.svelte +20 -0
  561. package/src/shadcn/trace/trace.svelte +23 -0
  562. package/src/shadcn/utilities.css +549 -0
  563. package/src/test-setup.ts +34 -0
  564. package/src/tokens.css +993 -0
@@ -0,0 +1,1699 @@
1
+ // @ts-nocheck WHY: vendored engine, kept as upstream wrote it so a re-sync stays a clean diff. Strict flags in consumers (review) would flag ~870 spots.
2
+ /* wear.ts — things the bots can wear: a hat, glasses, headphones, a bow
3
+ tie. Each is a small 3D object built from lit surfaces (see mesh.ts) and
4
+ placed on the body's own geometry — the top of the head, the sides, the
5
+ eyes, the front below the face — then projected through the same
6
+ rotation as the slice stack, so it turns, tips and flips with the head
7
+ instead of floating over it. Materials follow the body's light: felt and
8
+ knit, paper and foil, gold with cut stones and pearls, leather and soft
9
+ plastic with brushed metal, satin, acetate and glass. Each casts a soft
10
+ shadow where it sits on the body.
11
+
12
+ Everything is drawn in body space (the context carries the body's
13
+ transform when these run); the glasses in face space. */
14
+
15
+ import type { BotAvatarGlasses, BotAvatarHat } from "./types";
16
+ import { rasterize, PAD, SPAN } from "./plastic";
17
+ import { shade, parseColor } from "./color";
18
+ import {
19
+ makeMesh,
20
+ makeTube,
21
+ makeView,
22
+ projectMesh,
23
+ section,
24
+ paintLayers,
25
+ boundsOf,
26
+ hull,
27
+ norm,
28
+ cross,
29
+ sub,
30
+ segs,
31
+ felt,
32
+ velvet,
33
+ paper,
34
+ plastic,
35
+ satin,
36
+ gold,
37
+ silver,
38
+ metal,
39
+ sphereSprite,
40
+ cutGem,
41
+ scale,
42
+ type Layer,
43
+ type Mesh,
44
+ type Proj,
45
+ type RGB,
46
+ type View,
47
+ type V3,
48
+ type P2,
49
+ } from "./mesh";
50
+
51
+ export interface Wear {
52
+ hat: BotAvatarHat;
53
+ glasses: BotAvatarGlasses;
54
+ headphones: boolean;
55
+ bowTie: boolean;
56
+ /** the hat's, the headphones' and the bow tie's colour */
57
+ color: string;
58
+ /** the body is plush: the bow tie is the same felt rather than satin */
59
+ fabric?: boolean;
60
+ }
61
+
62
+ /** The rig as the slices use it: floored cos/sin of yaw and pitch, the
63
+ unfloored facing, the body's half-depth, and the light on screen. */
64
+ export interface WearRig {
65
+ cy: number;
66
+ sy: number;
67
+ cp: number;
68
+ sp: number;
69
+ facing: number;
70
+ halfDepth: number;
71
+ lx: number;
72
+ ly: number;
73
+ }
74
+
75
+ /* ── the outline's landmarks, once per shape ────────────────────────── */
76
+
77
+ interface Marks {
78
+ /** the top of the head over its middle, and the head's width a little below it */
79
+ top: number;
80
+ topL: number;
81
+ topR: number;
82
+ /** the widest row between a third and two thirds down, and its ends */
83
+ mid: number;
84
+ left: number;
85
+ right: number;
86
+ /** the bottom under the middle */
87
+ bottom: number;
88
+ /** each row's width, top to bottom, at `step` design units a row */
89
+ widths: Float32Array;
90
+ step: number;
91
+ /** the outline's convex hull, for what rests on the head */
92
+ hull: P2[];
93
+ }
94
+
95
+ /* where a hat sits: the first row, from the top, where the head is at
96
+ least `frac` of its full width — a narrow top (a star's point, a
97
+ triangle's apex) seats a hat lower than a round one */
98
+ function seat(m: Marks, frac: number): number {
99
+ const W = m.right - m.left;
100
+ for (let i = 0; i < m.widths.length; i++) if (m.widths[i] >= frac * W) return i * m.step - PAD;
101
+ return m.top;
102
+ }
103
+ const marks = new WeakMap<Path2D, Marks>();
104
+ function marksFor(path: Path2D): Marks | null {
105
+ const hit = marks.get(path);
106
+ if (hit) return hit;
107
+ const N = 128;
108
+ const cov = rasterize(path, N);
109
+ if (!cov) return null;
110
+ const u = SPAN / N;
111
+ const toX = (i: number) => (i + 0.5) * u - PAD;
112
+ const inside = (x: number, y: number) => cov[y * N + x] >= 128;
113
+ const c0 = Math.floor((42 + PAD) / u),
114
+ c1 = Math.ceil((58 + PAD) / u);
115
+ let top = N,
116
+ bottom = -1;
117
+ for (let x = c0; x <= c1; x++) {
118
+ for (let y = 0; y < N; y++)
119
+ if (inside(x, y)) {
120
+ if (y < top) top = y;
121
+ break;
122
+ }
123
+ for (let y = N - 1; y >= 0; y--)
124
+ if (inside(x, y)) {
125
+ if (y > bottom) bottom = y;
126
+ break;
127
+ }
128
+ }
129
+ const row = (y: number): [number, number] => {
130
+ let l = -1,
131
+ r = -1;
132
+ for (let x = 0; x < N; x++)
133
+ if (inside(x, y)) {
134
+ if (l < 0) l = x;
135
+ r = x;
136
+ }
137
+ return [l, r];
138
+ };
139
+ const yT = Math.min(N - 1, top + Math.round(8 / u));
140
+ const [tl, tr] = row(yT);
141
+ let best = -1,
142
+ bl = 0,
143
+ br = 0,
144
+ by = 0;
145
+ for (let y = Math.round((33 + PAD) / u); y <= Math.round((67 + PAD) / u); y++) {
146
+ const [l, r] = row(y);
147
+ if (l >= 0 && r - l > best) {
148
+ best = r - l;
149
+ bl = l;
150
+ br = r;
151
+ by = y;
152
+ }
153
+ }
154
+ const widths = new Float32Array(N);
155
+ const edge: P2[] = [];
156
+ for (let y = 0; y < N; y++) {
157
+ const [l, r] = row(y);
158
+ widths[y] = l < 0 ? 0 : (r - l + 1) * u;
159
+ if (l >= 0) edge.push([toX(l) - 0.5 * u, toX(y)], [toX(r) + 0.5 * u, toX(y)]);
160
+ }
161
+ const m: Marks = {
162
+ top: toX(top) - 0.5 * u,
163
+ topL: toX(tl),
164
+ topR: toX(tr),
165
+ mid: toX(by),
166
+ left: toX(bl) - 0.5 * u,
167
+ right: toX(br) + 0.5 * u,
168
+ bottom: toX(bottom) + 0.5 * u,
169
+ widths,
170
+ step: u,
171
+ hull: hull(edge),
172
+ };
173
+ marks.set(path, m);
174
+ return m;
175
+ }
176
+
177
+ /* ── projection ─────────────────────────────────────────────────────── */
178
+
179
+ /* the slices' orthographic turn: design (x, y) about the centre, z along
180
+ the depth in design units (positive toward the front cap) */
181
+ function project(r: WearRig, p: V3): P2 {
182
+ const X = p[0] - 50,
183
+ Y = p[1] - 50;
184
+ return [r.cy * X + r.sy * p[2], r.sy * r.sp * X + r.cp * Y - r.cy * r.sp * p[2]];
185
+ }
186
+ /* how far a point sits toward the viewer */
187
+ function depthOf(r: WearRig, p: V3): number {
188
+ return -r.sy * r.cp * (p[0] - 50) + r.sp * (p[1] - 50) + r.cy * r.cp * p[2];
189
+ }
190
+
191
+ /* rotate a local offset by a tilt about the depth axis, then a lean about x */
192
+ function tilt(v: V3, roll: number, lean: number): V3 {
193
+ const cr = Math.cos(roll),
194
+ sr = Math.sin(roll);
195
+ const x = v[0] * cr - v[1] * sr,
196
+ y = v[0] * sr + v[1] * cr;
197
+ const cl = Math.cos(lean),
198
+ sl = Math.sin(lean);
199
+ return [x, y * cl - v[2] * sl, y * sl + v[2] * cl];
200
+ }
201
+ const add = (a: V3, b: V3): V3 => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
202
+ const along = (a: V3, d: V3, k: number): V3 => [a[0] + d[0] * k, a[1] + d[1] * k, a[2] + d[2] * k];
203
+
204
+ function polyPath(pts: P2[], close = true): Path2D {
205
+ const p = new Path2D();
206
+ pts.forEach((q, i) => (i ? p.lineTo(q[0], q[1]) : p.moveTo(q[0], q[1])));
207
+ if (close) p.closePath();
208
+ return p;
209
+ }
210
+ function bounds(pts: P2[]) {
211
+ let x0 = Infinity,
212
+ y0 = Infinity,
213
+ x1 = -Infinity,
214
+ y1 = -Infinity;
215
+ for (const [x, y] of pts) {
216
+ if (x < x0) x0 = x;
217
+ if (y < y0) y0 = y;
218
+ if (x > x1) x1 = x;
219
+ if (y > y1) y1 = y;
220
+ }
221
+ return { x0, y0, x1, y1, cx: (x0 + x1) / 2, cy: (y0 + y1) / 2, w: x1 - x0, h: y1 - y0 };
222
+ }
223
+
224
+ /* a stable pseudo-random per index, so fuzz does not shimmer frame to frame */
225
+ const rnd = (i: number, k = 0) => {
226
+ let h = (i * 2654435761 + k * 40503) | 0;
227
+ h = Math.imul(h ^ (h >>> 15), 2246822507);
228
+ h = Math.imul(h ^ (h >>> 13), 3266489909);
229
+ return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
230
+ };
231
+
232
+ /* ── texture and small touches ──────────────────────────────────────── */
233
+
234
+ /* felt's grain: a small tile of soft noise, multiplied over felt faintly */
235
+ let grain: HTMLCanvasElement | OffscreenCanvas | null = null;
236
+ function grainTile(): HTMLCanvasElement | OffscreenCanvas | null {
237
+ if (grain) return grain;
238
+ const n = 48;
239
+ const c =
240
+ typeof OffscreenCanvas === "function"
241
+ ? new OffscreenCanvas(n, n)
242
+ : typeof document !== "undefined"
243
+ ? Object.assign(document.createElement("canvas"), { width: n, height: n })
244
+ : null;
245
+ const g = c && (c.getContext("2d") as CanvasRenderingContext2D | null);
246
+ if (!c || !g) return null;
247
+ const img = g.createImageData(n, n);
248
+ for (let i = 0; i < n * n; i++) {
249
+ /* short diagonal fibres: noise smeared along one direction */
250
+ const x = i % n,
251
+ y = (i / n) | 0;
252
+ let v = 0;
253
+ for (let k = -2; k <= 2; k++) v += rnd(((x + k + n) % n) + ((y + k + n) % n) * n, 5);
254
+ v /= 5;
255
+ const m = Math.round(255 * (0.82 + 0.36 * v));
256
+ img.data[i * 4] = img.data[i * 4 + 1] = img.data[i * 4 + 2] = Math.min(255, m);
257
+ img.data[i * 4 + 3] = 255;
258
+ }
259
+ g.putImageData(img, 0, 0);
260
+ grain = c;
261
+ return c;
262
+ }
263
+
264
+ /* the grain, laid over a painted outline; its pattern made once per
265
+ context and scale */
266
+ const patterns = new WeakMap<CanvasRenderingContext2D, Map<number, CanvasPattern | null>>();
267
+ function grainOver(ctx: CanvasRenderingContext2D, outline: P2[], alpha: number, scaleK = 0.18) {
268
+ const tile = grainTile();
269
+ if (!tile || outline.length < 3) return;
270
+ let byScale = patterns.get(ctx);
271
+ if (!byScale) patterns.set(ctx, (byScale = new Map()));
272
+ let pat = byScale.get(scaleK);
273
+ if (pat === undefined) {
274
+ pat = ctx.createPattern(tile as HTMLCanvasElement, "repeat");
275
+ pat?.setTransform?.(new DOMMatrix().scale(scaleK));
276
+ byScale.set(scaleK, pat);
277
+ }
278
+ if (!pat) return;
279
+ const b = bounds(outline);
280
+ ctx.save();
281
+ ctx.clip(polyPath(outline));
282
+ ctx.globalCompositeOperation = "multiply";
283
+ ctx.globalAlpha = alpha;
284
+ ctx.fillStyle = pat;
285
+ ctx.fillRect(b.x0 - 1, b.y0 - 1, b.w + 2, b.h + 2);
286
+ ctx.restore();
287
+ }
288
+
289
+ /* short hairs standing off an outline: a felt or pile edge. Two batched
290
+ strokes, a lighter and a darker set, rather than one call a hair. */
291
+ function fuzz(
292
+ ctx: CanvasRenderingContext2D,
293
+ outline: P2[],
294
+ base: string,
295
+ seed: number,
296
+ len: number,
297
+ px = 2.4,
298
+ ) {
299
+ const n = outline.length;
300
+ if (n < 3) return;
301
+ let total = 0;
302
+ const seg: number[] = [];
303
+ for (let i = 0; i < n; i++) {
304
+ const a = outline[i],
305
+ c = outline[(i + 1) % n];
306
+ const d = Math.hypot(c[0] - a[0], c[1] - a[1]);
307
+ seg.push(d);
308
+ total += d;
309
+ }
310
+ /* about a hair every other device pixel along the edge */
311
+ const count = Math.min(360, Math.max(24, Math.round(total * Math.min(2.4, px * 0.5))));
312
+ const b = bounds(outline);
313
+ const lightP = new Path2D(),
314
+ darkP = new Path2D();
315
+ let si = 0,
316
+ acc = 0;
317
+ for (let k = 0; k < count; k++) {
318
+ const want = (k + rnd(k, seed) * 0.8) * (total / count);
319
+ while (si < n - 1 && acc + seg[si] < want) acc += seg[si++];
320
+ const a = outline[si],
321
+ c = outline[(si + 1) % n];
322
+ const t = seg[si] ? (want - acc) / seg[si] : 0;
323
+ const x = a[0] + (c[0] - a[0]) * t,
324
+ y = a[1] + (c[1] - a[1]) * t;
325
+ /* outward: away from the shape's centre, turned a little at random */
326
+ let ox = x - b.cx,
327
+ oy = y - b.cy;
328
+ const ol = Math.hypot(ox, oy) || 1;
329
+ ox /= ol;
330
+ oy /= ol;
331
+ const ang = (rnd(k, seed + 1) - 0.5) * 0.9;
332
+ const ca = Math.cos(ang),
333
+ sa = Math.sin(ang);
334
+ const hx = ox * ca - oy * sa,
335
+ hy = ox * sa + oy * ca;
336
+ const l = len * (0.35 + rnd(k, seed + 2));
337
+ const p = rnd(k, seed + 3) > 0.5 ? lightP : darkP;
338
+ p.moveTo(x - hx * 0.4, y - hy * 0.4);
339
+ p.lineTo(x + hx * l, y + hy * l);
340
+ }
341
+ ctx.save();
342
+ ctx.lineCap = "round";
343
+ ctx.lineWidth = 0.4;
344
+ ctx.globalAlpha = 0.55;
345
+ ctx.strokeStyle = shade(base, 0.14);
346
+ ctx.stroke(lightP);
347
+ ctx.strokeStyle = shade(base, -0.06);
348
+ ctx.stroke(darkP);
349
+ ctx.restore();
350
+ }
351
+
352
+ /* a soft contact shadow cast on the body under an object's lower edge */
353
+ function contact(
354
+ ctx: CanvasRenderingContext2D,
355
+ clip: Path2D | null,
356
+ cx: number,
357
+ cy: number,
358
+ rx: number,
359
+ ry: number,
360
+ a: number,
361
+ ) {
362
+ if (!(rx > 0) || !(ry > 0)) return;
363
+ ctx.save();
364
+ if (clip) ctx.clip(clip);
365
+ ctx.translate(cx, cy);
366
+ ctx.scale(1, ry / rx);
367
+ const g = ctx.createRadialGradient(0, 0, 0, 0, 0, rx);
368
+ g.addColorStop(0, `rgba(0,0,0,${a})`);
369
+ g.addColorStop(0.55, `rgba(0,0,0,${a * 0.45})`);
370
+ g.addColorStop(1, "rgba(0,0,0,0)");
371
+ ctx.fillStyle = g;
372
+ ctx.beginPath();
373
+ ctx.arc(0, 0, rx, 0, Math.PI * 2);
374
+ ctx.fill();
375
+ ctx.restore();
376
+ }
377
+
378
+ /* ── the view for the meshes ───────────────────────────────────────── */
379
+
380
+ /* light elevation off the screen plane, as the body's material has it */
381
+ const EL = (48 * Math.PI) / 180;
382
+ function lightOf(lx: number, ly: number): { L: V3; H: V3 } {
383
+ const le = Math.cos(EL),
384
+ lz = Math.sin(EL);
385
+ const L = norm([lx * le, ly * le, lz]);
386
+ return { L, H: norm([L[0], L[1], L[2] + 1]) };
387
+ }
388
+ /* device pixels per unit of the context's current space */
389
+ function pxOf(ctx: CanvasRenderingContext2D): number {
390
+ const t = ctx.getTransform?.();
391
+ return t ? Math.sqrt(Math.abs(t.a * t.d - t.b * t.c)) || 1 : 2;
392
+ }
393
+ function viewOf(r: WearRig, px: number): View {
394
+ const { L, H } = lightOf(r.lx, r.ly);
395
+ /* the rows of project() and depthOf() */
396
+ return makeView(
397
+ [r.cy, 0, r.sy, r.sy * r.sp, r.cp, -r.cy * r.sp, -r.sy * r.cp, r.sp, r.cy * r.cp],
398
+ [50, 50, 0],
399
+ L,
400
+ H,
401
+ px,
402
+ );
403
+ }
404
+
405
+ /* The meshes, built once per shape, part and grid size and kept: a frame
406
+ only projects them. `owner` is what they belong to — the shape's marks,
407
+ or the glasses. */
408
+ const kept = new WeakMap<object, Map<string, Mesh>>();
409
+ let owner: object = kept;
410
+ function keep(key: string, make: () => Mesh): Mesh {
411
+ let c = kept.get(owner);
412
+ if (!c) kept.set(owner, (c = new Map()));
413
+ let g = c.get(key);
414
+ if (!g) {
415
+ if (c.size > 240) c.clear();
416
+ g = make();
417
+ c.set(key, g);
418
+ }
419
+ return g;
420
+ }
421
+ function surface(
422
+ view: View,
423
+ key: string,
424
+ surf: (u: number, v: number) => V3,
425
+ nu: number,
426
+ nv: number,
427
+ inside: V3 | ((u: number, v: number) => V3),
428
+ smooth = true,
429
+ ): Proj {
430
+ return projectMesh(
431
+ view,
432
+ keep(`${key}|${nu}x${nv}`, () => makeMesh(surf, nu, nv, inside, smooth)),
433
+ );
434
+ }
435
+ function tube(
436
+ view: View,
437
+ key: string,
438
+ path: (u: number) => V3,
439
+ up: V3,
440
+ prof: (u: number, ph: number) => [number, number],
441
+ nu: number,
442
+ nv: number,
443
+ closed = false,
444
+ ): Proj {
445
+ return projectMesh(
446
+ view,
447
+ keep(`${key}|${nu}x${nv}`, () => makeTube(path, up, prof, nu, nv, closed)),
448
+ );
449
+ }
450
+ const rgbOf = (c: string, fallback: RGB = [40, 40, 44]): RGB =>
451
+ (parseColor(c) as RGB | null) ?? fallback;
452
+ const TAU = Math.PI * 2;
453
+ const smooth01 = (t: number) => (t <= 0 ? 0 : t >= 1 ? 1 : t * t * (3 - 2 * t));
454
+ /* a signed power, for superellipses */
455
+ const spow = (c: number, e: number) => Math.sign(c) * Math.pow(Math.abs(c), e);
456
+
457
+ /* a sphere, as a mesh: pompoms and the like */
458
+ function sphereMesh(view: View, key: string, c: V3, rad: number, nu: number, nv: number): Proj {
459
+ return surface(
460
+ view,
461
+ key,
462
+ (u, v) => {
463
+ const th = u * TAU,
464
+ ph = v * Math.PI;
465
+ return [
466
+ c[0] + rad * Math.sin(ph) * Math.cos(th),
467
+ c[1] - rad * Math.cos(ph),
468
+ c[2] + rad * Math.sin(ph) * Math.sin(th),
469
+ ];
470
+ },
471
+ nu,
472
+ nv,
473
+ c,
474
+ );
475
+ }
476
+
477
+ /* a ring round a centre line in a hat's own frame: a binding, a trim, a
478
+ rim — a tube round a circle, each normal turned away from the circle */
479
+ function ringMesh(
480
+ view: View,
481
+ key: string,
482
+ place: (p: V3) => V3,
483
+ R: number,
484
+ y: number,
485
+ t: number,
486
+ zScale: number,
487
+ nu: number,
488
+ nv: number,
489
+ ): Proj {
490
+ return surface(
491
+ view,
492
+ key,
493
+ (u, v) => {
494
+ const th = u * TAU,
495
+ ph = v * TAU;
496
+ const rr = R + t * Math.cos(ph);
497
+ return place([rr * Math.cos(th), y - t * Math.sin(ph), rr * zScale * Math.sin(th)]);
498
+ },
499
+ nu,
500
+ nv,
501
+ (u) => place([R * Math.cos(u * TAU), y, R * zScale * Math.sin(u * TAU)]),
502
+ );
503
+ }
504
+
505
+ /* a path moved sideways off itself, in the plane across `up` */
506
+ function offsetPath(path: (u: number) => V3, up: V3, off: (u: number) => number) {
507
+ return (u: number): V3 => {
508
+ const e = 1e-3;
509
+ const T = norm(sub(path(Math.min(1, u + e)), path(Math.max(0, u - e))));
510
+ const B = cross(T, up);
511
+ return along(path(u), B, off(u));
512
+ };
513
+ }
514
+
515
+ /* a yarn or tinsel pompom: a shaded ball under a mass of short strands,
516
+ lighter and darker, standing out all round */
517
+ function yarnBall(
518
+ ctx: CanvasRenderingContext2D,
519
+ view: View,
520
+ c: V3,
521
+ rad: number,
522
+ base: string,
523
+ seed: number,
524
+ ) {
525
+ const rgb = rgbOf(base);
526
+ const n = segs(view, TAU * rad, 12, 20);
527
+ const vis = paintLayers(
528
+ ctx,
529
+ [
530
+ {
531
+ parts: [sphereMesh(view, `ball${seed}`, c, rad * 0.86, n, Math.max(6, n >> 1))],
532
+ shade: felt(view, rgb, 0.92),
533
+ },
534
+ ],
535
+ view.px,
536
+ );
537
+ const centre = view.toScreen(c);
538
+ const b = bounds(vis);
539
+ const R = Math.max(b.w, b.h) / 2;
540
+ const count = Math.round(Math.min(340, 50 + R * view.px * 5));
541
+ const light = new Path2D(),
542
+ dark = new Path2D();
543
+ for (let k = 0; k < count; k++) {
544
+ const a = rnd(k, seed) * TAU;
545
+ const r0 = R * (0.25 + 0.7 * Math.sqrt(rnd(k, seed + 1)));
546
+ const len = R * (0.25 + 0.35 * rnd(k, seed + 2));
547
+ const x0 = centre[0] + Math.cos(a) * r0,
548
+ y0 = centre[1] + Math.sin(a) * r0;
549
+ const bend = (rnd(k, seed + 3) - 0.5) * 0.6;
550
+ const ex = Math.cos(a + bend),
551
+ ey = Math.sin(a + bend);
552
+ const p = rnd(k, seed + 4) > 0.45 ? light : dark;
553
+ p.moveTo(x0, y0);
554
+ p.quadraticCurveTo(
555
+ x0 + Math.cos(a) * len * 0.5,
556
+ y0 + Math.sin(a) * len * 0.5,
557
+ x0 + ex * len,
558
+ y0 + ey * len,
559
+ );
560
+ }
561
+ ctx.save();
562
+ ctx.lineCap = "round";
563
+ ctx.lineWidth = Math.max(0.3, R * 0.06);
564
+ /* the strands take the ball's own light: lighter toward the light */
565
+ const L = view.L;
566
+ const g = ctx.createLinearGradient(
567
+ centre[0] + L[0] * R,
568
+ centre[1] + L[1] * R,
569
+ centre[0] - L[0] * R,
570
+ centre[1] - L[1] * R,
571
+ );
572
+ g.addColorStop(0, shade(base, 0.2, -0.03));
573
+ g.addColorStop(1, shade(base, -0.12, 0.02));
574
+ ctx.strokeStyle = g;
575
+ ctx.globalAlpha = 0.85;
576
+ ctx.stroke(light);
577
+ ctx.strokeStyle = shade(base, -0.18, 0.03);
578
+ ctx.globalAlpha = 0.6;
579
+ ctx.stroke(dark);
580
+ ctx.restore();
581
+ }
582
+
583
+ /* ── the hats ───────────────────────────────────────────────────────── */
584
+
585
+ /* The beret: a felt dome settled over the crown of the head, its widest
586
+ ring a little above the band and the underside tucking in to it, slid
587
+ and tipped over to one side the way a beret is worn. Soft pleats run
588
+ round it, so the light breaks along the brim; a narrow leather binding
589
+ sits where it meets the head; a short stalk stands on top. */
590
+ function drawBeret(
591
+ ctx: CanvasRenderingContext2D,
592
+ view: View,
593
+ m: Marks,
594
+ base: string,
595
+ bodyClip: Path2D | null,
596
+ ) {
597
+ const W = m.right - m.left;
598
+ const R = 0.46 * W,
599
+ Hh = 0.21 * W,
600
+ Rb = 0.33 * W,
601
+ lift = 0.05 * W;
602
+ const C: V3 = [50 - 0.07 * W, Math.max(m.top + 0.22 * W, seat(m, 0.5) + 0.1 * W), 0];
603
+ /* a negative lean tips the top toward the viewer, so the crown of the
604
+ beret shows and its underside stays hidden */
605
+ const roll = -0.24,
606
+ lean = -0.2;
607
+ const place = (p: V3): V3 => add(C, tilt(p, roll, lean));
608
+ const nu = segs(view, TAU * R, 32, 56),
609
+ nv = segs(view, 1.2 * R, 10, 32, 6);
610
+ const surf = (u: number, v: number): V3 => {
611
+ const th = u * TAU;
612
+ let rr: number, hh: number;
613
+ const E = 0.74; // where the widest ring is, along the profile
614
+ if (v <= E) {
615
+ const phi = (v / E) * (Math.PI / 2);
616
+ rr = R * Math.sin(phi);
617
+ /* a soft, slightly flattened top */
618
+ hh = -(lift + Hh * Math.pow(Math.cos(phi), 0.8));
619
+ } else {
620
+ const f = smooth01((v - E) / (1 - E));
621
+ rr = R + (Rb - R) * f;
622
+ hh = -lift * (1 - f);
623
+ }
624
+ /* pleats: strongest round the widest ring, gone at the top */
625
+ const band = Math.sin(Math.min(1, v / E) * Math.PI * 0.9);
626
+ rr *= 1 + 0.035 * Math.sin(th * 7 + 0.4) * band;
627
+ return place([rr * Math.cos(th), hh, rr * 0.93 * Math.sin(th)]);
628
+ };
629
+ const dome = surface(view, "beret:dome", surf, nu, nv, place([0, -lift - Hh * 0.4, 0]));
630
+ /* the underside inside the band, closed */
631
+ const under = surface(
632
+ view,
633
+ "beret:under",
634
+ (u, v) => {
635
+ const th = u * TAU,
636
+ rr = Rb * (1 - v);
637
+ return place([rr * Math.cos(th), -0.004 * W * v, rr * 0.93 * Math.sin(th)]);
638
+ },
639
+ 24,
640
+ 2,
641
+ place([0, -lift - Hh * 0.4, 0]),
642
+ );
643
+ const rgb = rgbOf(base);
644
+ /* its shadow on the head, under the band */
645
+ const bandMid = view.toScreen(place([0, 0.02 * W, 0]));
646
+ contact(ctx, bodyClip, bandMid[0] + 0.02 * W, bandMid[1] + 0.045 * W, Rb * 1.25, 0.08 * W, 0.4);
647
+ const binding = ringMesh(
648
+ view,
649
+ "beret:binding",
650
+ place,
651
+ Rb,
652
+ -0.005 * W,
653
+ 0.022 * W,
654
+ 0.93,
655
+ segs(view, TAU * Rb, 24, 48),
656
+ 5,
657
+ );
658
+ const leather = rgbOf(shade(base, -0.06));
659
+ const vis = paintLayers(
660
+ ctx,
661
+ [
662
+ { parts: [under], shade: felt(view, rgb, 0.7) },
663
+ { parts: [binding], shade: plastic(view, leather, 0.45) },
664
+ { parts: [dome], shade: felt(view, rgb) },
665
+ ],
666
+ view.px,
667
+ );
668
+ const outline = hull(vis);
669
+ grainOver(ctx, outline, 0.5, 0.14);
670
+ fuzz(ctx, outline, base, 11, 0.4, view.px);
671
+ /* the stalk: a short tail of felt on the crown, a little bent */
672
+ const x0 = 0.02 * W,
673
+ y0 = -lift - Hh + 0.006 * W;
674
+ const stalk = tube(
675
+ view,
676
+ "beret:stalk",
677
+ (u) => place([x0 + 0.014 * W * u * u, y0 - 0.045 * W * u, 0]),
678
+ [0, 0, 1],
679
+ (u, ph) => [
680
+ 0.016 * W * (1 - 0.25 * u) * Math.cos(ph),
681
+ 0.016 * W * (1 - 0.25 * u) * Math.sin(ph),
682
+ ],
683
+ 8,
684
+ 3,
685
+ );
686
+ paintLayers(ctx, [{ parts: [stalk], shade: felt(view, rgb, 0.95) }], view.px);
687
+ sphereSprite(ctx, view, place([x0 + 0.014 * W, y0 - 0.047 * W, 0]), 0.019 * W, scale(rgb, 1.15));
688
+ }
689
+
690
+ /* The beanie: a knitted dome with a folded, ribbed cuff and a yarn pompom.
691
+ Every stitch is drawn — a V in each cell of the dome, pointing to the
692
+ brim, with the yarn's light edge beside its dark gap — and the cuff's
693
+ ribs rise and fall round it. */
694
+ function drawBeanie(
695
+ ctx: CanvasRenderingContext2D,
696
+ view: View,
697
+ m: Marks,
698
+ base: string,
699
+ bodyClip: Path2D | null,
700
+ ) {
701
+ const W = m.right - m.left;
702
+ const headW = Math.max(0.5 * W, m.topR - m.topL);
703
+ const Rd = 0.53 * headW + 0.08 * W,
704
+ Hd = 0.32 * W,
705
+ cuffH = 0.1 * W,
706
+ Rc = Rd * 1.05;
707
+ const C: V3 = [50, seat(m, 0.72) + 0.06 * W, 0];
708
+ const roll = 0,
709
+ lean = -0.14;
710
+ const place = (p: V3): V3 => add(C, tilt(p, roll, lean));
711
+ const domeSurf = (u: number, v: number): V3 => {
712
+ const th = u * TAU,
713
+ phi = v * (Math.PI / 2);
714
+ const rr = Rd * Math.sin(phi) * (1 - 0.03 * (1 - v));
715
+ const hh = -(cuffH + Hd * Math.cos(phi));
716
+ return place([rr * Math.cos(th), hh, rr * 0.92 * Math.sin(th)]);
717
+ };
718
+ const inDome = place([0, -cuffH - Hd * 0.4, 0]);
719
+ const dome = surface(
720
+ view,
721
+ "beanie:dome",
722
+ domeSurf,
723
+ segs(view, TAU * Rd, 28, 48),
724
+ segs(view, 1.3 * Hd, 9, 24, 6),
725
+ inDome,
726
+ );
727
+ const cuffSurf = (u: number, v: number): V3 => {
728
+ const th = u * TAU;
729
+ const e = 2 * v - 1;
730
+ const rr = Rc * (1 - 0.07 * e * e * e * e) * 1.01;
731
+ return place([rr * Math.cos(th), -cuffH * (1 - v), rr * 0.92 * Math.sin(th)]);
732
+ };
733
+ const cuff = surface(
734
+ view,
735
+ "beanie:cuff",
736
+ cuffSurf,
737
+ segs(view, TAU * Rc, 28, 56),
738
+ 4,
739
+ place([0, -cuffH / 2, 0]),
740
+ );
741
+ const rgb = rgbOf(base),
742
+ cuffRgb = rgbOf(shade(base, 0.03));
743
+ const bb = boundsOf([cuff]);
744
+ contact(ctx, bodyClip, bb.cx, bb.y1 - 0.01 * W, bb.w * 0.5, 0.06 * W, 0.34);
745
+ const visDome = paintLayers(ctx, [{ parts: [dome], shade: felt(view, rgb, 0.96) }], view.px);
746
+ /* the stitches: one V per cell of their own grid, dark gap then light
747
+ yarn, only on the side facing the viewer and only when big enough */
748
+ const NU = 40,
749
+ NV = 11;
750
+ const cell = ((Rd * TAU) / NU) * view.px;
751
+ if (cell >= 2.2) {
752
+ const grid = surface(view, "beanie:stitch", domeSurf, NU, NV, inDome, false);
753
+ const dark = new Path2D(),
754
+ light = new Path2D();
755
+ const GW = NU + 1;
756
+ for (let c = 0; c < NU * NV; c++) {
757
+ if (grid.f[c * 3 + 2] < 0.12) continue;
758
+ const k = Math.floor(c / NU) * GW + (c % NU);
759
+ const A: P2 = [grid.x[k], grid.y[k]],
760
+ B: P2 = [grid.x[k + 1], grid.y[k + 1]];
761
+ const Cq: P2 = [grid.x[k + GW + 1], grid.y[k + GW + 1]],
762
+ D: P2 = [grid.x[k + GW], grid.y[k + GW]];
763
+ const at = (s: number, t: number): P2 => {
764
+ const top: P2 = [A[0] + (B[0] - A[0]) * s, A[1] + (B[1] - A[1]) * s];
765
+ const bot: P2 = [D[0] + (Cq[0] - D[0]) * s, D[1] + (Cq[1] - D[1]) * s];
766
+ return [top[0] + (bot[0] - top[0]) * t, top[1] + (bot[1] - top[1]) * t];
767
+ };
768
+ for (const [p, dx] of [
769
+ [dark, 0],
770
+ [light, -0.06],
771
+ ] as const) {
772
+ const a = at(0.14 + dx, 0.12),
773
+ b = at(0.5 + dx, 0.88),
774
+ c = at(0.86 + dx, 0.12);
775
+ p.moveTo(a[0], a[1]);
776
+ p.lineTo(b[0], b[1]);
777
+ p.lineTo(c[0], c[1]);
778
+ }
779
+ }
780
+ const cw = (Rd * TAU) / NU;
781
+ ctx.save();
782
+ ctx.clip(polyPath(hull(visDome)));
783
+ ctx.lineCap = "round";
784
+ ctx.lineJoin = "round";
785
+ ctx.lineWidth = Math.max(0.35, cw * 0.2);
786
+ ctx.strokeStyle = shade(base, -0.22, 0.03);
787
+ ctx.globalAlpha = 0.5;
788
+ ctx.stroke(dark);
789
+ ctx.lineWidth = Math.max(0.3, cw * 0.12);
790
+ ctx.strokeStyle = shade(base, 0.2, -0.03);
791
+ ctx.globalAlpha = 0.35;
792
+ ctx.stroke(light);
793
+ ctx.restore();
794
+ }
795
+ /* the cuff over the dome's lower edge, and its ribs: a groove and a
796
+ ridge of yarn every twelfth of a turn, on the side toward the viewer */
797
+ const visCuff = paintLayers(ctx, [{ parts: [cuff], shade: felt(view, cuffRgb, 0.98) }], view.px);
798
+ const ribGap = (TAU * Rc) / 30;
799
+ if (ribGap * view.px >= 3) {
800
+ const groove = new Path2D(),
801
+ ridge = new Path2D();
802
+ for (let k = 0; k < 60; k++) {
803
+ const u = k / 60;
804
+ const th = u * TAU;
805
+ if (view.dir(tilt([Math.cos(th), 0, 0.92 * Math.sin(th)], roll, lean))[2] < 0.08) continue;
806
+ const a = view.toScreen(cuffSurf(u, 0.1)),
807
+ b = view.toScreen(cuffSurf(u, 0.9));
808
+ const p = k % 2 ? groove : ridge;
809
+ p.moveTo(a[0], a[1]);
810
+ p.lineTo(b[0], b[1]);
811
+ }
812
+ ctx.save();
813
+ ctx.lineCap = "round";
814
+ ctx.lineWidth = ribGap * 0.34;
815
+ ctx.strokeStyle = shade(base, -0.2, 0.03);
816
+ ctx.globalAlpha = 0.45;
817
+ ctx.stroke(groove);
818
+ ctx.lineWidth = ribGap * 0.22;
819
+ ctx.strokeStyle = shade(base, 0.16, -0.02);
820
+ ctx.globalAlpha = 0.4;
821
+ ctx.stroke(ridge);
822
+ ctx.restore();
823
+ }
824
+ const outline = hull(visDome.concat(visCuff));
825
+ fuzz(ctx, outline, base, 23, 0.35, view.px);
826
+ yarnBall(ctx, view, place([0, -cuffH - Hd - 0.055 * W, 0]), 0.085 * W, shade(base, 0.06), 31);
827
+ }
828
+
829
+ /* The party hat: a paper cone with stripes that wrap it, a foil trim at
830
+ the base and a tinsel pompom at the tip. */
831
+ function drawParty(
832
+ ctx: CanvasRenderingContext2D,
833
+ view: View,
834
+ m: Marks,
835
+ base: string,
836
+ bodyClip: Path2D | null,
837
+ ) {
838
+ const W = m.right - m.left;
839
+ const C: V3 = [50 + 0.06 * W, seat(m, 0.3) + 0.04 * W, 0];
840
+ /* as tall as the canvas has room for above the head */
841
+ const R = 0.18 * W,
842
+ H = Math.max(0.3 * W, Math.min(0.46 * W, C[1] + 31 - 0.1 * W));
843
+ const roll = 0.18,
844
+ lean = -0.12;
845
+ const place = (p: V3): V3 => add(C, tilt(p, roll, lean));
846
+ const coneAt = (u: number, v: number, grow = 1): V3 => {
847
+ const th = u * TAU;
848
+ const rr = R * (1 - v) * (1 + 0.04 * Math.sin(v * Math.PI)) * grow;
849
+ return place([rr * Math.cos(th), -H * v, rr * Math.sin(th)]);
850
+ };
851
+ const inCone = (_u: number, v: number) => place([0, -H * Math.min(0.97, v), 0]);
852
+ const nAround = segs(view, TAU * R, 20, 40),
853
+ nUp = segs(view, H, 10, 24, 6);
854
+ const cone = surface(view, "party:cone", (u, v) => coneAt(u, v), nAround, nUp, inCone);
855
+ const a = rgbOf(base),
856
+ b: RGB = [252, 246, 234];
857
+ const bb = boundsOf([cone]);
858
+ contact(ctx, bodyClip, bb.cx, bb.y1, R * 1.15, 0.04 * W, 0.3);
859
+ const trim = ringMesh(
860
+ view,
861
+ "party:trim",
862
+ place,
863
+ R * 1.02,
864
+ 0.004 * W,
865
+ 0.016 * W,
866
+ 1,
867
+ segs(view, TAU * R, 20, 40),
868
+ 5,
869
+ );
870
+ /* the stripes: bands spiralling up the cone, each its own strip of the
871
+ cone's surface, so their edges run smooth rather than stepping */
872
+ const stripes: Proj[] = [];
873
+ const K = 6,
874
+ twist = 3.2;
875
+ for (let k = 0; k < K; k += 1) {
876
+ stripes.push(
877
+ surface(
878
+ view,
879
+ `party:stripe${k}`,
880
+ (s, t) => coneAt((k + 0.5 * t - twist * s * 0.97) / K, s * 0.97, 1.004),
881
+ nUp + 3,
882
+ 1,
883
+ inCone,
884
+ ),
885
+ );
886
+ }
887
+ const vis = paintLayers(
888
+ ctx,
889
+ [
890
+ { parts: [cone], shade: paper(view, b) },
891
+ { parts: [trim], shade: gold(view) },
892
+ ],
893
+ view.px,
894
+ );
895
+ paintLayers(ctx, [{ parts: stripes, shade: paper(view, a) }], view.px);
896
+ grainOver(ctx, hull(vis), 0.18, 0.1);
897
+ yarnBall(ctx, view, place([0, -H - 0.035 * W, 0]), 0.06 * W, "#f4efe6", 37);
898
+ }
899
+
900
+ /* The crown: a gold band between two rolled rims, five points with a
901
+ ridge up each and a pearl on its tip, small gold buds between them, a
902
+ cut stone in a gold setting under every point, a puffed velvet cap
903
+ filling it, and an orb and cross on top. */
904
+ function drawCrown(ctx: CanvasRenderingContext2D, view: View, m: Marks, bodyClip: Path2D | null) {
905
+ const W = m.right - m.left;
906
+ const headW = Math.max(0.45 * W, m.topR - m.topL);
907
+ const R = 0.44 * headW + 0.07 * W,
908
+ bandH = 0.11 * W,
909
+ pointH = 0.15 * W,
910
+ ez = 0.9;
911
+ const C: V3 = [50, seat(m, 0.42) + 0.05 * W, 0];
912
+ const roll = 0.05,
913
+ lean = -0.22;
914
+ const place = (p: V3): V3 => add(C, tilt(p, roll, lean));
915
+ const dirOf = (v: V3): V3 => norm(tilt(v, roll, lean));
916
+ const on = (th: number, rr: number, y: number): V3 =>
917
+ place([rr * Math.cos(th), y, rr * ez * Math.sin(th)]);
918
+ const axis = (y: number): V3 => place([0, y, 0]);
919
+ const nu = segs(view, TAU * R, 32, 64);
920
+ const band = surface(
921
+ view,
922
+ "crown:band",
923
+ (u, v) => on(u * TAU, R * (1 - 0.018 * Math.sin(v * Math.PI)), -bandH * (1 - v)),
924
+ nu,
925
+ 2,
926
+ (_u, v) => axis(-bandH * (1 - v)),
927
+ );
928
+ const rimT = 0.017 * W;
929
+ const rimTop = ringMesh(view, "crown:rimTop", place, R + 0.004 * W, -bandH, rimT, ez, nu, 5);
930
+ const rimBot = ringMesh(view, "crown:rimBot", place, R + 0.004 * W, 0, rimT * 1.15, ez, nu, 5);
931
+ /* the velvet: puffed between the points, tall enough to cover the head */
932
+ const capBase = -bandH * 0.55;
933
+ const capH = Math.max(0.12 * W, C[1] - m.top + 0.04 * W + capBase);
934
+ const P0 = Math.PI / 2; // the front point
935
+ const cap = surface(
936
+ view,
937
+ "crown:cap",
938
+ (u, v) => {
939
+ const th = u * TAU,
940
+ phi = v * (Math.PI / 2);
941
+ /* the puffs swell between the points, above the band, and tuck in
942
+ under it */
943
+ const puff = 0.08 * Math.sin(2 * phi) * (0.55 + 0.45 * Math.cos(5 * (th - P0 - Math.PI / 5)));
944
+ return on(th, R * (0.9 * Math.sin(phi) + puff), capBase - capH * Math.cos(phi));
945
+ },
946
+ segs(view, TAU * R, 24, 48),
947
+ segs(view, 1.4 * capH, 8, 20, 6),
948
+ axis(capBase - capH * 0.4),
949
+ );
950
+ /* the points: plates on the band's curve with a ridge up the middle,
951
+ their sides curving in to the tip, flaring out a little as they rise */
952
+ const points: Proj[] = [],
953
+ buds: Proj[] = [];
954
+ const pearls: V3[] = [],
955
+ budTops: V3[] = [];
956
+ const plate = (
957
+ key: string,
958
+ a0: number,
959
+ w: number,
960
+ h: number,
961
+ ridge: number,
962
+ flare: number,
963
+ out: Proj[],
964
+ ) => {
965
+ out.push(
966
+ surface(
967
+ view,
968
+ key,
969
+ (u, v) => {
970
+ const s = u * 2 - 1;
971
+ const th = a0 + s * w * Math.pow(1 - v, 1.3);
972
+ const rr = R + 0.004 * W + ridge * (1 - Math.abs(s)) * (1 - 0.5 * v) + flare * v * v;
973
+ return on(th, rr, -bandH - h * v);
974
+ },
975
+ 2,
976
+ segs(view, h, 3, 5),
977
+ (_u, v) => on(a0, R * 0.5, -bandH - h * v),
978
+ false,
979
+ ),
980
+ );
981
+ };
982
+ for (let i = 0; i < 5; i++) {
983
+ const a0 = P0 + (i / 5) * TAU;
984
+ plate(`crown:point${i}`, a0, (TAU / 5) * 0.34, pointH, 0.05 * W, 0.03 * W, points);
985
+ pearls.push(on(a0, R + 0.004 * W + 0.03 * W + 0.02 * W, -bandH - pointH - 0.018 * W));
986
+ const b0 = a0 + Math.PI / 5;
987
+ plate(`crown:bud${i}`, b0, (TAU / 5) * 0.12, pointH * 0.36, 0.02 * W, 0.01 * W, buds);
988
+ budTops.push(on(b0, R + 0.004 * W + 0.012 * W, -bandH - pointH * 0.36 - 0.012 * W));
989
+ }
990
+ const bb = boundsOf([band]);
991
+ contact(ctx, bodyClip, bb.cx, bb.y1, bb.w * 0.5, 0.045 * W, 0.34);
992
+ /* what sits behind the cap goes first, so the cap covers it */
993
+ const mid = view.depth(axis(capBase));
994
+ const g = gold(view);
995
+ /* the points' flat faces take a warmer, softer glint than the curves:
996
+ a whole facet flaring white reads as paper, not gold */
997
+ const facet = metal(view, [255, 196, 80], [92, 54, 10], [255, 222, 150]);
998
+ const goldBoth = (n: V3) => facet(n[2] < 0 ? [-n[0], -n[1], -n[2]] : n);
999
+ const pearlR = 0.024 * W,
1000
+ budR = 0.019 * W;
1001
+ for (const p of pearls) if (view.depth(p) < mid) sphereSprite(ctx, view, p, pearlR, "pearl");
1002
+ for (const p of budTops) if (view.depth(p) < mid) sphereSprite(ctx, view, p, budR, "gold");
1003
+ paintLayers(
1004
+ ctx,
1005
+ [
1006
+ { parts: [cap], shade: velvet(view, [150, 22, 44]) },
1007
+ { parts: [band], shade: g, bias: 0.01 * W },
1008
+ { parts: [rimTop], shade: g, bias: 0.012 * W },
1009
+ { parts: [rimBot], shade: g, bias: 0.012 * W },
1010
+ { parts: points, shade: goldBoth, cull: false, flat: true },
1011
+ { parts: buds, shade: goldBoth, cull: false, flat: true },
1012
+ ],
1013
+ view.px,
1014
+ );
1015
+ /* the stones in their settings, under each point, and gold studs between */
1016
+ const stones: RGB[] = [
1017
+ [208, 20, 60],
1018
+ [30, 150, 92],
1019
+ [36, 90, 220],
1020
+ [36, 90, 220],
1021
+ [30, 150, 92],
1022
+ ];
1023
+ for (let i = 0; i < 5; i++) {
1024
+ for (const [th, kind] of [
1025
+ [P0 + (i / 5) * TAU, "stone"],
1026
+ [P0 + (i / 5) * TAU + Math.PI / 5, "stud"],
1027
+ ] as const) {
1028
+ const N = dirOf([Math.cos(th), 0, ez * Math.sin(th)]);
1029
+ if (view.dir(N)[2] < 0.12) continue;
1030
+ const c = on(th, R + 0.01 * W, -bandH * 0.5);
1031
+ if (kind === "stud") {
1032
+ sphereSprite(ctx, view, c, 0.013 * W, "gold");
1033
+ continue;
1034
+ }
1035
+ const size = (i === 0 ? 0.034 : 0.027) * W;
1036
+ sphereSprite(ctx, view, c, size * 1.3, "gold");
1037
+ const T = dirOf([-Math.sin(th), 0, ez * Math.cos(th)]);
1038
+ cutGem(ctx, view, along(c, N, 0.006 * W), N, T, size, stones[i]);
1039
+ }
1040
+ }
1041
+ for (const p of pearls) if (view.depth(p) >= mid) sphereSprite(ctx, view, p, pearlR, "pearl");
1042
+ for (const p of budTops) if (view.depth(p) >= mid) sphereSprite(ctx, view, p, budR, "gold");
1043
+ /* the orb and the cross on top of the cap */
1044
+ const orbR = 0.034 * W;
1045
+ const orb = axis(capBase - capH - orbR * 0.75);
1046
+ const up = dirOf([0, -1, 0]);
1047
+ const bar = (key: string, a: V3, b: V3) =>
1048
+ tube(
1049
+ view,
1050
+ key,
1051
+ (u) => [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u, a[2] + (b[2] - a[2]) * u],
1052
+ dirOf([0, 0, 1]),
1053
+ section(0.009 * W, 0.009 * W, 0.5),
1054
+ 2,
1055
+ 6,
1056
+ );
1057
+ const c0 = along(orb, up, orbR * 0.8),
1058
+ c1 = along(orb, up, orbR * 0.8 + 0.075 * W);
1059
+ const cm = along(orb, up, orbR * 0.8 + 0.05 * W),
1060
+ side = dirOf([1, 0, 0]);
1061
+ sphereSprite(ctx, view, orb, orbR, "gold");
1062
+ paintLayers(
1063
+ ctx,
1064
+ [
1065
+ { parts: [bar("crown:barV", c0, c1)], shade: g },
1066
+ {
1067
+ parts: [bar("crown:barH", along(cm, side, -0.026 * W), along(cm, side, 0.026 * W))],
1068
+ shade: g,
1069
+ },
1070
+ ],
1071
+ view.px,
1072
+ );
1073
+ }
1074
+
1075
+ /* ── headphones ─────────────────────────────────────────────────────── */
1076
+
1077
+ /* Over-ear headphones: at each side a cup — a plump creased leather
1078
+ cushion against the head, a rounded shell, a brushed metal ring round a
1079
+ glossy plate — held in a metal yoke that pivots at its front and back;
1080
+ a slider rising from each yoke into the band; the band arched over the
1081
+ head, a shell on top of a padded leather cushion stitched along its
1082
+ face. The cups are turned a little toward the viewer, as they are drawn
1083
+ in illustration, so their faces show from the front.
1084
+
1085
+ Everything is built once and painted in two passes split by depth:
1086
+ what is behind the body's middle plane before the body (the far cup
1087
+ once the head turns, the band's far end), the rest after it. */
1088
+ interface Phones {
1089
+ layers: Layer[];
1090
+ pivots: V3[];
1091
+ pivotR: number;
1092
+ /** the stitch line along the band's cushion, and where the cushions meet the head */
1093
+ stitch: V3[];
1094
+ seats: { c: V3; ry: number }[];
1095
+ }
1096
+ function phonesOf(view: View, m: Marks, base: string): Phones {
1097
+ const W = m.right - m.left;
1098
+ const rc = 0.15 * W,
1099
+ oval = 1.1,
1100
+ pad = 0.05 * W,
1101
+ tc = 0.068 * W;
1102
+ const yc = m.mid - 0.07 * W;
1103
+ const tilt = 0.5;
1104
+ const shellRgb = rgbOf(base),
1105
+ padRgb = rgbOf(shade(base, -0.12)),
1106
+ plateRgb = rgbOf(shade(base, 0.08));
1107
+ const nu = segs(view, TAU * rc, 20, 40);
1108
+ const layers: Layer[] = [];
1109
+ const pivots: V3[] = [];
1110
+ const tops: V3[] = [];
1111
+ const seats: { c: V3; ry: number }[] = [];
1112
+ const g = silver(view);
1113
+ for (const s of [-1, 1] as const) {
1114
+ /* the cup's axis, out from the head and a little toward the viewer,
1115
+ and its face's two directions */
1116
+ const A: V3 = [s * Math.cos(tilt), 0, Math.sin(tilt)];
1117
+ const E1: V3 = [0, 1, 0];
1118
+ const E2: V3 = [-Math.sin(tilt), 0, s * Math.cos(tilt)];
1119
+ const O: V3 = [(s < 0 ? m.left : m.right) - s * 0.02 * W, yc, 0.01 * W];
1120
+ const at = (t: number, rho: number, th: number): V3 => {
1121
+ const c = Math.cos(th) * oval * rho,
1122
+ sn = Math.sin(th) * rho;
1123
+ return [
1124
+ O[0] + A[0] * t + E1[0] * c + E2[0] * sn,
1125
+ O[1] + A[1] * t + E1[1] * c + E2[1] * sn,
1126
+ O[2] + A[2] * t + E1[2] * c + E2[2] * sn,
1127
+ ];
1128
+ };
1129
+ const axis = (t: number): V3 => along(O, A, t);
1130
+ seats.push({ c: O, ry: rc * oval });
1131
+ /* the cushion: a plump roll of leather, creased round when there is room */
1132
+ const rho0 = rc * 0.9 - pad * 0.5;
1133
+ const creases = nu >= 48 ? 1 : 0;
1134
+ const cushion = surface(
1135
+ view,
1136
+ `phones:cushion${s}`,
1137
+ (u, v) => {
1138
+ const th = u * TAU,
1139
+ a = v * Math.PI;
1140
+ const crease = 1 + 0.006 * creases * Math.sin(th * 24) * Math.sin(a);
1141
+ return at(pad * 0.5 * (1 - Math.cos(a)), (rho0 + pad * 0.62 * Math.sin(a)) * crease, th);
1142
+ },
1143
+ nu,
1144
+ 5,
1145
+ (_u, v) => axis(pad * 0.5 * (1 - Math.cos(v * Math.PI))),
1146
+ );
1147
+ /* the shell: a rounded puck, its side meeting its face in a soft shoulder */
1148
+ const shell = surface(
1149
+ view,
1150
+ `phones:shell${s}`,
1151
+ (u, v) => {
1152
+ const a = v * (Math.PI / 2);
1153
+ return at(
1154
+ pad + tc * Math.pow(Math.sin(a), 0.55),
1155
+ rc * (0.7 + 0.3 * Math.pow(Math.cos(a), 0.55)),
1156
+ u * TAU,
1157
+ );
1158
+ },
1159
+ nu,
1160
+ 5,
1161
+ axis(pad + tc * 0.5),
1162
+ );
1163
+ /* the ring round its face, and the plate inside it: a shallow gloss dome */
1164
+ const ringR = 0.66 * rc,
1165
+ ringT = 0.017 * W;
1166
+ const ring = surface(
1167
+ view,
1168
+ `phones:ring${s}`,
1169
+ (u, v) =>
1170
+ at(pad + tc + ringT * 0.45 * Math.sin(v * TAU), ringR + ringT * Math.cos(v * TAU), u * TAU),
1171
+ nu,
1172
+ 5,
1173
+ (u) => at(pad + tc, ringR, u * TAU),
1174
+ );
1175
+ const plateR = ringR - ringT * 0.5;
1176
+ const plate = surface(
1177
+ view,
1178
+ `phones:plate${s}`,
1179
+ (u, v) => at(pad + tc + 0.013 * W * (1 - v * v), plateR * v, u * TAU),
1180
+ nu,
1181
+ 3,
1182
+ axis(pad + tc - 0.02 * W),
1183
+ );
1184
+ /* the yoke: an arm round the top of the cup, pivot to pivot */
1185
+ const ty = pad + tc * 0.45,
1186
+ ry = rc * 1.04 + 0.022 * W;
1187
+ const yokeAt = (u: number): V3 => {
1188
+ const a = u * Math.PI;
1189
+ const c = -Math.sin(a) * oval * ry,
1190
+ sn = Math.cos(a) * ry;
1191
+ return [
1192
+ O[0] + A[0] * ty + E1[0] * c + E2[0] * sn,
1193
+ O[1] + A[1] * ty + E1[1] * c + E2[1] * sn,
1194
+ O[2] + A[2] * ty + E1[2] * c + E2[2] * sn,
1195
+ ];
1196
+ };
1197
+ const yoke = tube(
1198
+ view,
1199
+ `phones:yoke${s}`,
1200
+ yokeAt,
1201
+ A,
1202
+ section(0.013 * W, 0.008 * W, 0.6),
1203
+ segs(view, Math.PI * ry, 12, 24),
1204
+ 6,
1205
+ );
1206
+ pivots.push(yokeAt(0), yokeAt(1));
1207
+ tops.push(yokeAt(0.5));
1208
+ layers.push(
1209
+ { parts: [cushion], shade: plastic(view, padRgb, 0.35) },
1210
+ { parts: [shell], shade: plastic(view, shellRgb, 0.8) },
1211
+ { parts: [ring], shade: g, bias: 0.02 * W },
1212
+ { parts: [plate], shade: plastic(view, plateRgb, 1.3), bias: 0.01 * W },
1213
+ { parts: [yoke], shade: g },
1214
+ );
1215
+ }
1216
+ /* the band: a smooth arch from one yoke over the head to the other,
1217
+ just high enough to clear the head everywhere — resting on it where
1218
+ the head is highest, bridging any dip — with a metal slider at each
1219
+ end running into the yoke */
1220
+ const [PL, PR] = tops;
1221
+ const zb = (PL[2] + PR[2]) / 2,
1222
+ cx = (PL[0] + PR[0]) / 2,
1223
+ ax = (PR[0] - PL[0]) / 2,
1224
+ yEnd = (PL[1] + PR[1]) / 2;
1225
+ const padT = (u: number) => 0.005 * W + 0.022 * W * Math.pow(Math.sin(Math.PI * u), 0.35);
1226
+ const e = 2 / 2.2;
1227
+ /* the head's top edge over x, from its hull */
1228
+ const topAt = (x: number) => {
1229
+ let y = Infinity;
1230
+ const h = m.hull;
1231
+ for (let i = 0; i < h.length; i++) {
1232
+ const p = h[i],
1233
+ q = h[(i + 1) % h.length];
1234
+ if (p[0] === q[0] || x < Math.min(p[0], q[0]) || x > Math.max(p[0], q[0])) continue;
1235
+ y = Math.min(y, p[1] + ((q[1] - p[1]) * (x - p[0])) / (q[0] - p[0]));
1236
+ }
1237
+ return y;
1238
+ };
1239
+ let lift = yEnd - m.top;
1240
+ for (let i = 1; i < 64; i++) {
1241
+ const t = Math.PI * (1 - i / 64);
1242
+ const s = Math.pow(Math.abs(Math.sin(t)), e);
1243
+ const yt = topAt(cx + ax * spow(Math.cos(t), e));
1244
+ if (s > 0.05 && Number.isFinite(yt))
1245
+ lift = Math.max(lift, (yEnd - yt + 2 * padT(i / 64) + 0.006 * W) / s);
1246
+ }
1247
+ const arch = (u: number): V3 => {
1248
+ const t = Math.PI * (1 - u);
1249
+ return [cx + ax * spow(Math.cos(t), e), yEnd - lift * Math.pow(Math.abs(Math.sin(t)), e), zb];
1250
+ };
1251
+ const part = (u0: number, u1: number) => (u: number) => arch(u0 + (u1 - u0) * u);
1252
+ const Z: V3 = [0, 0, 1];
1253
+ const span = ax * 2 + lift * 2;
1254
+ const nb = segs(view, span, 24, 48);
1255
+ const us = 0.09;
1256
+ const shellOff = 0.016 * W;
1257
+ const shellPath = offsetPath(part(us, 1 - us), Z, () => shellOff);
1258
+ /* the shell narrows at its ends to the slider it holds */
1259
+ const taper = (u: number) => smooth01(Math.min(u, 1 - u) / 0.07);
1260
+ const bandShell = tube(
1261
+ view,
1262
+ "phones:bandShell",
1263
+ shellPath,
1264
+ Z,
1265
+ (u, ph) => {
1266
+ const k = taper(u);
1267
+ return [
1268
+ (0.024 + 0.016 * k) * W * spow(Math.cos(ph), 0.4),
1269
+ (0.008 + 0.008 * k) * W * spow(Math.sin(ph), 0.4),
1270
+ ];
1271
+ },
1272
+ nb,
1273
+ 6,
1274
+ );
1275
+ const cushionPath = offsetPath(
1276
+ part(us + 0.02, 1 - us - 0.02),
1277
+ Z,
1278
+ (u) => -padT(us + 0.02 + (1 - 2 * us - 0.04) * u),
1279
+ );
1280
+ const bandPad = tube(
1281
+ view,
1282
+ "phones:bandPad",
1283
+ cushionPath,
1284
+ Z,
1285
+ (u, ph) => [
1286
+ 0.034 * W * spow(Math.cos(ph), 0.5),
1287
+ padT(us + 0.02 + (1 - 2 * us - 0.04) * u) * spow(Math.sin(ph), 0.7),
1288
+ ],
1289
+ nb,
1290
+ 6,
1291
+ );
1292
+ const sliders = [part(0, us + 0.03), part(1 - us - 0.03, 1)].map((p, i) =>
1293
+ tube(view, `phones:slider${i}`, p, Z, section(0.022 * W, 0.006 * W, 0.4), 4, 6),
1294
+ );
1295
+ layers.push(
1296
+ { parts: [bandShell], shade: plastic(view, shellRgb, 0.9) },
1297
+ { parts: [bandPad], shade: plastic(view, padRgb, 0.3) },
1298
+ { parts: sliders, shade: g },
1299
+ );
1300
+ const stitch: V3[] = [];
1301
+ for (let i = 0; i <= 48; i++)
1302
+ stitch.push(along(cushionPath(0.04 + (0.92 * i) / 48), Z, 0.034 * W + 0.001 * W));
1303
+ return { layers, pivots, pivotR: 0.017 * W, stitch, seats };
1304
+ }
1305
+
1306
+ /* the behind and front passes of one frame share one projection */
1307
+ let lastPhones: { m: Marks; key: string; ph: Phones } | null = null;
1308
+ function drawPhones(
1309
+ ctx: CanvasRenderingContext2D,
1310
+ view: View,
1311
+ m: Marks,
1312
+ base: string,
1313
+ pass: "behind" | "front",
1314
+ bodyClip: Path2D | null,
1315
+ ) {
1316
+ const key = `${view.m.join()}|${view.px}|${view.L.join()}|${base}`;
1317
+ if (!lastPhones || lastPhones.m !== m || lastPhones.key !== key)
1318
+ lastPhones = { m, key, ph: phonesOf(view, m, base) };
1319
+ const ph = lastPhones.ph;
1320
+ const W = m.right - m.left;
1321
+ const near = (d: number) => (pass === "front" ? d >= 0 : d < 0);
1322
+ if (pass === "front") {
1323
+ for (const s of ph.seats) {
1324
+ if (view.depth(s.c) < 0) continue;
1325
+ const c = view.toScreen(s.c);
1326
+ contact(ctx, bodyClip, c[0], c[1] + 0.01 * W, 0.06 * W, s.ry * 1.05, 0.26);
1327
+ }
1328
+ }
1329
+ paintLayers(
1330
+ ctx,
1331
+ ph.layers.map((l) => ({ ...l, keep: near })),
1332
+ view.px,
1333
+ );
1334
+ for (const p of ph.pivots)
1335
+ if (near(view.depth(p))) sphereSprite(ctx, view, p, ph.pivotR, "silver");
1336
+ /* the stitches along the band's cushion, on its face toward the viewer */
1337
+ if (pass === "front" && W * view.px > 120 && view.dir([0, 0, 1])[2] > 0.3) {
1338
+ const pts = ph.stitch.map((p) => view.toScreen(p));
1339
+ ctx.save();
1340
+ ctx.setLineDash([0.022 * W, 0.014 * W]);
1341
+ ctx.lineCap = "round";
1342
+ ctx.lineWidth = 0.0045 * W;
1343
+ ctx.strokeStyle = shade(base, 0.12);
1344
+ ctx.globalAlpha = 0.55;
1345
+ ctx.stroke(polyPath(pts, false));
1346
+ ctx.restore();
1347
+ }
1348
+ }
1349
+
1350
+ /* ── the bow tie ────────────────────────────────────────────────────── */
1351
+
1352
+ /* Two satin wings gathered into a knot: each wing narrow where the knot
1353
+ pinches it and full at its end, puffed toward the viewer, with folds
1354
+ that run out from the knot and fade toward the tip, and a shallow notch
1355
+ in its end; the knot a small wrapped pillow over them. */
1356
+ function drawBowTie(
1357
+ ctx: CanvasRenderingContext2D,
1358
+ view: View,
1359
+ r: WearRig,
1360
+ m: Marks,
1361
+ base: string,
1362
+ bodyZ: (x: number, y: number) => number,
1363
+ bodyClip: Path2D | null,
1364
+ face: { y: number; scale: number },
1365
+ fabricBow = false,
1366
+ ) {
1367
+ if (r.facing < 0.05) return;
1368
+ const W = m.right - m.left;
1369
+ /* under the face, clear of anything worn on it, and small enough to fit
1370
+ between the face and the bottom — a low face (a triangle's) gets a
1371
+ smaller bow */
1372
+ const clear = face.y + 22 * face.scale;
1373
+ const cyB = Math.max(clear, m.bottom - 0.15 * W);
1374
+ const room = Math.max(0, m.bottom - 3 - cyB);
1375
+ const k = Math.min(W, 100) * Math.max(0.5, Math.min(1, room / 15));
1376
+ /* on the body's front surface where the bow sits */
1377
+ const zFront = bodyZ(50, cyB);
1378
+ /* the bow's own frame, in fractions of the body's width: x across, y
1379
+ down, z out of the body's front */
1380
+ const Q = (x: number, y: number, z: number): V3 => [50 + x * k, cyB + y * k, zFront + z * k];
1381
+ const rgb = rgbOf(base);
1382
+ const corners = [Q(-0.36, -0.18, 0), Q(0.36, 0.18, 0), Q(-0.36, 0.18, 0), Q(0.36, -0.18, 0)].map(
1383
+ (p) => view.toScreen(p),
1384
+ );
1385
+ const bb = bounds(corners);
1386
+ const sh = view.toScreen(Q(0.01, 0.08, 0));
1387
+ contact(ctx, bodyClip, sh[0], sh[1], bb.w * 0.52, bb.h * 0.5, 0.3);
1388
+ const nu = segs(view, 0.32 * k, 8, 16),
1389
+ nv = segs(view, 0.3 * k, 8, 16, 6);
1390
+ const at = `${zFront.toFixed(2)},${cyB.toFixed(2)},${k.toFixed(2)}`;
1391
+ /* satin on a plastic body; on a plush one the same felt as the body */
1392
+ const soft = !!fabricBow;
1393
+ const wingShade = soft ? felt(view, rgb) : satin(view, rgb);
1394
+ const layers: Layer[] = [];
1395
+ for (const dir of [-1, 1] as const) {
1396
+ const wing = (u: number, v: number): V3 => {
1397
+ const x = 0.058 + 0.3 * u - 0.035 * Math.pow(u, 4) * Math.sin(Math.PI * v);
1398
+ const h = 0.05 + 0.12 * Math.pow(u, 0.8);
1399
+ const y = -h * Math.cos(Math.PI * v) + 0.012 * u * u;
1400
+ const bulge = 0.018 + 0.058 * Math.sin(Math.PI * (0.12 + 0.88 * u));
1401
+ const fold = 0.016 * Math.sin(3 * Math.PI * v) * Math.pow(1 - u, 1.3);
1402
+ return Q(dir * x, y, (bulge + fold) * Math.sin(Math.PI * v) + 0.004);
1403
+ };
1404
+ layers.push({
1405
+ parts: [
1406
+ surface(view, `bow:wing${dir}@${at}`, wing, nu, nv, (u) =>
1407
+ Q(dir * (0.058 + 0.3 * u), 0, -0.03),
1408
+ ),
1409
+ ],
1410
+ shade: wingShade,
1411
+ });
1412
+ /* the wing's end, closed down to the body */
1413
+ layers.push({
1414
+ parts: [
1415
+ surface(
1416
+ view,
1417
+ `bow:end${dir}@${at}`,
1418
+ (s, t) => {
1419
+ const p = wing(1, t);
1420
+ return [p[0], p[1], p[2] - (p[2] - zFront) * s];
1421
+ },
1422
+ 1,
1423
+ nv,
1424
+ Q(dir * 0.2, 0, -0.03),
1425
+ ),
1426
+ ],
1427
+ shade: wingShade,
1428
+ });
1429
+ }
1430
+ /* the knot: pinched at its waist, rounded all round */
1431
+ const knot = surface(
1432
+ view,
1433
+ `bow:knot@${at}`,
1434
+ (u, v) => {
1435
+ const x = 0.068 * (2 * u - 1) * (1 - 0.12 * Math.sin(Math.PI * v));
1436
+ const y = 0.075 * (2 * v - 1);
1437
+ const zx = Math.pow(Math.max(0, 1 - Math.pow(Math.abs(2 * u - 1), 3)), 0.5);
1438
+ const zy = Math.pow(Math.max(0, 1 - Math.pow(Math.abs(2 * v - 1), 5)), 0.4);
1439
+ return Q(x, y, 0.012 + 0.075 * zx * zy);
1440
+ },
1441
+ 6,
1442
+ 6,
1443
+ Q(0, 0, -0.02),
1444
+ );
1445
+ layers.push({ parts: [knot], shade: soft ? felt(view, rgb, 0.9) : satin(view, scale(rgb, 0.9)) });
1446
+ if (!soft) {
1447
+ paintLayers(ctx, layers, view.px);
1448
+ return;
1449
+ }
1450
+ /* felt: each wing and the knot in turn, each with its own grain and fuzz
1451
+ round its own outline (the bow's is not convex) */
1452
+ for (let i = 0; i < layers.length; i += 2) {
1453
+ const part = layers.slice(i, i + 2);
1454
+ const outline = hull(paintLayers(ctx, part, view.px));
1455
+ grainOver(ctx, outline, 0.5, 0.1);
1456
+ fuzz(ctx, outline, base, 41 + i, 0.3, view.px);
1457
+ }
1458
+ }
1459
+
1460
+ /* ── glasses: in face space, on the eyes ────────────────────────────── */
1461
+
1462
+ export interface EyeSpot {
1463
+ x: number;
1464
+ y: number;
1465
+ sx: number;
1466
+ sy: number;
1467
+ z: number;
1468
+ /** the eye's design position on the face, and the face sphere's depth there */
1469
+ ox: number;
1470
+ oy: number;
1471
+ fz: number;
1472
+ }
1473
+
1474
+ /* the frames: each lens's half width and height, how square its corners
1475
+ (a superellipse's exponent), the rim's half thickness across and its
1476
+ half depth */
1477
+ const FRAMES: Record<
1478
+ Exclude<BotAvatarGlasses, "none">,
1479
+ { rx: number; ry: number; n: number; tr: number; td: number }
1480
+ > = {
1481
+ round: { rx: 10.8, ry: 10.8, n: 2, tr: 1.15, td: 1.2 },
1482
+ square: { rx: 12, ry: 9.6, n: 4.2, tr: 1.45, td: 1.4 },
1483
+ shades: { rx: 12.6, ry: 10, n: 3.4, tr: 1.6, td: 1.5 },
1484
+ };
1485
+
1486
+ /** Glasses over two eye spots (face space, already placed on the face's
1487
+ sphere), turned rigidly with the head: acetate rims with rounded
1488
+ edges, a bridge, small metal rivets at the hinges, and lenses — clear
1489
+ with a reflection, or dark. Drawn after the face, unclipped, with a
1490
+ soft shadow on the face laid first (clipped by the caller's face clip
1491
+ through `shadow`). */
1492
+ export function drawGlasses(
1493
+ ctx: CanvasRenderingContext2D,
1494
+ kind: BotAvatarGlasses,
1495
+ eyes: [EyeSpot, EyeSpot],
1496
+ light: { lx: number; ly: number },
1497
+ pass: "shadow" | "frame",
1498
+ turn: { yaw: number; pitch: number } = { yaw: 0, pitch: 0 },
1499
+ ) {
1500
+ if (kind === "none") return;
1501
+ owner = FRAMES;
1502
+ const F = FRAMES[kind];
1503
+ const cy = Math.cos(turn.yaw),
1504
+ sy = Math.sin(turn.yaw),
1505
+ cp = Math.cos(turn.pitch),
1506
+ sp = Math.sin(turn.pitch);
1507
+ const { L: lightL, H: lightH } = lightOf(light.lx, light.ly);
1508
+ /* the head's turn, rigidly: yaw about y, then pitch about x */
1509
+ const view = makeView(
1510
+ [cy, 0, sy, sp * sy, cp, -sp * cy, -cp * sy, sp, cp * cy],
1511
+ [0, 0, 0],
1512
+ lightL,
1513
+ lightH,
1514
+ pxOf(ctx),
1515
+ );
1516
+ /* the front of the frame: a plane just off the face, wrapping back a
1517
+ little toward its outer edges */
1518
+ const zPlane = Math.max(eyes[0].fz, eyes[1].fz) + 3.2;
1519
+ const zAt = (x: number) => zPlane - 0.0045 * x * x;
1520
+ const e2 = 2 / F.n;
1521
+ const rimAt =
1522
+ (e: EyeSpot, grow: number) =>
1523
+ (u: number): V3 => {
1524
+ const t = u * TAU;
1525
+ const x = e.ox + (F.rx + grow) * spow(Math.cos(t), e2),
1526
+ y = e.oy + (F.ry + grow) * spow(Math.sin(t), e2);
1527
+ return [x, y, zAt(x)];
1528
+ };
1529
+ const ring = (e: EyeSpot, grow: number, n = 48): P2[] => {
1530
+ const f = rimAt(e, grow);
1531
+ const pts: P2[] = [];
1532
+ for (let i = 0; i < n; i++) pts.push(view.toScreen(f(i / n)));
1533
+ return pts;
1534
+ };
1535
+ const vis = eyes.map((e) => Math.max(0, Math.min(1, (e.z - 0.18) * 3.2)));
1536
+ if (pass === "shadow") {
1537
+ for (let i = 0; i < 2; i++) {
1538
+ if (vis[i] <= 0) continue;
1539
+ ctx.save();
1540
+ ctx.globalAlpha = 0.22 * vis[i];
1541
+ ctx.translate(-light.lx * 1.6, -light.ly * 1.6 + 0.8);
1542
+ ctx.lineWidth = F.tr * 2 + 0.6;
1543
+ ctx.lineJoin = "round";
1544
+ ctx.strokeStyle = "#000";
1545
+ ctx.stroke(polyPath(ring(eyes[i], 0)));
1546
+ ctx.restore();
1547
+ }
1548
+ return;
1549
+ }
1550
+ const frame: RGB = [22, 22, 26];
1551
+ const rimShade = plastic(view, frame, 1.1);
1552
+ /* the lenses first, under the rims */
1553
+ for (let i = 0; i < 2; i++) {
1554
+ if (vis[i] <= 0) continue;
1555
+ const lens = polyPath(ring(eyes[i], -F.tr * 0.5));
1556
+ const b = bounds(ring(eyes[i], 0, 16));
1557
+ ctx.save();
1558
+ ctx.globalAlpha = vis[i];
1559
+ if (kind === "shades") {
1560
+ const g = ctx.createLinearGradient(b.cx, b.y0, b.cx, b.y1);
1561
+ g.addColorStop(0, "rgba(58,60,78,0.97)");
1562
+ g.addColorStop(0.55, "rgba(18,18,26,0.98)");
1563
+ g.addColorStop(1, "rgba(30,26,44,0.98)");
1564
+ ctx.fillStyle = g;
1565
+ } else {
1566
+ const g = ctx.createLinearGradient(b.cx, b.y0, b.cx, b.y1);
1567
+ g.addColorStop(0, "rgba(236,244,255,0.26)");
1568
+ g.addColorStop(0.45, "rgba(214,230,255,0.08)");
1569
+ g.addColorStop(1, "rgba(214,230,255,0.14)");
1570
+ ctx.fillStyle = g;
1571
+ }
1572
+ ctx.fill(lens);
1573
+ ctx.clip(lens);
1574
+ /* the reflection: a broad soft band and a thin bright streak, slanting */
1575
+ const w = b.w,
1576
+ h = b.h;
1577
+ const streak = (x0: number, width: number, a: number) => {
1578
+ ctx.globalAlpha = vis[i] * a;
1579
+ ctx.fillStyle = "#fff";
1580
+ ctx.beginPath();
1581
+ ctx.moveTo(b.x0 + w * x0, b.y1 + h * 0.1);
1582
+ ctx.lineTo(b.x0 + w * (x0 + 0.55), b.y0 - h * 0.1);
1583
+ ctx.lineTo(b.x0 + w * (x0 + 0.55 + width), b.y0 - h * 0.1);
1584
+ ctx.lineTo(b.x0 + w * (x0 + width), b.y1 + h * 0.1);
1585
+ ctx.closePath();
1586
+ ctx.fill();
1587
+ };
1588
+ const k = kind === "shades" ? 1.4 : 1;
1589
+ streak(-0.28, 0.26, 0.16 * k);
1590
+ streak(0.06, 0.07, 0.3 * k);
1591
+ ctx.restore();
1592
+ }
1593
+ /* the rims, the bridge between them and the rivets at the hinges */
1594
+ const nr = segs(view, TAU * F.rx * 1.1, 28, 56, 8);
1595
+ const rim = (e: EyeSpot) =>
1596
+ tube(
1597
+ view,
1598
+ `rim:${kind}:${e.ox},${e.oy},${e.fz.toFixed(2)}`,
1599
+ rimAt(e, 0),
1600
+ [0, 0, 1],
1601
+ section(F.td, F.tr, 0.55),
1602
+ nr,
1603
+ 6,
1604
+ true,
1605
+ );
1606
+ const [L, R] = eyes;
1607
+ const bridgeAt = (u: number): V3 => {
1608
+ const round = kind === "round";
1609
+ const a: V3 = [L.ox + F.rx * (round ? 0.9 : 0.98), L.oy - F.ry * (round ? 0.32 : 0.42), 0];
1610
+ const b: V3 = [R.ox - F.rx * (round ? 0.9 : 0.98), R.oy - F.ry * (round ? 0.32 : 0.42), 0];
1611
+ const c: V3 = [0, (L.oy + R.oy) / 2 - F.ry * (round ? 0.8 : 0.6), 0];
1612
+ const m1 = 1 - u;
1613
+ const x = m1 * m1 * a[0] + 2 * m1 * u * c[0] + u * u * b[0],
1614
+ y = m1 * m1 * a[1] + 2 * m1 * u * c[1] + u * u * b[1];
1615
+ return [x, y, zAt(x) + 0.3];
1616
+ };
1617
+ const bridge = tube(
1618
+ view,
1619
+ `bridge:${kind}:${L.ox},${L.oy},${R.ox},${zPlane.toFixed(2)}`,
1620
+ bridgeAt,
1621
+ [0, 0, 1],
1622
+ section(F.td * 0.8, F.tr * 0.75, 0.7),
1623
+ 8,
1624
+ 6,
1625
+ );
1626
+ const parts = [
1627
+ { part: rim(L), a: vis[0], d: view.depth([L.ox, L.oy, zPlane]) },
1628
+ { part: rim(R), a: vis[1], d: view.depth([R.ox, R.oy, zPlane]) },
1629
+ { part: bridge, a: Math.min(vis[0], vis[1]), d: view.depth([0, L.oy, zPlane]) },
1630
+ ].sort((p, q) => p.d - q.d);
1631
+ for (const p of parts) {
1632
+ if (p.a <= 0) continue;
1633
+ ctx.save();
1634
+ ctx.globalAlpha = p.a;
1635
+ paintLayers(ctx, [{ parts: [p.part], shade: rimShade }], view.px);
1636
+ ctx.restore();
1637
+ }
1638
+ for (const [e, s, a] of [
1639
+ [L, -1, vis[0]],
1640
+ [R, 1, vis[1]],
1641
+ ] as const) {
1642
+ if (a <= 0) continue;
1643
+ ctx.save();
1644
+ ctx.globalAlpha = a;
1645
+ const x = e.ox + s * F.rx * 0.93;
1646
+ sphereSprite(ctx, view, [x, e.oy - F.ry * 0.5, zAt(x) + F.td], 0.75, "silver");
1647
+ ctx.restore();
1648
+ }
1649
+ }
1650
+
1651
+ /* ── the pass entry points ──────────────────────────────────────────── */
1652
+
1653
+ /** Behind the body: what of the headphones is behind its middle plane. */
1654
+ export function drawWearBehind(
1655
+ ctx: CanvasRenderingContext2D,
1656
+ path: Path2D,
1657
+ r: WearRig,
1658
+ wear: Wear,
1659
+ ) {
1660
+ if (!wear.headphones) return;
1661
+ const m = marksFor(path);
1662
+ if (!m) return;
1663
+ owner = m;
1664
+ drawPhones(ctx, viewOf(r, pxOf(ctx)), m, wear.color, "behind", null);
1665
+ }
1666
+
1667
+ /** Over the body: the hat, the headphones' near part, the bow tie. The
1668
+ `bodyClip` is the body's front outline, for the shadows things cast on
1669
+ it; `bodyZ` how far the body's front surface stands at a design point. */
1670
+ export function drawWearFront(
1671
+ ctx: CanvasRenderingContext2D,
1672
+ path: Path2D,
1673
+ r: WearRig,
1674
+ wear: Wear,
1675
+ bodyClip: Path2D | null,
1676
+ bodyZ: (x: number, y: number) => number,
1677
+ face: { y: number; scale: number },
1678
+ ) {
1679
+ const m = marksFor(path);
1680
+ if (!m) return;
1681
+ owner = m;
1682
+ const view = viewOf(r, pxOf(ctx));
1683
+ if (wear.bowTie) drawBowTie(ctx, view, r, m, wear.color, bodyZ, bodyClip, face, wear.fabric);
1684
+ if (wear.headphones) drawPhones(ctx, view, m, wear.color, "front", bodyClip);
1685
+ switch (wear.hat) {
1686
+ case "beret":
1687
+ drawBeret(ctx, view, m, wear.color, bodyClip);
1688
+ break;
1689
+ case "beanie":
1690
+ drawBeanie(ctx, view, m, wear.color, bodyClip);
1691
+ break;
1692
+ case "party":
1693
+ drawParty(ctx, view, m, wear.color, bodyClip);
1694
+ break;
1695
+ case "crown":
1696
+ drawCrown(ctx, view, m, bodyClip);
1697
+ break;
1698
+ }
1699
+ }