@heyramzi/ui 2.11.1 → 2.11.2

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