@heyramzi/ui 2.11.0 → 2.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/dist/agents/helpers.js +38 -38
  2. package/dist/agents/page-state.js +49 -50
  3. package/dist/agents/vendor/color.js +146 -127
  4. package/dist/agents/vendor/draw.js +617 -508
  5. package/dist/agents/vendor/engine.js +698 -599
  6. package/dist/agents/vendor/mesh.js +653 -683
  7. package/dist/agents/vendor/plastic.js +3913 -3112
  8. package/dist/agents/vendor/presets.js +163 -164
  9. package/dist/agents/vendor/shapes.js +21 -20
  10. package/dist/agents/vendor/ticker.js +41 -42
  11. package/dist/agents/vendor/types.js +1 -0
  12. package/dist/agents/vendor/wear.js +1421 -1448
  13. package/dist/ai/confirmation/confirmation-context.svelte.js +6 -6
  14. package/dist/ai/confirmation/index.js +3 -1
  15. package/dist/ai/index.js +33 -0
  16. package/dist/ai/prompt-input/index.js +3 -1
  17. package/dist/ai/reasoning/index.js +3 -1
  18. package/dist/ai/reasoning/reasoning-context.svelte.js +6 -6
  19. package/dist/ai/suggestion/index.js +3 -1
  20. package/dist/ai/typing-dots/index.js +3 -1
  21. package/dist/animations/text-swap.js +24 -24
  22. package/dist/brand/index.js +54 -48
  23. package/dist/canvas/card/resize.js +5 -3
  24. package/dist/canvas/edges/edge-path.js +255 -242
  25. package/dist/canvas/editor/context-menu.js +39 -36
  26. package/dist/canvas/editor/dispatch.svelte.js +154 -151
  27. package/dist/canvas/editor/editor-context.js +7 -2
  28. package/dist/canvas/editor/editor-mode.js +1 -0
  29. package/dist/canvas/editor/frame.js +73 -76
  30. package/dist/canvas/editor/hand-context.js +5 -2
  31. package/dist/canvas/editor/history-core.js +163 -191
  32. package/dist/canvas/editor/keymap.js +95 -169
  33. package/dist/canvas/editor/layout-actions.js +143 -144
  34. package/dist/canvas/editor/lock.js +52 -51
  35. package/dist/canvas/editor/rebase.js +217 -240
  36. package/dist/canvas/editor/style.js +132 -137
  37. package/dist/canvas/editor/text-field-context.js +7 -2
  38. package/dist/canvas/editor/z-order.js +65 -70
  39. package/dist/canvas/headless/alignment.js +22 -16
  40. package/dist/canvas/headless/break-apart-find.js +286 -279
  41. package/dist/canvas/headless/break-apart-pieces.js +62 -58
  42. package/dist/canvas/headless/break-apart-plan.js +299 -325
  43. package/dist/canvas/headless/break-apart-style.js +75 -70
  44. package/dist/canvas/headless/break-apart.js +188 -181
  45. package/dist/canvas/headless/canvas-cleanup.js +78 -58
  46. package/dist/canvas/headless/edge-anchors.js +115 -116
  47. package/dist/canvas/headless/edge-memo.js +24 -22
  48. package/dist/canvas/headless/edge-serialization.js +17 -27
  49. package/dist/canvas/headless/figure-geometry.js +232 -264
  50. package/dist/canvas/headless/fit-viewport.js +84 -67
  51. package/dist/canvas/headless/grid.js +88 -86
  52. package/dist/canvas/headless/icon-node.js +44 -36
  53. package/dist/canvas/headless/index.js +1 -0
  54. package/dist/canvas/headless/linkify.js +17 -19
  55. package/dist/canvas/headless/pen-marks.js +101 -95
  56. package/dist/canvas/headless/sketch.js +231 -209
  57. package/dist/canvas/headless/snap-guides.js +99 -83
  58. package/dist/canvas/headless/svg-figure.js +133 -117
  59. package/dist/canvas/headless/table-layout.js +146 -107
  60. package/dist/canvas/index.js +2 -0
  61. package/dist/canvas/lod/lod-mode.svelte.js +20 -18
  62. package/dist/canvas/lod/render-all.svelte.js +36 -39
  63. package/dist/canvas/ops/arrange.js +78 -71
  64. package/dist/canvas/ops/ops.js +260 -255
  65. package/dist/canvas/pen/colors.js +37 -35
  66. package/dist/canvas/pen/eraser.js +28 -27
  67. package/dist/canvas/pen/geometry.js +120 -130
  68. package/dist/canvas/pen/index.js +1 -0
  69. package/dist/canvas/pen/ops.js +11 -11
  70. package/dist/canvas/pen/pen-session.js +150 -159
  71. package/dist/canvas/pen/plugin.js +107 -104
  72. package/dist/canvas/pen/shapes.js +73 -68
  73. package/dist/canvas/pen/stroke-path.js +32 -30
  74. package/dist/canvas/pen/types.js +21 -21
  75. package/dist/canvas/pen/ui.js +10 -8
  76. package/dist/canvas/plugin.js +46 -46
  77. package/dist/canvas/screen/glyphs.js +12 -11
  78. package/dist/canvas/screen/labels.js +22 -22
  79. package/dist/canvas/screen/parts.js +98 -111
  80. package/dist/canvas/screen/plugin.js +47 -56
  81. package/dist/canvas/screen/types.js +30 -35
  82. package/dist/canvas/screen/ui.js +7 -7
  83. package/dist/canvas/test-utils.js +14 -14
  84. package/dist/canvas/types.js +1 -0
  85. package/dist/canvas/ui/forward-wheel.js +24 -25
  86. package/dist/canvas/ui/icons.js +64 -131
  87. package/dist/canvas/ui/place-panel.js +58 -49
  88. package/dist/canvas/ui/shortcut-tip.js +14 -12
  89. package/dist/canvas/ui/tools.js +300 -385
  90. package/dist/charts/dashboard/chart.js +4 -0
  91. package/dist/charts/dashboard/data-table.js +11 -0
  92. package/dist/charts/dashboard/windows.js +8 -3
  93. package/dist/charts/edge-fade.js +10 -5
  94. package/dist/charts/hover-band.js +16 -11
  95. package/dist/charts/index.js +0 -1
  96. package/dist/charts/motion.js +11 -7
  97. package/dist/charts/primitives/chart-utils.js +12 -12
  98. package/dist/components/copy/clipboard.js +32 -32
  99. package/dist/components/copy/copied.svelte.js +31 -23
  100. package/dist/components/data-table/data-table.js +39 -39
  101. package/dist/components/docs/index.js +4 -0
  102. package/dist/components/docs/scroll-spy.svelte.js +73 -68
  103. package/dist/components/list-page/filter-section.js +4 -1
  104. package/dist/components/list-page/index.js +2 -0
  105. package/dist/components/list-page/list-table.js +7 -2
  106. package/dist/components/list-page/queue-rail.js +14 -0
  107. package/dist/components/list-page/section-host.js +4 -2
  108. package/dist/components/list-page/step.js +4 -3
  109. package/dist/components/list-page/types.js +1 -0
  110. package/dist/components/picker/trigger.js +48 -30
  111. package/dist/components/picker/types.js +1 -0
  112. package/dist/components/segmented/collapse.js +38 -27
  113. package/dist/components/segmented/index.js +8 -0
  114. package/dist/components/segmented/segmented.js +67 -42
  115. package/dist/components/star-rating-input/index.js +3 -1
  116. package/dist/components/table-view/types.js +1 -0
  117. package/dist/components/tags-input/tags.js +81 -69
  118. package/dist/components/theme/index.js +4 -0
  119. package/dist/components/toolbar/index.js +4 -0
  120. package/dist/components/toolbar/toolbar-trigger.js +14 -1
  121. package/dist/editor/anchors.js +17 -13
  122. package/dist/editor/block-id.js +52 -51
  123. package/dist/editor/block-ops.js +108 -80
  124. package/dist/editor/doc-blocks.js +155 -166
  125. package/dist/editor/extensions.js +139 -180
  126. package/dist/editor/index.js +4 -0
  127. package/dist/editor/markdown-test-helpers.js +4 -2
  128. package/dist/editor/markdown.js +420 -408
  129. package/dist/editor/serializer.js +204 -200
  130. package/dist/editor/slash-items.js +89 -101
  131. package/dist/editor/youtube-url.js +25 -20
  132. package/dist/effects/animated-beam/path-calculator.svelte.js +43 -38
  133. package/dist/effects/animated-beam/types.js +1 -0
  134. package/dist/effects/border-beam/css.js +92 -78
  135. package/dist/effects/border-beam/presets.js +1705 -1720
  136. package/dist/effects/border-beam/register-properties.js +43 -33
  137. package/dist/effects/border-beam/types.js +1 -0
  138. package/dist/effects/image-comparison/compare.js +3 -2
  139. package/dist/effects/image-reveal/types.js +1 -0
  140. package/dist/effects/magnetic/magnetic.js +58 -52
  141. package/dist/effects/reveal/observe.js +50 -40
  142. package/dist/effects/reveal/types.js +1 -0
  143. package/dist/effects/shader-gradient/color.js +36 -29
  144. package/dist/effects/shader-gradient/renderer.js +384 -295
  145. package/dist/effects/shader-gradient/shaders.js +31 -0
  146. package/dist/effects/shader-gradient/types.js +1 -0
  147. package/dist/icons/apple-symbol-exports.js +99 -124
  148. package/dist/icons/sf-symbols.js +357 -461
  149. package/dist/icons/trades.js +43 -28
  150. package/dist/inbox/index.js +1 -0
  151. package/dist/inbox/logic.js +126 -122
  152. package/dist/inbox/types.js +43 -43
  153. package/dist/lib/arrive.js +19 -12
  154. package/dist/lib/icon-types.js +1 -0
  155. package/dist/lib/initials.js +16 -10
  156. package/dist/lib/layout.js +5 -0
  157. package/dist/lib/panel.js +13 -0
  158. package/dist/lib/result.js +22 -22
  159. package/dist/lib/sliding-pill.js +59 -44
  160. package/dist/lib/theme-transition.js +43 -43
  161. package/dist/lib/tone.js +31 -24
  162. package/dist/lib/utils.js +3 -1
  163. package/dist/onboarding/define-tours.js +24 -23
  164. package/dist/onboarding/onboarding.svelte.js +16 -12
  165. package/dist/onboarding/pick.js +10 -6
  166. package/dist/onboarding/stores.js +40 -41
  167. package/dist/onboarding/wait.js +24 -23
  168. package/dist/panel/side-panel-types.js +1 -0
  169. package/dist/record/demo-data.js +459 -486
  170. package/dist/record/fields.js +129 -131
  171. package/dist/record/format.js +70 -82
  172. package/dist/record/gutter.js +1 -0
  173. package/dist/record/kinds.js +95 -105
  174. package/dist/record/logic.js +238 -219
  175. package/dist/record/types.js +49 -48
  176. package/dist/shadcn/accordion/index.js +3 -1
  177. package/dist/shadcn/alert/index.js +3 -1
  178. package/dist/shadcn/alert-dialog/index.js +3 -1
  179. package/dist/shadcn/attachment/index.js +3 -1
  180. package/dist/shadcn/avatar/index.js +3 -1
  181. package/dist/shadcn/breadcrumb/index.js +3 -1
  182. package/dist/shadcn/bubble/index.js +3 -1
  183. package/dist/shadcn/button/index.js +3 -1
  184. package/dist/shadcn/calendar/index.js +3 -1
  185. package/dist/shadcn/card/index.js +3 -1
  186. package/dist/shadcn/checkbox/index.js +3 -1
  187. package/dist/shadcn/code-block/index.js +3 -1
  188. package/dist/shadcn/collapsible/index.js +3 -1
  189. package/dist/shadcn/command/index.js +4 -1
  190. package/dist/shadcn/context-menu/index.js +3 -1
  191. package/dist/shadcn/dialog/index.js +3 -1
  192. package/dist/shadcn/dropdown-menu/menu-item.js +40 -20
  193. package/dist/shadcn/empty/index.js +3 -1
  194. package/dist/shadcn/field/index.js +3 -1
  195. package/dist/shadcn/hooks/highlighter.js +77 -75
  196. package/dist/shadcn/hooks/is-mobile.svelte.js +3 -3
  197. package/dist/shadcn/hooks/use-highlighted-code.svelte.js +62 -62
  198. package/dist/shadcn/input/index.js +3 -1
  199. package/dist/shadcn/kbd/index.js +3 -1
  200. package/dist/shadcn/label/index.js +3 -1
  201. package/dist/shadcn/marker/index.js +3 -1
  202. package/dist/shadcn/message/index.js +3 -1
  203. package/dist/shadcn/message-scroller/index.js +3 -1
  204. package/dist/shadcn/popover/index.js +3 -1
  205. package/dist/shadcn/progress/index.js +3 -1
  206. package/dist/shadcn/progress/tone.js +8 -7
  207. package/dist/shadcn/radio-group/index.js +3 -1
  208. package/dist/shadcn/scroll-area/index.js +3 -1
  209. package/dist/shadcn/select/index.js +3 -1
  210. package/dist/shadcn/separator/index.js +3 -1
  211. package/dist/shadcn/sheet/index.js +3 -1
  212. package/dist/shadcn/sidebar/context.svelte.js +42 -28
  213. package/dist/shadcn/sidebar/index.js +3 -1
  214. package/dist/shadcn/skeleton/index.js +3 -1
  215. package/dist/shadcn/slider/index.js +3 -1
  216. package/dist/shadcn/switch/index.js +3 -1
  217. package/dist/shadcn/table/index.js +3 -1
  218. package/dist/shadcn/tabs/index.js +3 -1
  219. package/dist/shadcn/textarea/index.js +3 -1
  220. package/dist/shadcn/toggle/index.js +3 -1
  221. package/dist/shadcn/toggle-group/index.js +3 -1
  222. package/dist/shadcn/tooltip/context.js +7 -2
  223. package/dist/shadcn/tooltip/index.js +3 -1
  224. package/dist/shadcn/trace/index.js +3 -1
  225. package/dist/slides/bridges.js +65 -59
  226. package/dist/slides/deck-controller.svelte.js +210 -207
  227. package/dist/slides/export/capture.js +66 -71
  228. package/dist/slides/export/ffmpeg.js +76 -72
  229. package/dist/slides/export/index.js +3 -0
  230. package/dist/slides/export/launch.js +9 -6
  231. package/dist/slides/export/pdf.js +60 -54
  232. package/dist/slides/export/types.js +1 -0
  233. package/dist/slides/labels.js +24 -24
  234. package/dist/slides/layout.js +39 -41
  235. package/dist/slides/model.js +9 -7
  236. package/dist/slides/navigation.js +78 -73
  237. package/dist/slides/types.js +2 -2
  238. package/dist/status/define-statuses.js +18 -14
  239. package/dist/status/status-types.js +1 -0
  240. package/dist/test-setup.js +24 -20
  241. package/dist/thread/logic.js +73 -77
  242. package/dist/thread/types.js +21 -21
  243. package/dist/work/board-order.js +94 -90
  244. package/dist/work/dates.js +16 -9
  245. package/dist/work/detail/detail-types.js +18 -18
  246. package/dist/work/docs/edited.js +32 -29
  247. package/dist/work/docs/index.js +4 -0
  248. package/dist/work/docs/labels.js +34 -34
  249. package/dist/work/docs/menu.js +113 -106
  250. package/dist/work/docs/outline.js +39 -29
  251. package/dist/work/gantt/logic.js +211 -235
  252. package/dist/work/inbox/inbox-logic.js +71 -75
  253. package/dist/work/inbox/inbox-statuses.js +75 -72
  254. package/dist/work/inbox/inbox-types.js +1 -0
  255. package/dist/work/kanban-drag.js +27 -22
  256. package/dist/work/kanban-types.js +1 -0
  257. package/dist/work/presets/sales.js +36 -32
  258. package/dist/work/reorder.js +9 -8
  259. package/dist/work/task-types.js +1 -0
  260. package/dist/work/time/duration.js +51 -46
  261. package/dist/work/time/time-types.js +23 -23
  262. package/dist/work/use-kanban-dnd.svelte.js +126 -125
  263. package/package.json +3 -1
  264. package/src/components/list-page/master-detail.svelte +3 -1
  265. package/src/components/list-page/rail-row.svelte +4 -4
  266. package/src/components/tags-input/tags-input.svelte +2 -0
  267. package/src/editor/markdown.ts +30 -35
@@ -1,727 +1,697 @@
1
1
  export function makeView(m, o, L, H, px) {
2
- return {
3
- m,
4
- o,
5
- L,
6
- H,
7
- px,
8
- toScreen: (p)=>{
9
- const x = p[0] - o[0], y = p[1] - o[1], z = p[2] - o[2];
10
- return [
11
- m[0] * x + m[1] * y + m[2] * z,
12
- m[3] * x + m[4] * y + m[5] * z
13
- ];
14
- },
15
- depth: (p)=>m[6] * (p[0] - o[0]) + m[7] * (p[1] - o[1]) + m[8] * (p[2] - o[2]),
16
- dir: (v)=>norm([
17
- m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
18
- m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
19
- m[6] * v[0] + m[7] * v[1] + m[8] * v[2]
20
- ])
21
- };
2
+ return {
3
+ m,
4
+ o,
5
+ L,
6
+ H,
7
+ px,
8
+ toScreen: (p) => {
9
+ const x = p[0] - o[0], y = p[1] - o[1], z = p[2] - o[2];
10
+ return [m[0] * x + m[1] * y + m[2] * z, m[3] * x + m[4] * y + m[5] * z];
11
+ },
12
+ depth: (p) => m[6] * (p[0] - o[0]) + m[7] * (p[1] - o[1]) + m[8] * (p[2] - o[2]),
13
+ dir: (v) => norm([
14
+ m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
15
+ m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
16
+ m[6] * v[0] + m[7] * v[1] + m[8] * v[2]
17
+ ])
18
+ };
22
19
  }
23
- export const sub = (a, b)=>[
24
- a[0] - b[0],
25
- a[1] - b[1],
26
- a[2] - b[2]
27
- ];
28
- export const cross = (a, b)=>[
29
- a[1] * b[2] - a[2] * b[1],
30
- a[2] * b[0] - a[0] * b[2],
31
- a[0] * b[1] - a[1] * b[0]
32
- ];
33
- export const dot = (a, b)=>a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
34
- export const norm = (v)=>{
35
- const l = Math.hypot(v[0], v[1], v[2]) || 1;
36
- return [
37
- v[0] / l,
38
- v[1] / l,
39
- v[2] / l
40
- ];
20
+ export const sub = (a, b) => [
21
+ a[0] - b[0],
22
+ a[1] - b[1],
23
+ a[2] - b[2]
24
+ ];
25
+ export const cross = (a, b) => [
26
+ a[1] * b[2] - a[2] * b[1],
27
+ a[2] * b[0] - a[0] * b[2],
28
+ a[0] * b[1] - a[1] * b[0]
29
+ ];
30
+ export const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
31
+ export const norm = (v) => {
32
+ const l = Math.hypot(v[0], v[1], v[2]) || 1;
33
+ return [
34
+ v[0] / l,
35
+ v[1] / l,
36
+ v[2] / l
37
+ ];
41
38
  };
42
39
  const TAU = Math.PI * 2;
40
+ /** How many segments to cut a length into for this view: about one per
41
+ `quad` device pixels, within `lo … hi`. */
43
42
  export function segs(view, len, lo, hi, quad = 11) {
44
- return Math.max(lo, Math.min(hi, Math.round(len * view.px / quad)));
43
+ return Math.max(lo, Math.min(hi, Math.round(len * view.px / quad)));
45
44
  }
45
+ /**
46
+ * Sample `surf(u, v)` (u, v ∈ [0, 1]) on an `nu × nv` grid. `inside` is a
47
+ * point inside the object, or for a tube the point on its centre line:
48
+ * each normal is turned to face away from it, so a surface needs no care
49
+ * over its winding. With `smooth`, every corner gets the mean of its
50
+ * cells' normals — a surface closed round u, or pinched to a pole, is
51
+ * joined up — for the gradient fill; without, the cells stay faceted.
52
+ */
46
53
  export function makeMesh(surf, nu, nv, inside, smooth = true) {
47
- const W = nu + 1, N = W * (nv + 1);
48
- const pos = new Float64Array(N * 3);
49
- let ext = 0;
50
- for(let j = 0, k = 0; j <= nv; j++){
51
- for(let i = 0; i <= nu; i++, k++){
52
- const p = surf(i / nu, j / nv);
53
- pos[k * 3] = p[0];
54
- pos[k * 3 + 1] = p[1];
55
- pos[k * 3 + 2] = p[2];
56
- ext = Math.max(ext, Math.abs(p[0] - pos[0]) + Math.abs(p[1] - pos[1]) + Math.abs(p[2] - pos[2]));
57
- }
58
- }
59
- const P = (k)=>[
60
- pos[k * 3],
61
- pos[k * 3 + 1],
62
- pos[k * 3 + 2]
63
- ];
64
- const fn = new Float64Array(nu * nv * 3);
65
- for(let j = 0; j < nv; j++){
66
- for(let i = 0; i < nu; i++){
67
- const a = j * W + i;
68
- const A = P(a), B = P(a + 1), C = P(a + W + 1), D = P(a + W);
69
- let n = cross(sub(C, A), sub(D, B));
70
- const mid = [
71
- (A[0] + B[0] + C[0] + D[0]) / 4,
72
- (A[1] + B[1] + C[1] + D[1]) / 4,
73
- (A[2] + B[2] + C[2] + D[2]) / 4
74
- ];
75
- const ins = typeof inside === "function" ? inside((i + 0.5) / nu, (j + 0.5) / nv) : inside;
76
- if (dot(n, sub(mid, ins)) < 0) n = [
77
- -n[0],
78
- -n[1],
79
- -n[2]
80
- ];
81
- n = norm(n);
82
- const f = (j * nu + i) * 3;
83
- fn[f] = n[0];
84
- fn[f + 1] = n[1];
85
- fn[f + 2] = n[2];
86
- }
87
- }
88
- const eps = 1e-6 + ext * 1e-5;
89
- const same = (p, q)=>Math.abs(pos[p * 3] - pos[q * 3]) + Math.abs(pos[p * 3 + 1] - pos[q * 3 + 1]) + Math.abs(pos[p * 3 + 2] - pos[q * 3 + 2]) < eps;
90
- let closed = true;
91
- for(let j = 0; j <= nv && closed; j++)closed = same(j * W, j * W + nu);
92
- let vn = null;
93
- if (smooth) {
94
- const acc = new Float64Array(N * 3);
95
- for(let j = 0; j < nv; j++){
96
- for(let i = 0; i < nu; i++){
97
- const f = (j * nu + i) * 3;
98
- const a = j * W + i;
99
- for (const k of [
100
- a,
101
- a + 1,
102
- a + W + 1,
103
- a + W
104
- ]){
105
- acc[k * 3] += fn[f];
106
- acc[k * 3 + 1] += fn[f + 1];
107
- acc[k * 3 + 2] += fn[f + 2];
108
- }
109
- }
110
- }
111
- const join = (ks)=>{
112
- let x = 0, y = 0, z = 0;
113
- for (const k of ks){
114
- x += acc[k * 3];
115
- y += acc[k * 3 + 1];
116
- z += acc[k * 3 + 2];
117
- }
118
- for (const k of ks){
119
- acc[k * 3] = x;
120
- acc[k * 3 + 1] = y;
121
- acc[k * 3 + 2] = z;
122
- }
123
- };
124
- if (closed) for(let j = 0; j <= nv; j++)join([
125
- j * W,
126
- j * W + nu
127
- ]);
128
- for (const j of [
129
- 0,
130
- nv
131
- ]){
132
- let pole = true;
133
- for(let i = 1; i <= nu && pole; i++)pole = same(j * W, j * W + i);
134
- if (pole) join(Array.from({
135
- length: W
136
- }, (_, i)=>j * W + i));
137
- }
138
- for(let k = 0; k < N; k++){
139
- const x = acc[k * 3], y = acc[k * 3 + 1], z = acc[k * 3 + 2];
140
- const l = Math.hypot(x, y, z) || 1;
141
- acc[k * 3] = x / l;
142
- acc[k * 3 + 1] = y / l;
143
- acc[k * 3 + 2] = z / l;
144
- }
145
- vn = acc;
146
- }
147
- return {
148
- nu,
149
- nv,
150
- closed,
151
- pos,
152
- fn,
153
- vn
154
- };
54
+ const W = nu + 1, N = W * (nv + 1);
55
+ const pos = new Float64Array(N * 3);
56
+ let ext = 0;
57
+ for (let j = 0, k = 0; j <= nv; j++) {
58
+ for (let i = 0; i <= nu; i++, k++) {
59
+ const p = surf(i / nu, j / nv);
60
+ pos[k * 3] = p[0];
61
+ pos[k * 3 + 1] = p[1];
62
+ pos[k * 3 + 2] = p[2];
63
+ ext = Math.max(ext, Math.abs(p[0] - pos[0]) + Math.abs(p[1] - pos[1]) + Math.abs(p[2] - pos[2]));
64
+ }
65
+ }
66
+ const P = (k) => [
67
+ pos[k * 3],
68
+ pos[k * 3 + 1],
69
+ pos[k * 3 + 2]
70
+ ];
71
+ const fn = new Float64Array(nu * nv * 3);
72
+ for (let j = 0; j < nv; j++) {
73
+ for (let i = 0; i < nu; i++) {
74
+ const a = j * W + i;
75
+ const A = P(a), B = P(a + 1), C = P(a + W + 1), D = P(a + W);
76
+ /* the diagonals' cross product survives a quad that pinches to a
77
+ point at a pole */
78
+ let n = cross(sub(C, A), sub(D, B));
79
+ const mid = [
80
+ (A[0] + B[0] + C[0] + D[0]) / 4,
81
+ (A[1] + B[1] + C[1] + D[1]) / 4,
82
+ (A[2] + B[2] + C[2] + D[2]) / 4
83
+ ];
84
+ const ins = typeof inside === "function" ? inside((i + .5) / nu, (j + .5) / nv) : inside;
85
+ if (dot(n, sub(mid, ins)) < 0) n = [
86
+ -n[0],
87
+ -n[1],
88
+ -n[2]
89
+ ];
90
+ n = norm(n);
91
+ const f = (j * nu + i) * 3;
92
+ fn[f] = n[0];
93
+ fn[f + 1] = n[1];
94
+ fn[f + 2] = n[2];
95
+ }
96
+ }
97
+ const eps = 1e-6 + ext * 1e-5;
98
+ const same = (p, q) => Math.abs(pos[p * 3] - pos[q * 3]) + Math.abs(pos[p * 3 + 1] - pos[q * 3 + 1]) + Math.abs(pos[p * 3 + 2] - pos[q * 3 + 2]) < eps;
99
+ let closed = true;
100
+ for (let j = 0; j <= nv && closed; j++) closed = same(j * W, j * W + nu);
101
+ let vn = null;
102
+ if (smooth) {
103
+ const acc = new Float64Array(N * 3);
104
+ for (let j = 0; j < nv; j++) {
105
+ for (let i = 0; i < nu; i++) {
106
+ const f = (j * nu + i) * 3;
107
+ const a = j * W + i;
108
+ for (const k of [
109
+ a,
110
+ a + 1,
111
+ a + W + 1,
112
+ a + W
113
+ ]) {
114
+ acc[k * 3] += fn[f];
115
+ acc[k * 3 + 1] += fn[f + 1];
116
+ acc[k * 3 + 2] += fn[f + 2];
117
+ }
118
+ }
119
+ }
120
+ const join = (ks) => {
121
+ let x = 0, y = 0, z = 0;
122
+ for (const k of ks) {
123
+ x += acc[k * 3];
124
+ y += acc[k * 3 + 1];
125
+ z += acc[k * 3 + 2];
126
+ }
127
+ for (const k of ks) {
128
+ acc[k * 3] = x;
129
+ acc[k * 3 + 1] = y;
130
+ acc[k * 3 + 2] = z;
131
+ }
132
+ };
133
+ if (closed) for (let j = 0; j <= nv; j++) join([j * W, j * W + nu]);
134
+ for (const j of [0, nv]) {
135
+ let pole = true;
136
+ for (let i = 1; i <= nu && pole; i++) pole = same(j * W, j * W + i);
137
+ if (pole) join(Array.from({ length: W }, (_, i) => j * W + i));
138
+ }
139
+ for (let k = 0; k < N; k++) {
140
+ const x = acc[k * 3], y = acc[k * 3 + 1], z = acc[k * 3 + 2];
141
+ const l = Math.hypot(x, y, z) || 1;
142
+ acc[k * 3] = x / l;
143
+ acc[k * 3 + 1] = y / l;
144
+ acc[k * 3 + 2] = z / l;
145
+ }
146
+ vn = acc;
147
+ }
148
+ return {
149
+ nu,
150
+ nv,
151
+ closed,
152
+ pos,
153
+ fn,
154
+ vn
155
+ };
155
156
  }
157
+ /**
158
+ * A tube along a curve: `path(u)` its centre line, `up` the direction its
159
+ * cross-section is squared to (a band's depth axis, a rim's face normal),
160
+ * `prof(u, φ)` the cross-section's offset at angle φ — along the up
161
+ * direction and across it. Rows of the grid run along the tube.
162
+ */
156
163
  export function makeTube(path, up, prof, nu, nv, closed = false) {
157
- const e = 1e-3;
158
- const at = (u)=>path(closed ? (u % 1 + 1) % 1 : Math.max(0, Math.min(1, u)));
159
- return makeMesh((u, v)=>{
160
- const c = path(u);
161
- const T = norm(sub(at(u + e), at(u - e)));
162
- const k = dot(up, T);
163
- const Nn = norm([
164
- up[0] - T[0] * k,
165
- up[1] - T[1] * k,
166
- up[2] - T[2] * k
167
- ]);
168
- const Bn = cross(T, Nn);
169
- const [a, b] = prof(u, v * TAU);
170
- return [
171
- c[0] + Nn[0] * a + Bn[0] * b,
172
- c[1] + Nn[1] * a + Bn[1] * b,
173
- c[2] + Nn[2] * a + Bn[2] * b
174
- ];
175
- }, nu, nv, (u)=>path(u));
164
+ const e = .001;
165
+ const at = (u) => path(closed ? (u % 1 + 1) % 1 : Math.max(0, Math.min(1, u)));
166
+ return makeMesh((u, v) => {
167
+ const c = path(u);
168
+ const T = norm(sub(at(u + e), at(u - e)));
169
+ const k = dot(up, T);
170
+ const Nn = norm([
171
+ up[0] - T[0] * k,
172
+ up[1] - T[1] * k,
173
+ up[2] - T[2] * k
174
+ ]);
175
+ const Bn = cross(T, Nn);
176
+ const [a, b] = prof(u, v * TAU);
177
+ return [
178
+ c[0] + Nn[0] * a + Bn[0] * b,
179
+ c[1] + Nn[1] * a + Bn[1] * b,
180
+ c[2] + Nn[2] * a + Bn[2] * b
181
+ ];
182
+ }, nu, nv, (u) => path(u));
176
183
  }
177
- export const section = (a, b, p = 1)=>(_u, ph)=>{
178
- const c = Math.cos(ph), s = Math.sin(ph);
179
- return [
180
- a * Math.sign(c) * Math.pow(Math.abs(c), p),
181
- b * Math.sign(s) * Math.pow(Math.abs(s), p)
182
- ];
183
- };
184
+ /** A cross-section: an ellipse (`p` = 1) squared toward a rounded box as
185
+ `p` falls, `a` along the up direction and `b` across it. */
186
+ export const section = (a, b, p = 1) => (_u, ph) => {
187
+ const c = Math.cos(ph), s = Math.sin(ph);
188
+ return [a * Math.sign(c) * Math.pow(Math.abs(c), p), b * Math.sign(s) * Math.pow(Math.abs(s), p)];
189
+ };
184
190
  export function projectMesh(view, mesh) {
185
- const { nu, nv, pos, fn, vn } = mesh;
186
- const W = nu + 1, N = W * (nv + 1), C = nu * nv;
187
- const [m0, m1, m2, m3, m4, m5, m6, m7, m8] = view.m;
188
- const [ox, oy, oz] = view.o;
189
- const x = new Float64Array(N), y = new Float64Array(N), d = new Float64Array(N);
190
- for(let k = 0; k < N; k++){
191
- const X = pos[k * 3] - ox, Y = pos[k * 3 + 1] - oy, Z = pos[k * 3 + 2] - oz;
192
- x[k] = m0 * X + m1 * Y + m2 * Z;
193
- y[k] = m3 * X + m4 * Y + m5 * Z;
194
- d[k] = m6 * X + m7 * Y + m8 * Z;
195
- }
196
- const turn = (src, dst, count)=>{
197
- for(let k = 0; k < count; k++){
198
- const a = src[k * 3], b = src[k * 3 + 1], c = src[k * 3 + 2];
199
- const X = m0 * a + m1 * b + m2 * c, Y = m3 * a + m4 * b + m5 * c, Z = m6 * a + m7 * b + m8 * c;
200
- const l = Math.hypot(X, Y, Z) || 1;
201
- dst[k * 3] = X / l;
202
- dst[k * 3 + 1] = Y / l;
203
- dst[k * 3 + 2] = Z / l;
204
- }
205
- };
206
- let n = null;
207
- if (vn) turn(vn, n = new Float64Array(N * 3), N);
208
- const f = new Float64Array(C * 3), cd = new Float64Array(C);
209
- turn(fn, f, C);
210
- for(let j = 0, c = 0; j < nv; j++){
211
- for(let i = 0; i < nu; i++, c++){
212
- const a = j * W + i;
213
- cd[c] = (d[a] + d[a + 1] + d[a + W] + d[a + W + 1]) / 4;
214
- }
215
- }
216
- return {
217
- nu,
218
- nv,
219
- closed: mesh.closed,
220
- x,
221
- y,
222
- d,
223
- n,
224
- f,
225
- cd
226
- };
191
+ const { nu, nv, pos, fn, vn } = mesh;
192
+ const W = nu + 1, N = W * (nv + 1), C = nu * nv;
193
+ const [m0, m1, m2, m3, m4, m5, m6, m7, m8] = view.m;
194
+ const [ox, oy, oz] = view.o;
195
+ const x = new Float64Array(N), y = new Float64Array(N), d = new Float64Array(N);
196
+ for (let k = 0; k < N; k++) {
197
+ const X = pos[k * 3] - ox, Y = pos[k * 3 + 1] - oy, Z = pos[k * 3 + 2] - oz;
198
+ x[k] = m0 * X + m1 * Y + m2 * Z;
199
+ y[k] = m3 * X + m4 * Y + m5 * Z;
200
+ d[k] = m6 * X + m7 * Y + m8 * Z;
201
+ }
202
+ const turn = (src, dst, count) => {
203
+ for (let k = 0; k < count; k++) {
204
+ const a = src[k * 3], b = src[k * 3 + 1], c = src[k * 3 + 2];
205
+ const X = m0 * a + m1 * b + m2 * c, Y = m3 * a + m4 * b + m5 * c, Z = m6 * a + m7 * b + m8 * c;
206
+ const l = Math.hypot(X, Y, Z) || 1;
207
+ dst[k * 3] = X / l;
208
+ dst[k * 3 + 1] = Y / l;
209
+ dst[k * 3 + 2] = Z / l;
210
+ }
211
+ };
212
+ let n = null;
213
+ if (vn) turn(vn, n = new Float64Array(N * 3), N);
214
+ const f = new Float64Array(C * 3), cd = new Float64Array(C);
215
+ turn(fn, f, C);
216
+ for (let j = 0, c = 0; j < nv; j++) {
217
+ for (let i = 0; i < nu; i++, c++) {
218
+ const a = j * W + i;
219
+ cd[c] = (d[a] + d[a + 1] + d[a + W] + d[a + W + 1]) / 4;
220
+ }
221
+ }
222
+ return {
223
+ nu,
224
+ nv,
225
+ closed: mesh.closed,
226
+ x,
227
+ y,
228
+ d,
229
+ n,
230
+ f,
231
+ cd
232
+ };
227
233
  }
234
+ /** The screen bounds of some projected meshes. */
228
235
  export function boundsOf(ps) {
229
- let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
230
- for (const p of ps){
231
- for(let k = 0; k < p.x.length; k++){
232
- if (p.x[k] < x0) x0 = p.x[k];
233
- if (p.x[k] > x1) x1 = p.x[k];
234
- if (p.y[k] < y0) y0 = p.y[k];
235
- if (p.y[k] > y1) y1 = p.y[k];
236
- }
237
- }
238
- return {
239
- x0,
240
- y0,
241
- x1,
242
- y1,
243
- cx: (x0 + x1) / 2,
244
- cy: (y0 + y1) / 2,
245
- w: x1 - x0,
246
- h: y1 - y0
247
- };
236
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
237
+ for (const p of ps) {
238
+ for (let k = 0; k < p.x.length; k++) {
239
+ if (p.x[k] < x0) x0 = p.x[k];
240
+ if (p.x[k] > x1) x1 = p.x[k];
241
+ if (p.y[k] < y0) y0 = p.y[k];
242
+ if (p.y[k] > y1) y1 = p.y[k];
243
+ }
244
+ }
245
+ return {
246
+ x0,
247
+ y0,
248
+ x1,
249
+ y1,
250
+ cx: (x0 + x1) / 2,
251
+ cy: (y0 + y1) / 2,
252
+ w: x1 - x0,
253
+ h: y1 - y0
254
+ };
248
255
  }
256
+ /* is the run of column midpoints s … e one way along its chord */
249
257
  function monotone(mx, my, s, e) {
250
- const ax = mx[e] - mx[s], ay = my[e] - my[s];
251
- let prev = -Infinity;
252
- for(let k = s; k <= e; k++){
253
- const t = (mx[k] - mx[s]) * ax + (my[k] - my[s]) * ay;
254
- if (t < prev - 1e-9) return false;
255
- prev = t;
256
- }
257
- return true;
258
+ const ax = mx[e] - mx[s], ay = my[e] - my[s];
259
+ let prev = -Infinity;
260
+ for (let k = s; k <= e; k++) {
261
+ const t = (mx[k] - mx[s]) * ax + (my[k] - my[s]) * ay;
262
+ if (t < prev - 1e-9) return false;
263
+ prev = t;
264
+ }
265
+ return true;
258
266
  }
259
- const clampByte = (v)=>v < 0 ? 0 : v > 255 ? 255 : Math.round(v);
260
- export const css = (c)=>`rgb(${clampByte(c[0])},${clampByte(c[1])},${clampByte(c[2])})`;
267
+ const clampByte = (v) => v < 0 ? 0 : v > 255 ? 255 : Math.round(v);
268
+ export const css = (c) => `rgb(${clampByte(c[0])},${clampByte(c[1])},${clampByte(c[2])})`;
269
+ /** Paint several surfaces as one, back to front, the parts facing away
270
+ dropped. `px` is the view's scale. Returns the corners painted, for an
271
+ outline. */
261
272
  export function paintLayers(ctx, layers, px) {
262
- const pieces = [];
263
- const corners = [];
264
- for (const l of layers){
265
- const bias = l.bias ?? 0, keep = l.keep, cull = l.cull !== false;
266
- for (const P of l.parts){
267
- const { nu, nv, f, cd, x, y } = P, W = nu + 1;
268
- const vis = (c)=>(!cull || f[c * 3 + 2] > -0.04) && (!keep || keep(cd[c]));
269
- for(let j = 0; j < nv; j++){
270
- if (l.flat) {
271
- for(let i = 0; i < nu; i++){
272
- const c = j * nu + i;
273
- if (!vis(c)) continue;
274
- pieces.push({
275
- d: cd[c] + bias,
276
- l,
277
- p: P,
278
- j,
279
- cols: [
280
- i,
281
- i + 1
282
- ],
283
- cells: [
284
- c
285
- ]
286
- });
287
- corners.push([
288
- x[j * W + i],
289
- y[j * W + i]
290
- ], [
291
- x[(j + 1) * W + i + 1],
292
- y[(j + 1) * W + i + 1]
293
- ]);
294
- }
295
- continue;
296
- }
297
- const runs = [];
298
- let cur = null;
299
- for(let i = 0; i < nu; i++){
300
- if (vis(j * nu + i)) {
301
- if (!cur) runs.push(cur = []);
302
- cur.push(i);
303
- } else cur = null;
304
- }
305
- if (P.closed && runs.length > 1 && runs[0][0] === 0 && runs[runs.length - 1][runs[runs.length - 1].length - 1] === nu - 1) {
306
- runs[0] = runs.pop().concat(runs[0]);
307
- }
308
- for (const run of runs){
309
- const cols = run.concat(run[run.length - 1] + 1);
310
- const mx = [], my = [];
311
- for(let k = 0; k < cols.length; k++){
312
- const t = j * W + cols[k], b = t + W;
313
- mx.push((x[t] + x[b]) / 2);
314
- my.push((y[t] + y[b]) / 2);
315
- if (k === 0 || k === cols.length - 1) corners.push([
316
- x[t],
317
- y[t]
318
- ], [
319
- x[b],
320
- y[b]
321
- ]);
322
- else if (j === 0) corners.push([
323
- x[t],
324
- y[t]
325
- ]);
326
- else if (j === nv - 1) corners.push([
327
- x[b],
328
- y[b]
329
- ]);
330
- }
331
- let s = 0;
332
- while(s < cols.length - 1){
333
- let e = s + 1;
334
- while(e + 1 < cols.length && monotone(mx, my, s, e + 1))e++;
335
- let dd = 0;
336
- for(let k = s; k < e; k++)dd += cd[j * nu + run[k]];
337
- pieces.push({
338
- d: dd / (e - s) + bias,
339
- l,
340
- p: P,
341
- j,
342
- cols: cols.slice(s, e + 1),
343
- cells: run.slice(s, e)
344
- });
345
- s = e;
346
- }
347
- }
348
- }
349
- }
350
- }
351
- pieces.sort((a, b)=>a.d - b.d);
352
- const grow = 0.5 / px;
353
- for (const pc of pieces){
354
- const { p: P, l, j, cols, cells } = pc;
355
- const { x, y, n, f } = P, W = P.nu + 1;
356
- const nc = cols.length;
357
- const tx = [], ty = [], bx = [], by = [];
358
- const t0 = j * W + cols[0], tn = j * W + cols[nc - 1];
359
- let ax = (x[tn] + x[tn + W]) / 2 - (x[t0] + x[t0 + W]) / 2, ay = (y[tn] + y[tn + W]) / 2 - (y[t0] + y[t0 + W]) / 2;
360
- const al = Math.hypot(ax, ay);
361
- const ux = al > 1e-9 ? ax / al : 0, uy = al > 1e-9 ? ay / al : 0;
362
- for(let k = 0; k < nc; k++){
363
- const t = j * W + cols[k], b = t + W;
364
- let dx = x[t] - x[b], dy = y[t] - y[b];
365
- const dl = Math.hypot(dx, dy);
366
- if (dl > 1e-9) dx /= dl, dy /= dl;
367
- else dx = dy = 0;
368
- const e = k === 0 ? -grow : k === nc - 1 ? grow : 0;
369
- tx.push(x[t] + dx * grow + ux * e);
370
- ty.push(y[t] + dy * grow + uy * e);
371
- bx.push(x[b] - dx * grow + ux * e);
372
- by.push(y[b] - dy * grow + uy * e);
373
- }
374
- const colour = (k)=>{
375
- if (n && !l.flat) {
376
- const t = (j * W + cols[k]) * 3, b = t + W * 3;
377
- return l.shade(norm([
378
- n[t] + n[b],
379
- n[t + 1] + n[b + 1],
380
- n[t + 2] + n[b + 2]
381
- ]));
382
- }
383
- const c0 = cells[Math.max(0, Math.min(cells.length - 1, k - 1))] * 3, c1 = cells[Math.min(cells.length - 1, k)] * 3;
384
- return l.shade(l.flat ? [
385
- f[c1],
386
- f[c1 + 1],
387
- f[c1 + 2]
388
- ] : norm([
389
- f[c0] + f[c1],
390
- f[c0 + 1] + f[c1 + 1],
391
- f[c0 + 2] + f[c1 + 2]
392
- ]));
393
- };
394
- let style;
395
- if (l.flat || al < 1e-6) style = css(colour(0));
396
- else {
397
- const g = ctx.createLinearGradient((x[t0] + x[t0 + W]) / 2, (y[t0] + y[t0 + W]) / 2, (x[tn] + x[tn + W]) / 2, (y[tn] + y[tn + W]) / 2);
398
- let last = null;
399
- const m0x = (x[t0] + x[t0 + W]) / 2, m0y = (y[t0] + y[t0 + W]) / 2;
400
- for(let k = 0; k < nc; k++){
401
- const c = colour(k);
402
- if (last && k < nc - 1 && Math.abs(c[0] - last[0]) + Math.abs(c[1] - last[1]) + Math.abs(c[2] - last[2]) < 5) continue;
403
- const t = j * W + cols[k];
404
- const off = (((x[t] + x[t + W]) / 2 - m0x) * ax + ((y[t] + y[t + W]) / 2 - m0y) * ay) / (al * al);
405
- g.addColorStop(off < 0 ? 0 : off > 1 ? 1 : off, css(c));
406
- last = c;
407
- }
408
- style = g;
409
- }
410
- ctx.fillStyle = style;
411
- ctx.beginPath();
412
- ctx.moveTo(tx[0], ty[0]);
413
- for(let k = 1; k < nc; k++)ctx.lineTo(tx[k], ty[k]);
414
- for(let k = nc - 1; k >= 0; k--)ctx.lineTo(bx[k], by[k]);
415
- ctx.closePath();
416
- ctx.fill();
417
- }
418
- return corners;
273
+ const pieces = [];
274
+ const corners = [];
275
+ for (const l of layers) {
276
+ const bias = l.bias ?? 0, keep = l.keep, cull = l.cull !== false;
277
+ for (const P of l.parts) {
278
+ const { nu, nv, f, cd, x, y } = P, W = nu + 1;
279
+ const vis = (c) => (!cull || f[c * 3 + 2] > -.04) && (!keep || keep(cd[c]));
280
+ for (let j = 0; j < nv; j++) {
281
+ if (l.flat) {
282
+ for (let i = 0; i < nu; i++) {
283
+ const c = j * nu + i;
284
+ if (!vis(c)) continue;
285
+ pieces.push({
286
+ d: cd[c] + bias,
287
+ l,
288
+ p: P,
289
+ j,
290
+ cols: [i, i + 1],
291
+ cells: [c]
292
+ });
293
+ corners.push([x[j * W + i], y[j * W + i]], [x[(j + 1) * W + i + 1], y[(j + 1) * W + i + 1]]);
294
+ }
295
+ continue;
296
+ }
297
+ /* the row's visible runs, joined across the seam of a closed grid */
298
+ const runs = [];
299
+ let cur = null;
300
+ for (let i = 0; i < nu; i++) {
301
+ if (vis(j * nu + i)) {
302
+ if (!cur) runs.push(cur = []);
303
+ cur.push(i);
304
+ } else cur = null;
305
+ }
306
+ if (P.closed && runs.length > 1 && runs[0][0] === 0 && runs[runs.length - 1][runs[runs.length - 1].length - 1] === nu - 1) {
307
+ runs[0] = runs.pop().concat(runs[0]);
308
+ }
309
+ for (const run of runs) {
310
+ const cols = run.concat(run[run.length - 1] + 1);
311
+ const mx = [], my = [];
312
+ for (let k = 0; k < cols.length; k++) {
313
+ const t = j * W + cols[k], b = t + W;
314
+ mx.push((x[t] + x[b]) / 2);
315
+ my.push((y[t] + y[b]) / 2);
316
+ /* the outline needs only the silhouette: a run's ends, and the
317
+ grid's first and last rows */
318
+ if (k === 0 || k === cols.length - 1) corners.push([x[t], y[t]], [x[b], y[b]]);
319
+ else if (j === 0) corners.push([x[t], y[t]]);
320
+ else if (j === nv - 1) corners.push([x[b], y[b]]);
321
+ }
322
+ /* split where the row turns back on itself on screen, so each
323
+ strip can take one linear gradient */
324
+ let s = 0;
325
+ while (s < cols.length - 1) {
326
+ let e = s + 1;
327
+ while (e + 1 < cols.length && monotone(mx, my, s, e + 1)) e++;
328
+ let dd = 0;
329
+ for (let k = s; k < e; k++) dd += cd[j * nu + run[k]];
330
+ pieces.push({
331
+ d: dd / (e - s) + bias,
332
+ l,
333
+ p: P,
334
+ j,
335
+ cols: cols.slice(s, e + 1),
336
+ cells: run.slice(s, e)
337
+ });
338
+ s = e;
339
+ }
340
+ }
341
+ }
342
+ }
343
+ }
344
+ pieces.sort((a, b) => a.d - b.d);
345
+ const grow = .5 / px;
346
+ for (const pc of pieces) {
347
+ const { p: P, l, j, cols, cells } = pc;
348
+ const { x, y, n, f } = P, W = P.nu + 1;
349
+ const nc = cols.length;
350
+ /* the strip's outline, pushed out a little all round so neighbours
351
+ overlap and no seam of background shows */
352
+ const tx = [], ty = [], bx = [], by = [];
353
+ const t0 = j * W + cols[0], tn = j * W + cols[nc - 1];
354
+ let ax = (x[tn] + x[tn + W]) / 2 - (x[t0] + x[t0 + W]) / 2, ay = (y[tn] + y[tn + W]) / 2 - (y[t0] + y[t0 + W]) / 2;
355
+ const al = Math.hypot(ax, ay);
356
+ const ux = al > 1e-9 ? ax / al : 0, uy = al > 1e-9 ? ay / al : 0;
357
+ for (let k = 0; k < nc; k++) {
358
+ const t = j * W + cols[k], b = t + W;
359
+ let dx = x[t] - x[b], dy = y[t] - y[b];
360
+ const dl = Math.hypot(dx, dy);
361
+ if (dl > 1e-9) dx /= dl, dy /= dl;
362
+ else dx = dy = 0;
363
+ const e = k === 0 ? -grow : k === nc - 1 ? grow : 0;
364
+ tx.push(x[t] + dx * grow + ux * e);
365
+ ty.push(y[t] + dy * grow + uy * e);
366
+ bx.push(x[b] - dx * grow + ux * e);
367
+ by.push(y[b] - dy * grow + uy * e);
368
+ }
369
+ /* the colour at each column: from the corners' normals, or the cells'
370
+ beside it on a faceted mesh; stops only where the colour has moved */
371
+ const colour = (k) => {
372
+ if (n && !l.flat) {
373
+ const t = (j * W + cols[k]) * 3, b = t + W * 3;
374
+ return l.shade(norm([
375
+ n[t] + n[b],
376
+ n[t + 1] + n[b + 1],
377
+ n[t + 2] + n[b + 2]
378
+ ]));
379
+ }
380
+ const c0 = cells[Math.max(0, Math.min(cells.length - 1, k - 1))] * 3, c1 = cells[Math.min(cells.length - 1, k)] * 3;
381
+ return l.shade(l.flat ? [
382
+ f[c1],
383
+ f[c1 + 1],
384
+ f[c1 + 2]
385
+ ] : norm([
386
+ f[c0] + f[c1],
387
+ f[c0 + 1] + f[c1 + 1],
388
+ f[c0 + 2] + f[c1 + 2]
389
+ ]));
390
+ };
391
+ let style;
392
+ if (l.flat || al < 1e-6) style = css(colour(0));
393
+ else {
394
+ const g = ctx.createLinearGradient((x[t0] + x[t0 + W]) / 2, (y[t0] + y[t0 + W]) / 2, (x[tn] + x[tn + W]) / 2, (y[tn] + y[tn + W]) / 2);
395
+ let last = null;
396
+ const m0x = (x[t0] + x[t0 + W]) / 2, m0y = (y[t0] + y[t0 + W]) / 2;
397
+ for (let k = 0; k < nc; k++) {
398
+ const c = colour(k);
399
+ if (last && k < nc - 1 && Math.abs(c[0] - last[0]) + Math.abs(c[1] - last[1]) + Math.abs(c[2] - last[2]) < 5) continue;
400
+ const t = j * W + cols[k];
401
+ const off = (((x[t] + x[t + W]) / 2 - m0x) * ax + ((y[t] + y[t + W]) / 2 - m0y) * ay) / (al * al);
402
+ g.addColorStop(off < 0 ? 0 : off > 1 ? 1 : off, css(c));
403
+ last = c;
404
+ }
405
+ style = g;
406
+ }
407
+ ctx.fillStyle = style;
408
+ ctx.beginPath();
409
+ ctx.moveTo(tx[0], ty[0]);
410
+ for (let k = 1; k < nc; k++) ctx.lineTo(tx[k], ty[k]);
411
+ for (let k = nc - 1; k >= 0; k--) ctx.lineTo(bx[k], by[k]);
412
+ ctx.closePath();
413
+ ctx.fill();
414
+ }
415
+ return corners;
419
416
  }
420
417
  export function hull(pts) {
421
- const p = pts.slice().sort((a, b)=>a[0] - b[0] || a[1] - b[1]);
422
- if (p.length < 3) return p;
423
- const cr = (o, a, b)=>(a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
424
- const lo = [], up = [];
425
- for (const q of p){
426
- while(lo.length >= 2 && cr(lo[lo.length - 2], lo[lo.length - 1], q) <= 0)lo.pop();
427
- lo.push(q);
428
- }
429
- for(let i = p.length - 1; i >= 0; i--){
430
- const q = p[i];
431
- while(up.length >= 2 && cr(up[up.length - 2], up[up.length - 1], q) <= 0)up.pop();
432
- up.push(q);
433
- }
434
- lo.pop();
435
- up.pop();
436
- return lo.concat(up);
418
+ const p = pts.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
419
+ if (p.length < 3) return p;
420
+ const cr = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
421
+ const lo = [], up = [];
422
+ for (const q of p) {
423
+ while (lo.length >= 2 && cr(lo[lo.length - 2], lo[lo.length - 1], q) <= 0) lo.pop();
424
+ lo.push(q);
425
+ }
426
+ for (let i = p.length - 1; i >= 0; i--) {
427
+ const q = p[i];
428
+ while (up.length >= 2 && cr(up[up.length - 2], up[up.length - 1], q) <= 0) up.pop();
429
+ up.push(q);
430
+ }
431
+ lo.pop();
432
+ up.pop();
433
+ return lo.concat(up);
437
434
  }
438
- const clamp01 = (x)=>x < 0 ? 0 : x > 1 ? 1 : x;
439
- export const mix = (a, b, t)=>[
440
- a[0] + (b[0] - a[0]) * t,
441
- a[1] + (b[1] - a[1]) * t,
442
- a[2] + (b[2] - a[2]) * t
443
- ];
444
- export const scale = (a, k)=>[
445
- a[0] * k,
446
- a[1] * k,
447
- a[2] * k
448
- ];
435
+ /* ── materials: a view and a base colour to a shade ────────────────── */
436
+ const clamp01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
437
+ export const mix = (a, b, t) => [
438
+ a[0] + (b[0] - a[0]) * t,
439
+ a[1] + (b[1] - a[1]) * t,
440
+ a[2] + (b[2] - a[2]) * t
441
+ ];
442
+ export const scale = (a, k) => [
443
+ a[0] * k,
444
+ a[1] * k,
445
+ a[2] * k
446
+ ];
449
447
  const WHITE = [
450
- 255,
451
- 255,
452
- 255
448
+ 255,
449
+ 255,
450
+ 255
453
451
  ];
452
+ /** Felt and knit: matte, the light wrapping softly round, a faint sheen
453
+ where the fibres lie side-on; a dark felt keeps a floor of light so it
454
+ reads charcoal on its lit side. `k` darkens it (a fold, an underside). */
454
455
  export function felt(view, base, k = 1) {
455
- const L = view.L;
456
- return (n)=>{
457
- const nl = dot(n, L);
458
- const dif = Math.pow(clamp01((nl + 0.4) / 1.4), 1.35);
459
- const graze = Math.pow(1 - clamp01(n[2]), 2);
460
- const lit = (0.22 + 0.95 * dif + 0.22 * graze * clamp01(nl + 0.6)) * k;
461
- const floor = 26 * dif * k;
462
- return [
463
- base[0] * lit + floor,
464
- base[1] * lit + floor,
465
- base[2] * lit + floor * 1.05
466
- ];
467
- };
456
+ const L = view.L;
457
+ return (n) => {
458
+ const nl = dot(n, L);
459
+ const dif = Math.pow(clamp01((nl + .4) / 1.4), 1.35);
460
+ const graze = Math.pow(1 - clamp01(n[2]), 2);
461
+ const lit = (.22 + .95 * dif + .22 * graze * clamp01(nl + .6)) * k;
462
+ const floor = 26 * dif * k;
463
+ return [
464
+ base[0] * lit + floor,
465
+ base[1] * lit + floor,
466
+ base[2] * lit + floor * 1.05
467
+ ];
468
+ };
468
469
  }
470
+ /** Velvet: deep in its folds, its colour lifting toward the silhouette. */
469
471
  export function velvet(view, base) {
470
- const L = view.L, sheen = mix(base, WHITE, 0.3);
471
- return (n)=>{
472
- const nl = dot(n, L);
473
- const dif = clamp01((nl + 0.3) / 1.3);
474
- const graze = Math.pow(1 - clamp01(n[2]), 1.6);
475
- return mix(scale(base, 0.2 + 0.7 * dif), sheen, 0.6 * graze * (0.4 + 0.6 * dif));
476
- };
472
+ const L = view.L, sheen = mix(base, WHITE, .3);
473
+ return (n) => {
474
+ const nl = dot(n, L);
475
+ const dif = clamp01((nl + .3) / 1.3);
476
+ const graze = Math.pow(1 - clamp01(n[2]), 1.6);
477
+ return mix(scale(base, .2 + .7 * dif), sheen, .6 * graze * (.4 + .6 * dif));
478
+ };
477
479
  }
480
+ /** Paper: Lambert with a soft wide sheen. */
478
481
  export function paper(view, base) {
479
- const L = view.L, H = view.H;
480
- return (n)=>{
481
- const dif = clamp01((dot(n, L) + 0.2) / 1.2);
482
- const spec = Math.pow(clamp01(dot(n, H)), 22) * 0.3;
483
- return mix(scale(base, 0.36 + 0.7 * dif), WHITE, spec);
484
- };
482
+ const L = view.L, H = view.H;
483
+ return (n) => {
484
+ const dif = clamp01((dot(n, L) + .2) / 1.2);
485
+ const spec = Math.pow(clamp01(dot(n, H)), 22) * .3;
486
+ return mix(scale(base, .36 + .7 * dif), WHITE, spec);
487
+ };
485
488
  }
489
+ /** Soft plastic and leather: diffuse, a broad sheen and a tighter hot
490
+ spot, a thin rim of light at the silhouette. */
486
491
  export function plastic(view, base, gloss = 1) {
487
- const L = view.L, H = view.H;
488
- return (n)=>{
489
- const dif = clamp01((dot(n, L) + 0.25) / 1.25);
490
- const nh = clamp01(dot(n, H));
491
- const spec = (Math.pow(nh, 16) * 0.2 + Math.pow(nh, 80) * 0.6) * gloss;
492
- const rim = Math.pow(1 - clamp01(n[2]), 3) * 0.14 * gloss;
493
- return mix(scale(base, 0.28 + 0.78 * dif), WHITE, clamp01(spec + rim));
494
- };
492
+ const L = view.L, H = view.H;
493
+ return (n) => {
494
+ const dif = clamp01((dot(n, L) + .25) / 1.25);
495
+ const nh = clamp01(dot(n, H));
496
+ const spec = (Math.pow(nh, 16) * .2 + Math.pow(nh, 80) * .6) * gloss;
497
+ const rim = Math.pow(1 - clamp01(n[2]), 3) * .14 * gloss;
498
+ return mix(scale(base, .28 + .78 * dif), WHITE, clamp01(spec + rim));
499
+ };
495
500
  }
501
+ /** Satin: a silk ribbon's light — a wide soft sheen that runs along the
502
+ folds and a deep, saturated shade between them. */
496
503
  export function satin(view, base) {
497
- const L = view.L, H = view.H, deep = scale(base, 0.42), sheen = mix(base, WHITE, 0.55);
498
- return (n)=>{
499
- const dif = clamp01((dot(n, L) + 0.35) / 1.35);
500
- const nh = clamp01(dot(n, H));
501
- const s = Math.pow(nh, 6) * 0.5 + Math.pow(nh, 40) * 0.35;
502
- return mix(mix(deep, base, dif), sheen, clamp01(s));
503
- };
504
+ const L = view.L, H = view.H, deep = scale(base, .42), sheen = mix(base, WHITE, .55);
505
+ return (n) => {
506
+ const dif = clamp01((dot(n, L) + .35) / 1.35);
507
+ const nh = clamp01(dot(n, H));
508
+ const s = Math.pow(nh, 6) * .5 + Math.pow(nh, 40) * .35;
509
+ return mix(mix(deep, base, dif), sheen, clamp01(s));
510
+ };
504
511
  }
512
+ /** Metal: the colour of what it reflects — a bright sky above, a dark
513
+ floor below, a bright horizon between — tinted, with a hot glint. */
505
514
  export function metal(view, tint, dark, glint) {
506
- const H = view.H, L = view.L;
507
- return (n)=>{
508
- const rz = 2 * n[2] * n[2] - 1, ry = 2 * n[2] * n[1];
509
- const up = -ry;
510
- const sky = clamp01(0.45 + up * 0.85 + rz * 0.1);
511
- const horizon = Math.exp(-Math.pow((up + 0.12) / 0.16, 2)) * 0.55;
512
- const nl = clamp01(dot(n, L));
513
- const c = mix(dark, tint, clamp01(0.1 + sky * 0.72 + nl * 0.3));
514
- const spec = Math.pow(clamp01(dot(n, H)), 70) + horizon * 0.4;
515
- return mix(c, glint, clamp01(spec));
516
- };
515
+ const H = view.H, L = view.L;
516
+ return (n) => {
517
+ /* the reflection of the view about the normal; up is −y */
518
+ const rz = 2 * n[2] * n[2] - 1, ry = 2 * n[2] * n[1];
519
+ const up = -ry;
520
+ const sky = clamp01(.45 + up * .85 + rz * .1);
521
+ const horizon = Math.exp(-Math.pow((up + .12) / .16, 2)) * .55;
522
+ const nl = clamp01(dot(n, L));
523
+ const c = mix(dark, tint, clamp01(.1 + sky * .72 + nl * .3));
524
+ const spec = Math.pow(clamp01(dot(n, H)), 70) + horizon * .4;
525
+ return mix(c, glint, clamp01(spec));
526
+ };
517
527
  }
518
- export const gold = (view)=>metal(view, [
519
- 255,
520
- 200,
521
- 88
522
- ], [
523
- 86,
524
- 50,
525
- 8
526
- ], [
527
- 255,
528
- 247,
529
- 220
530
- ]);
531
- export const silver = (view)=>metal(view, [
532
- 232,
533
- 236,
534
- 242
535
- ], [
536
- 58,
537
- 62,
538
- 72
539
- ], [
540
- 255,
541
- 255,
542
- 255
543
- ]);
528
+ export const gold = (view) => metal(view, [
529
+ 255,
530
+ 200,
531
+ 88
532
+ ], [
533
+ 86,
534
+ 50,
535
+ 8
536
+ ], [
537
+ 255,
538
+ 247,
539
+ 220
540
+ ]);
541
+ export const silver = (view) => metal(view, [
542
+ 232,
543
+ 236,
544
+ 242
545
+ ], [
546
+ 58,
547
+ 62,
548
+ 72
549
+ ], [
550
+ 255,
551
+ 255,
552
+ 255
553
+ ]);
554
+ /** A pearl: soft white with a pink-blue play and a small bright glint. */
544
555
  export function pearl(view) {
545
- const L = view.L, H = view.H;
546
- return (n)=>{
547
- const dif = clamp01((dot(n, L) + 0.4) / 1.4);
548
- const base = mix([
549
- 238,
550
- 228,
551
- 228
552
- ], [
553
- 212,
554
- 222,
555
- 242
556
- ], 1 - clamp01(n[2]));
557
- return mix(scale(base, 0.62 + 0.42 * dif), WHITE, Math.pow(clamp01(dot(n, H)), 40) * 0.8);
558
- };
556
+ const L = view.L, H = view.H;
557
+ return (n) => {
558
+ const dif = clamp01((dot(n, L) + .4) / 1.4);
559
+ const base = mix([
560
+ 238,
561
+ 228,
562
+ 228
563
+ ], [
564
+ 212,
565
+ 222,
566
+ 242
567
+ ], 1 - clamp01(n[2]));
568
+ return mix(scale(base, .62 + .42 * dif), WHITE, Math.pow(clamp01(dot(n, H)), 40) * .8);
569
+ };
559
570
  }
571
+ /** A cut gem's facet: its colour deepening toward the edges, the facets
572
+ turned to the light bright, a white fire on the one that catches it. */
560
573
  export function gem(view, base) {
561
- const L = view.L, H = view.H, deep = scale(base, 0.22), clear = mix(base, WHITE, 0.3);
562
- return (n)=>{
563
- const c = mix(deep, clear, clamp01(clamp01(n[2]) * 0.75 + dot(n, L) * 0.4));
564
- return mix(c, WHITE, Math.pow(clamp01(dot(n, H)), 24) * 0.95);
565
- };
574
+ const L = view.L, H = view.H, deep = scale(base, .22), clear = mix(base, WHITE, .3);
575
+ return (n) => {
576
+ const c = mix(deep, clear, clamp01(clamp01(n[2]) * .75 + dot(n, L) * .4));
577
+ return mix(c, WHITE, Math.pow(clamp01(dot(n, H)), 24) * .95);
578
+ };
566
579
  }
567
580
  const SPRITE = {
568
- gold: [
569
- [
570
- 0,
571
- "#fffbe8"
572
- ],
573
- [
574
- 0.08,
575
- "#ffe7a0"
576
- ],
577
- [
578
- 0.3,
579
- "#eeb546"
580
- ],
581
- [
582
- 0.62,
583
- "#ad7214"
584
- ],
585
- [
586
- 0.87,
587
- "#5a3806"
588
- ],
589
- [
590
- 1,
591
- "#8c5c18"
592
- ]
593
- ],
594
- silver: [
595
- [
596
- 0,
597
- "#ffffff"
598
- ],
599
- [
600
- 0.08,
601
- "#f3f5f9"
602
- ],
603
- [
604
- 0.32,
605
- "#c6ccd6"
606
- ],
607
- [
608
- 0.64,
609
- "#7a808c"
610
- ],
611
- [
612
- 0.88,
613
- "#3d4149"
614
- ],
615
- [
616
- 1,
617
- "#6b717c"
618
- ]
619
- ],
620
- pearl: [
621
- [
622
- 0,
623
- "#ffffff"
624
- ],
625
- [
626
- 0.1,
627
- "#fdf9f7"
628
- ],
629
- [
630
- 0.45,
631
- "#ebe4e6"
632
- ],
633
- [
634
- 0.8,
635
- "#c6bccb"
636
- ],
637
- [
638
- 1,
639
- "#a89db5"
640
- ]
641
- ]
581
+ gold: [
582
+ [0, "#fffbe8"],
583
+ [.08, "#ffe7a0"],
584
+ [.3, "#eeb546"],
585
+ [.62, "#ad7214"],
586
+ [.87, "#5a3806"],
587
+ [1, "#8c5c18"]
588
+ ],
589
+ silver: [
590
+ [0, "#ffffff"],
591
+ [.08, "#f3f5f9"],
592
+ [.32, "#c6ccd6"],
593
+ [.64, "#7a808c"],
594
+ [.88, "#3d4149"],
595
+ [1, "#6b717c"]
596
+ ],
597
+ pearl: [
598
+ [0, "#ffffff"],
599
+ [.1, "#fdf9f7"],
600
+ [.45, "#ebe4e6"],
601
+ [.8, "#c6bccb"],
602
+ [1, "#a89db5"]
603
+ ]
642
604
  };
605
+ /** A small sphere as one radial fill: its hot spot where the normal meets
606
+ the half vector, falling to a dark edge with a little reflected light. */
643
607
  export function sphereSprite(ctx, view, c, rad, kind) {
644
- const s = view.toScreen(c);
645
- const H = view.H;
646
- const g = ctx.createRadialGradient(s[0] + H[0] * rad * 0.85, s[1] + H[1] * rad * 0.85, 0, s[0], s[1], rad);
647
- if (typeof kind === "string") for (const [t, col] of SPRITE[kind])g.addColorStop(t, col);
648
- else {
649
- g.addColorStop(0, css(mix(kind, WHITE, 0.8)));
650
- g.addColorStop(0.1, css(mix(kind, WHITE, 0.3)));
651
- g.addColorStop(0.35, css(kind));
652
- g.addColorStop(0.85, css(scale(kind, 0.5)));
653
- g.addColorStop(1, css(scale(kind, 0.62)));
654
- }
655
- ctx.fillStyle = g;
656
- ctx.beginPath();
657
- ctx.arc(s[0], s[1], rad, 0, TAU);
658
- ctx.fill();
608
+ const s = view.toScreen(c);
609
+ const H = view.H;
610
+ const g = ctx.createRadialGradient(s[0] + H[0] * rad * .85, s[1] + H[1] * rad * .85, 0, s[0], s[1], rad);
611
+ if (typeof kind === "string") for (const [t, col] of SPRITE[kind]) g.addColorStop(t, col);
612
+ else {
613
+ g.addColorStop(0, css(mix(kind, WHITE, .8)));
614
+ g.addColorStop(.1, css(mix(kind, WHITE, .3)));
615
+ g.addColorStop(.35, css(kind));
616
+ g.addColorStop(.85, css(scale(kind, .5)));
617
+ g.addColorStop(1, css(scale(kind, .62)));
618
+ }
619
+ ctx.fillStyle = g;
620
+ ctx.beginPath();
621
+ ctx.arc(s[0], s[1], rad, 0, TAU);
622
+ ctx.fill();
659
623
  }
624
+ /**
625
+ * A cut stone set in a surface: `c` its centre, `N` the surface's outward
626
+ * normal, `T` a tangent (object space, unit). An eight-sided girdle, a
627
+ * table on top and the crown facets between, each lit by its own normal.
628
+ */
660
629
  export function cutGem(ctx, view, c, N, T, rad, col) {
661
- const B = cross(N, T);
662
- const at = (r, a, h)=>{
663
- const x = r * Math.cos(a), y = r * Math.sin(a);
664
- return [
665
- c[0] + x * T[0] + y * B[0] + h * N[0],
666
- c[1] + x * T[1] + y * B[1] + h * N[1],
667
- c[2] + x * T[2] + y * B[2] + h * N[2]
668
- ];
669
- };
670
- const K = 8, tr = 0.55, th = 0.36 * rad;
671
- const girdle = [], table = [];
672
- for(let k = 0; k < K; k++){
673
- const a = (k + 0.5) / K * TAU;
674
- girdle.push(at(rad, a, 0));
675
- table.push(at(rad * tr, a, th));
676
- }
677
- const shade = gem(view, col);
678
- const poly = (pts, n)=>{
679
- const nv = view.dir(n);
680
- if (nv[2] <= 0.02) return;
681
- ctx.fillStyle = css(shade(nv));
682
- ctx.beginPath();
683
- pts.forEach((p, i)=>{
684
- const s = view.toScreen(p);
685
- if (i) ctx.lineTo(s[0], s[1]);
686
- else ctx.moveTo(s[0], s[1]);
687
- });
688
- ctx.closePath();
689
- ctx.fill();
690
- ctx.strokeStyle = ctx.fillStyle;
691
- ctx.stroke();
692
- };
693
- ctx.lineWidth = Math.min(0.3, 1 / view.px);
694
- ctx.lineJoin = "round";
695
- for(let k = 0; k < K; k++){
696
- const g0 = girdle[k], g1 = girdle[(k + 1) % K], t1 = table[(k + 1) % K], t0 = table[k];
697
- let n = cross(sub(t1, g0), sub(t0, g1));
698
- if (dot(n, N) < 0) n = [
699
- -n[0],
700
- -n[1],
701
- -n[2]
702
- ];
703
- poly([
704
- g0,
705
- g1,
706
- t1,
707
- t0
708
- ], norm(n));
709
- }
710
- poly(table, N);
711
- const nv = view.dir(N);
712
- if (nv[2] > 0.1) {
713
- const s0 = view.toScreen(at(0, 0, th));
714
- const L = view.L;
715
- ctx.fillStyle = "rgba(255,255,255,0.3)";
716
- ctx.beginPath();
717
- const s1 = view.toScreen(at(rad * tr * 0.8, Math.atan2(-L[1], -L[0]) + 2.2, th));
718
- const s2 = view.toScreen(at(rad * tr * 0.9, Math.atan2(-L[1], -L[0]) + Math.PI, th));
719
- const s3 = view.toScreen(at(rad * tr * 0.8, Math.atan2(-L[1], -L[0]) + Math.PI + 0.94, th));
720
- ctx.moveTo(s0[0], s0[1]);
721
- ctx.lineTo(s1[0], s1[1]);
722
- ctx.lineTo(s2[0], s2[1]);
723
- ctx.lineTo(s3[0], s3[1]);
724
- ctx.closePath();
725
- ctx.fill();
726
- }
630
+ const B = cross(N, T);
631
+ const at = (r, a, h) => {
632
+ const x = r * Math.cos(a), y = r * Math.sin(a);
633
+ return [
634
+ c[0] + x * T[0] + y * B[0] + h * N[0],
635
+ c[1] + x * T[1] + y * B[1] + h * N[1],
636
+ c[2] + x * T[2] + y * B[2] + h * N[2]
637
+ ];
638
+ };
639
+ const K = 8, tr = .55, th = .36 * rad;
640
+ const girdle = [], table = [];
641
+ for (let k = 0; k < K; k++) {
642
+ const a = (k + .5) / K * TAU;
643
+ girdle.push(at(rad, a, 0));
644
+ table.push(at(rad * tr, a, th));
645
+ }
646
+ const shade = gem(view, col);
647
+ const poly = (pts, n) => {
648
+ const nv = view.dir(n);
649
+ if (nv[2] <= .02) return;
650
+ ctx.fillStyle = css(shade(nv));
651
+ ctx.beginPath();
652
+ pts.forEach((p, i) => {
653
+ const s = view.toScreen(p);
654
+ if (i) ctx.lineTo(s[0], s[1]);
655
+ else ctx.moveTo(s[0], s[1]);
656
+ });
657
+ ctx.closePath();
658
+ ctx.fill();
659
+ ctx.strokeStyle = ctx.fillStyle;
660
+ ctx.stroke();
661
+ };
662
+ ctx.lineWidth = Math.min(.3, 1 / view.px);
663
+ ctx.lineJoin = "round";
664
+ for (let k = 0; k < K; k++) {
665
+ const g0 = girdle[k], g1 = girdle[(k + 1) % K], t1 = table[(k + 1) % K], t0 = table[k];
666
+ let n = cross(sub(t1, g0), sub(t0, g1));
667
+ if (dot(n, N) < 0) n = [
668
+ -n[0],
669
+ -n[1],
670
+ -n[2]
671
+ ];
672
+ poly([
673
+ g0,
674
+ g1,
675
+ t1,
676
+ t0
677
+ ], norm(n));
678
+ }
679
+ poly(table, N);
680
+ /* the table's inner light: a lighter kite toward the light */
681
+ const nv = view.dir(N);
682
+ if (nv[2] > .1) {
683
+ const s0 = view.toScreen(at(0, 0, th));
684
+ const L = view.L;
685
+ ctx.fillStyle = "rgba(255,255,255,0.3)";
686
+ ctx.beginPath();
687
+ const s1 = view.toScreen(at(rad * tr * .8, Math.atan2(-L[1], -L[0]) + 2.2, th));
688
+ const s2 = view.toScreen(at(rad * tr * .9, Math.atan2(-L[1], -L[0]) + Math.PI, th));
689
+ const s3 = view.toScreen(at(rad * tr * .8, Math.atan2(-L[1], -L[0]) + Math.PI + .94, th));
690
+ ctx.moveTo(s0[0], s0[1]);
691
+ ctx.lineTo(s1[0], s1[1]);
692
+ ctx.lineTo(s2[0], s2[1]);
693
+ ctx.lineTo(s3[0], s3[1]);
694
+ ctx.closePath();
695
+ ctx.fill();
696
+ }
727
697
  }