react-cheminfo 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/lib/chrome/ui/NavLink.d.ts.map +1 -1
  2. package/lib/chrome/ui/NavLink.js +4 -3
  3. package/lib/chrome/ui/NavLink.js.map +1 -1
  4. package/lib/chrome/ui/SiteFooter.d.ts +8 -0
  5. package/lib/chrome/ui/SiteFooter.d.ts.map +1 -1
  6. package/lib/chrome/ui/SiteFooter.js +7 -2
  7. package/lib/chrome/ui/SiteFooter.js.map +1 -1
  8. package/lib/chrome/ui/SiteHeader.d.ts +11 -0
  9. package/lib/chrome/ui/SiteHeader.d.ts.map +1 -1
  10. package/lib/chrome/ui/SiteHeader.js +7 -2
  11. package/lib/chrome/ui/SiteHeader.js.map +1 -1
  12. package/lib/chrome/ui/navItem.d.ts +12 -0
  13. package/lib/chrome/ui/navItem.d.ts.map +1 -1
  14. package/lib/chrome/ui/navItem.js.map +1 -1
  15. package/lib/core.d.ts +1 -0
  16. package/lib/core.d.ts.map +1 -1
  17. package/lib/core.js +1 -0
  18. package/lib/core.js.map +1 -1
  19. package/lib/download/core/figureLegend.d.ts +90 -0
  20. package/lib/download/core/figureLegend.d.ts.map +1 -0
  21. package/lib/download/core/figureLegend.js +77 -0
  22. package/lib/download/core/figureLegend.js.map +1 -0
  23. package/lib/download/core/figureSvg.d.ts.map +1 -1
  24. package/lib/download/core/figureSvg.js +71 -1
  25. package/lib/download/core/figureSvg.js.map +1 -1
  26. package/lib/download/core/figureSvgDocument.d.ts +9 -2
  27. package/lib/download/core/figureSvgDocument.d.ts.map +1 -1
  28. package/lib/download/core/figureSvgDocument.js +4 -1
  29. package/lib/download/core/figureSvgDocument.js.map +1 -1
  30. package/lib/download/core/figureTarget.d.ts +11 -0
  31. package/lib/download/core/figureTarget.d.ts.map +1 -1
  32. package/lib/download/core/figureTarget.js +19 -0
  33. package/lib/download/core/figureTarget.js.map +1 -1
  34. package/lib/download/core/index.d.ts +3 -1
  35. package/lib/download/core/index.d.ts.map +1 -1
  36. package/lib/download/core/index.js +2 -1
  37. package/lib/download/core/index.js.map +1 -1
  38. package/lib/ecosystem/core/sites.d.ts +1 -1
  39. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  40. package/lib/ecosystem/core/sites.js +13 -0
  41. package/lib/ecosystem/core/sites.js.map +1 -1
  42. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
  43. package/lib/ecosystem/ui/glyphs.js +5 -0
  44. package/lib/ecosystem/ui/glyphs.js.map +1 -1
  45. package/lib/overlay/ui/OverlayLegend.d.ts.map +1 -1
  46. package/lib/overlay/ui/OverlayLegend.js +1 -1
  47. package/lib/overlay/ui/OverlayLegend.js.map +1 -1
  48. package/lib/projection/core/projectionCopy.d.ts +4 -0
  49. package/lib/projection/core/projectionCopy.d.ts.map +1 -1
  50. package/lib/projection/core/projectionCopy.js +3 -0
  51. package/lib/projection/core/projectionCopy.js.map +1 -1
  52. package/lib/projection/core/projectionOptions.d.ts +12 -0
  53. package/lib/projection/core/projectionOptions.d.ts.map +1 -1
  54. package/lib/projection/core/projectionOptions.js +27 -1
  55. package/lib/projection/core/projectionOptions.js.map +1 -1
  56. package/lib/projection/core/projectionStrings.d.ts +4 -0
  57. package/lib/projection/core/projectionStrings.d.ts.map +1 -1
  58. package/lib/projection/core/projectionStrings.js +14 -0
  59. package/lib/projection/core/projectionStrings.js.map +1 -1
  60. package/lib/projection/core/projectionTabDefaults.d.ts.map +1 -1
  61. package/lib/projection/core/projectionTabDefaults.js +12 -0
  62. package/lib/projection/core/projectionTabDefaults.js.map +1 -1
  63. package/lib/projection/core/projectionTabs.d.ts +1 -1
  64. package/lib/projection/core/projectionTabs.d.ts.map +1 -1
  65. package/lib/projection/core/projectionTabs.js +9 -0
  66. package/lib/projection/core/projectionTabs.js.map +1 -1
  67. package/lib/projection/ui/ProjectionMapTab.d.ts +6 -0
  68. package/lib/projection/ui/ProjectionMapTab.d.ts.map +1 -1
  69. package/lib/projection/ui/ProjectionMapTab.js +2 -1
  70. package/lib/projection/ui/ProjectionMapTab.js.map +1 -1
  71. package/lib/projection/ui/ProjectionPanel.d.ts +6 -0
  72. package/lib/projection/ui/ProjectionPanel.d.ts.map +1 -1
  73. package/lib/projection/ui/ProjectionPanel.js +15 -1
  74. package/lib/projection/ui/ProjectionPanel.js.map +1 -1
  75. package/lib/projection/ui/ProjectionReadout.d.ts +7 -0
  76. package/lib/projection/ui/ProjectionReadout.d.ts.map +1 -1
  77. package/lib/projection/ui/ProjectionReadout.js +5 -4
  78. package/lib/projection/ui/ProjectionReadout.js.map +1 -1
  79. package/lib/projection/ui/ProjectionSpaceControls.d.ts +51 -0
  80. package/lib/projection/ui/ProjectionSpaceControls.d.ts.map +1 -0
  81. package/lib/projection/ui/ProjectionSpaceControls.js +71 -0
  82. package/lib/projection/ui/ProjectionSpaceControls.js.map +1 -0
  83. package/lib/projection/ui/ProjectionSpaceMore.d.ts +32 -0
  84. package/lib/projection/ui/ProjectionSpaceMore.d.ts.map +1 -0
  85. package/lib/projection/ui/ProjectionSpaceMore.js +57 -0
  86. package/lib/projection/ui/ProjectionSpaceMore.js.map +1 -0
  87. package/lib/projection/ui/ProjectionSpaceTab.d.ts +98 -0
  88. package/lib/projection/ui/ProjectionSpaceTab.d.ts.map +1 -0
  89. package/lib/projection/ui/ProjectionSpaceTab.js +66 -0
  90. package/lib/projection/ui/ProjectionSpaceTab.js.map +1 -0
  91. package/lib/projection/ui/ProjectionViewer.d.ts +1 -0
  92. package/lib/projection/ui/ProjectionViewer.d.ts.map +1 -1
  93. package/lib/projection/ui/ProjectionViewer.js +2 -1
  94. package/lib/projection/ui/ProjectionViewer.js.map +1 -1
  95. package/lib/projection/ui/index.d.ts +1 -0
  96. package/lib/projection/ui/index.d.ts.map +1 -1
  97. package/lib/projection/ui/index.js.map +1 -1
  98. package/lib/projection/ui/projectionBarModel.d.ts.map +1 -1
  99. package/lib/projection/ui/projectionBarModel.js +3 -2
  100. package/lib/projection/ui/projectionBarModel.js.map +1 -1
  101. package/lib/projection/ui/projectionBarReadings.d.ts.map +1 -1
  102. package/lib/projection/ui/projectionBarReadings.js +17 -0
  103. package/lib/projection/ui/projectionBarReadings.js.map +1 -1
  104. package/lib/projection/ui/projectionBarSlots.d.ts.map +1 -1
  105. package/lib/projection/ui/projectionBarSlots.js +17 -0
  106. package/lib/projection/ui/projectionBarSlots.js.map +1 -1
  107. package/lib/projection/ui/projectionCloudChoices.d.ts +18 -0
  108. package/lib/projection/ui/projectionCloudChoices.d.ts.map +1 -0
  109. package/lib/projection/ui/projectionCloudChoices.js +19 -0
  110. package/lib/projection/ui/projectionCloudChoices.js.map +1 -0
  111. package/lib/projection/ui/projectionEllipsoid.d.ts +38 -0
  112. package/lib/projection/ui/projectionEllipsoid.d.ts.map +1 -0
  113. package/lib/projection/ui/projectionEllipsoid.js +71 -0
  114. package/lib/projection/ui/projectionEllipsoid.js.map +1 -0
  115. package/lib/projection/ui/projectionMapChrome.d.ts +8 -0
  116. package/lib/projection/ui/projectionMapChrome.d.ts.map +1 -1
  117. package/lib/projection/ui/projectionMapChrome.js +10 -6
  118. package/lib/projection/ui/projectionMapChrome.js.map +1 -1
  119. package/lib/projection/ui/projectionMapView.d.ts +13 -0
  120. package/lib/projection/ui/projectionMapView.d.ts.map +1 -1
  121. package/lib/projection/ui/projectionMapView.js +26 -0
  122. package/lib/projection/ui/projectionMapView.js.map +1 -1
  123. package/lib/projection/ui/projectionSpaceModel.d.ts +14 -0
  124. package/lib/projection/ui/projectionSpaceModel.d.ts.map +1 -0
  125. package/lib/projection/ui/projectionSpaceModel.js +14 -0
  126. package/lib/projection/ui/projectionSpaceModel.js.map +1 -0
  127. package/lib/projection/ui/projectionViewerProps.d.ts +34 -0
  128. package/lib/projection/ui/projectionViewerProps.d.ts.map +1 -1
  129. package/lib/scatter/ui/ScatterPlot.d.ts +1 -1
  130. package/lib/scatter/ui/ScatterPlot.d.ts.map +1 -1
  131. package/lib/scatter/ui/ScatterPlot.js +18 -2
  132. package/lib/scatter/ui/ScatterPlot.js.map +1 -1
  133. package/lib/scatter/ui/lassoGesture.d.ts +14 -2
  134. package/lib/scatter/ui/lassoGesture.d.ts.map +1 -1
  135. package/lib/scatter/ui/lassoGesture.js +4 -1
  136. package/lib/scatter/ui/lassoGesture.js.map +1 -1
  137. package/lib/scatter/ui/scatterPlotProps.d.ts +34 -0
  138. package/lib/scatter/ui/scatterPlotProps.d.ts.map +1 -1
  139. package/lib/scatter/ui/useScatterInteraction.d.ts +9 -0
  140. package/lib/scatter/ui/useScatterInteraction.d.ts.map +1 -1
  141. package/lib/scatter/ui/useScatterInteraction.js +9 -1
  142. package/lib/scatter/ui/useScatterInteraction.js.map +1 -1
  143. package/lib/scatter3d/core/cloudGesture.d.ts +17 -0
  144. package/lib/scatter3d/core/cloudGesture.d.ts.map +1 -0
  145. package/lib/scatter3d/core/cloudGesture.js +2 -0
  146. package/lib/scatter3d/core/cloudGesture.js.map +1 -0
  147. package/lib/scatter3d/core/confidenceEllipsoid.d.ts +63 -0
  148. package/lib/scatter3d/core/confidenceEllipsoid.d.ts.map +1 -0
  149. package/lib/scatter3d/core/confidenceEllipsoid.js +128 -0
  150. package/lib/scatter3d/core/confidenceEllipsoid.js.map +1 -0
  151. package/lib/scatter3d/core/cubeFrame.d.ts +40 -0
  152. package/lib/scatter3d/core/cubeFrame.d.ts.map +1 -0
  153. package/lib/scatter3d/core/cubeFrame.js +58 -0
  154. package/lib/scatter3d/core/cubeFrame.js.map +1 -0
  155. package/lib/scatter3d/core/cubePoints.d.ts +35 -0
  156. package/lib/scatter3d/core/cubePoints.d.ts.map +1 -0
  157. package/lib/scatter3d/core/cubePoints.js +50 -0
  158. package/lib/scatter3d/core/cubePoints.js.map +1 -0
  159. package/lib/scatter3d/core/ellipsoidCoverage.d.ts +32 -0
  160. package/lib/scatter3d/core/ellipsoidCoverage.d.ts.map +1 -0
  161. package/lib/scatter3d/core/ellipsoidCoverage.js +83 -0
  162. package/lib/scatter3d/core/ellipsoidCoverage.js.map +1 -0
  163. package/lib/scatter3d/core/ellipsoidSilhouette.d.ts +42 -0
  164. package/lib/scatter3d/core/ellipsoidSilhouette.d.ts.map +1 -0
  165. package/lib/scatter3d/core/ellipsoidSilhouette.js +52 -0
  166. package/lib/scatter3d/core/ellipsoidSilhouette.js.map +1 -0
  167. package/lib/scatter3d/core/index.d.ts +29 -0
  168. package/lib/scatter3d/core/index.d.ts.map +1 -0
  169. package/lib/scatter3d/core/index.js +22 -0
  170. package/lib/scatter3d/core/index.js.map +1 -0
  171. package/lib/scatter3d/core/orbitCamera.d.ts +113 -0
  172. package/lib/scatter3d/core/orbitCamera.d.ts.map +1 -0
  173. package/lib/scatter3d/core/orbitCamera.js +104 -0
  174. package/lib/scatter3d/core/orbitCamera.js.map +1 -0
  175. package/lib/scatter3d/core/symmetricEigen.d.ts +47 -0
  176. package/lib/scatter3d/core/symmetricEigen.d.ts.map +1 -0
  177. package/lib/scatter3d/core/symmetricEigen.js +219 -0
  178. package/lib/scatter3d/core/symmetricEigen.js.map +1 -0
  179. package/lib/scatter3d/ui/CloudFrameLayer.d.ts +33 -0
  180. package/lib/scatter3d/ui/CloudFrameLayer.d.ts.map +1 -0
  181. package/lib/scatter3d/ui/CloudFrameLayer.js +37 -0
  182. package/lib/scatter3d/ui/CloudFrameLayer.js.map +1 -0
  183. package/lib/scatter3d/ui/CloudPointLayer.d.ts +62 -0
  184. package/lib/scatter3d/ui/CloudPointLayer.d.ts.map +1 -0
  185. package/lib/scatter3d/ui/CloudPointLayer.js +67 -0
  186. package/lib/scatter3d/ui/CloudPointLayer.js.map +1 -0
  187. package/lib/scatter3d/ui/CloudShellLayer.d.ts +52 -0
  188. package/lib/scatter3d/ui/CloudShellLayer.d.ts.map +1 -0
  189. package/lib/scatter3d/ui/CloudShellLayer.js +40 -0
  190. package/lib/scatter3d/ui/CloudShellLayer.js.map +1 -0
  191. package/lib/scatter3d/ui/ScatterCloud.d.ts +24 -0
  192. package/lib/scatter3d/ui/ScatterCloud.d.ts.map +1 -0
  193. package/lib/scatter3d/ui/ScatterCloud.js +189 -0
  194. package/lib/scatter3d/ui/ScatterCloud.js.map +1 -0
  195. package/lib/scatter3d/ui/cloudGeometry.d.ts +108 -0
  196. package/lib/scatter3d/ui/cloudGeometry.d.ts.map +1 -0
  197. package/lib/scatter3d/ui/cloudGeometry.js +122 -0
  198. package/lib/scatter3d/ui/cloudGeometry.js.map +1 -0
  199. package/lib/scatter3d/ui/cloudShellStyles.d.ts +36 -0
  200. package/lib/scatter3d/ui/cloudShellStyles.d.ts.map +1 -0
  201. package/lib/scatter3d/ui/cloudShellStyles.js +62 -0
  202. package/lib/scatter3d/ui/cloudShellStyles.js.map +1 -0
  203. package/lib/scatter3d/ui/cloudZoom.d.ts +21 -0
  204. package/lib/scatter3d/ui/cloudZoom.d.ts.map +1 -0
  205. package/lib/scatter3d/ui/cloudZoom.js +22 -0
  206. package/lib/scatter3d/ui/cloudZoom.js.map +1 -0
  207. package/lib/scatter3d/ui/index.d.ts +29 -0
  208. package/lib/scatter3d/ui/index.d.ts.map +1 -0
  209. package/lib/scatter3d/ui/index.js +21 -0
  210. package/lib/scatter3d/ui/index.js.map +1 -0
  211. package/lib/scatter3d/ui/scatterCloudModel.d.ts +60 -0
  212. package/lib/scatter3d/ui/scatterCloudModel.d.ts.map +1 -0
  213. package/lib/scatter3d/ui/scatterCloudModel.js +66 -0
  214. package/lib/scatter3d/ui/scatterCloudModel.js.map +1 -0
  215. package/lib/scatter3d/ui/scatterCloudProps.d.ts +185 -0
  216. package/lib/scatter3d/ui/scatterCloudProps.d.ts.map +1 -0
  217. package/lib/scatter3d/ui/scatterCloudProps.js +9 -0
  218. package/lib/scatter3d/ui/scatterCloudProps.js.map +1 -0
  219. package/lib/scatter3d/ui/useOrbitDrag.d.ts +60 -0
  220. package/lib/scatter3d/ui/useOrbitDrag.d.ts.map +1 -0
  221. package/lib/scatter3d/ui/useOrbitDrag.js +129 -0
  222. package/lib/scatter3d/ui/useOrbitDrag.js.map +1 -0
  223. package/lib/ui.d.ts +1 -0
  224. package/lib/ui.d.ts.map +1 -1
  225. package/lib/ui.js +1 -0
  226. package/lib/ui.js.map +1 -1
  227. package/package.json +1 -1
  228. package/src/chrome/ui/NavLink.tsx +5 -3
  229. package/src/chrome/ui/SiteFooter.tsx +16 -1
  230. package/src/chrome/ui/SiteHeader.tsx +19 -1
  231. package/src/chrome/ui/navItem.ts +12 -0
  232. package/src/core.ts +1 -0
  233. package/src/download/core/figureLegend.ts +167 -0
  234. package/src/download/core/figureSvg.ts +81 -1
  235. package/src/download/core/figureSvgDocument.ts +13 -3
  236. package/src/download/core/figureTarget.ts +21 -0
  237. package/src/download/core/index.ts +8 -0
  238. package/src/ecosystem/core/sites.ts +14 -0
  239. package/src/ecosystem/ui/glyphs.tsx +26 -0
  240. package/src/overlay/ui/OverlayLegend.tsx +7 -1
  241. package/src/projection/core/projectionCopy.ts +9 -0
  242. package/src/projection/core/projectionOptions.ts +48 -3
  243. package/src/projection/core/projectionStrings.ts +16 -0
  244. package/src/projection/core/projectionTabDefaults.ts +12 -0
  245. package/src/projection/core/projectionTabs.ts +10 -1
  246. package/src/projection/ui/ProjectionMapTab.tsx +8 -0
  247. package/src/projection/ui/ProjectionPanel.tsx +63 -0
  248. package/src/projection/ui/ProjectionReadout.tsx +16 -6
  249. package/src/projection/ui/ProjectionSpaceControls.tsx +219 -0
  250. package/src/projection/ui/ProjectionSpaceMore.tsx +169 -0
  251. package/src/projection/ui/ProjectionSpaceTab.tsx +217 -0
  252. package/src/projection/ui/ProjectionViewer.tsx +3 -0
  253. package/src/projection/ui/index.ts +1 -0
  254. package/src/projection/ui/projectionBarModel.ts +3 -2
  255. package/src/projection/ui/projectionBarReadings.ts +18 -0
  256. package/src/projection/ui/projectionBarSlots.tsx +19 -0
  257. package/src/projection/ui/projectionCloudChoices.ts +23 -0
  258. package/src/projection/ui/projectionEllipsoid.ts +78 -0
  259. package/src/projection/ui/projectionMapChrome.ts +26 -5
  260. package/src/projection/ui/projectionMapView.ts +45 -0
  261. package/src/projection/ui/projectionSpaceModel.ts +14 -0
  262. package/src/projection/ui/projectionViewerProps.ts +35 -0
  263. package/src/scatter/ui/ScatterPlot.tsx +21 -2
  264. package/src/scatter/ui/lassoGesture.ts +15 -2
  265. package/src/scatter/ui/scatterPlotProps.ts +35 -0
  266. package/src/scatter/ui/useScatterInteraction.ts +22 -1
  267. package/src/scatter3d/core/cloudGesture.ts +16 -0
  268. package/src/scatter3d/core/confidenceEllipsoid.ts +177 -0
  269. package/src/scatter3d/core/cubeFrame.ts +72 -0
  270. package/src/scatter3d/core/cubePoints.ts +75 -0
  271. package/src/scatter3d/core/ellipsoidCoverage.ts +86 -0
  272. package/src/scatter3d/core/ellipsoidSilhouette.ts +76 -0
  273. package/src/scatter3d/core/index.ts +52 -0
  274. package/src/scatter3d/core/orbitCamera.ts +159 -0
  275. package/src/scatter3d/core/symmetricEigen.ts +273 -0
  276. package/src/scatter3d/ui/CloudFrameLayer.tsx +97 -0
  277. package/src/scatter3d/ui/CloudPointLayer.tsx +132 -0
  278. package/src/scatter3d/ui/CloudShellLayer.tsx +107 -0
  279. package/src/scatter3d/ui/ScatterCloud.tsx +312 -0
  280. package/src/scatter3d/ui/cloudGeometry.ts +200 -0
  281. package/src/scatter3d/ui/cloudShellStyles.ts +74 -0
  282. package/src/scatter3d/ui/cloudZoom.ts +22 -0
  283. package/src/scatter3d/ui/index.ts +41 -0
  284. package/src/scatter3d/ui/scatterCloudModel.ts +98 -0
  285. package/src/scatter3d/ui/scatterCloudProps.ts +191 -0
  286. package/src/scatter3d/ui/useOrbitDrag.ts +220 -0
  287. package/src/ui.ts +1 -0
  288. package/styles/chrome.css +29 -4
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Where the reader is standing, and where a point in the cube lands on screen.
3
+ *
4
+ * The cloud is drawn with an orthographic projection rather than a perspective
5
+ * one: a scatter is read by comparing distances, and perspective makes the same
6
+ * distance shorter at the back of the box than at the front, so two groups
7
+ * equally far apart would not look it. What is left to give the picture its
8
+ * depth is the frame around it, the order the dots are painted in, and their
9
+ * size — all of which cost nothing and none of which distorts a measurement.
10
+ */
11
+
12
+ /** A point of the cube, `[x, y, z]`, with `z` running towards the reader. */
13
+ export type Vector3 = readonly [number, number, number];
14
+
15
+ /**
16
+ * Where the reader is standing.
17
+ *
18
+ * The scene is turned by `pitch` about the horizontal axis and then by `yaw`
19
+ * about the vertical one, which is the order a hand expects: dragging sideways
20
+ * spins the box about its upright axis whatever it is already tilted to, and
21
+ * dragging up and down tips it towards the reader.
22
+ */
23
+ export interface OrbitCamera {
24
+ /** Azimuth, in radians. A sideways drag changes it. */
25
+ yaw: number;
26
+ /** Elevation, in radians. A vertical drag changes it. */
27
+ pitch: number;
28
+ }
29
+
30
+ /** Where a point of the cube landed, in pixels, and how near the reader it is. */
31
+ export interface ProjectedPoint {
32
+ /** Pixels from the left of the drawing area. */
33
+ x: number;
34
+ /** Pixels from its top. */
35
+ y: number;
36
+ /**
37
+ * Distance towards the reader, in cube units: larger is nearer. It orders
38
+ * the painting and sizes the dots, and is never drawn as a number.
39
+ */
40
+ depth: number;
41
+ }
42
+
43
+ /**
44
+ * The corner the box is first seen from.
45
+ *
46
+ * Slightly off both axes on purpose. Square-on, two of the three axes lie
47
+ * exactly along the screen's own and the third collapses to a point, so the
48
+ * first thing the reader sees is a flat scatter that hides a whole component —
49
+ * a quarter turn and a slight tilt say "this is a box" before the pointer has
50
+ * touched it.
51
+ */
52
+ export const DEFAULT_ORBIT_CAMERA: OrbitCamera = { yaw: 0.6, pitch: 0.5 };
53
+
54
+ /**
55
+ * How far the camera may tip, in radians.
56
+ *
57
+ * Just short of a right angle. Straight down the vertical axis the horizon
58
+ * degenerates and a further drag flips the scene over, which reads as the plot
59
+ * breaking rather than as the reader reaching the end of a range.
60
+ */
61
+ export const ORBIT_PITCH_LIMIT = 1.4;
62
+
63
+ /** How many radians one pixel of drag turns the scene. */
64
+ export const ORBIT_RADIANS_PER_PIXEL = 0.01;
65
+
66
+ /**
67
+ * Turn a point of the cube into the reader's own frame.
68
+ * @param point - The point, in cube units.
69
+ * @param camera - Where the reader is standing.
70
+ * @returns The point in the reader's frame: right, up, and towards them.
71
+ */
72
+ export function rotatePoint(point: Vector3, camera: OrbitCamera): Vector3 {
73
+ const [x, y, z] = point;
74
+ const cosPitch = Math.cos(camera.pitch);
75
+ const sinPitch = Math.sin(camera.pitch);
76
+ const upright = y * cosPitch - z * sinPitch;
77
+ const tipped = y * sinPitch + z * cosPitch;
78
+ const cosYaw = Math.cos(camera.yaw);
79
+ const sinYaw = Math.sin(camera.yaw);
80
+ return [x * cosYaw + tipped * sinYaw, upright, -x * sinYaw + tipped * cosYaw];
81
+ }
82
+
83
+ /** Where the projection puts the middle of the cube, and how large it draws it. */
84
+ export interface OrbitViewport {
85
+ /** Pixels per cube unit. */
86
+ scale: number;
87
+ /** Pixels from the left of the drawing area to the middle of the cube. */
88
+ centerX: number;
89
+ /** Pixels from its top to the middle of the cube. */
90
+ centerY: number;
91
+ }
92
+
93
+ /**
94
+ * Where a point of the cube lands on screen.
95
+ *
96
+ * The vertical axis is flipped, because a cube whose `y` grows upwards has to
97
+ * be drawn on a screen whose pixels grow downwards.
98
+ * @param point - The point, in cube units.
99
+ * @param camera - Where the reader is standing.
100
+ * @param viewport - Where the middle of the cube is, and how large it is drawn.
101
+ * @returns See {@link ProjectedPoint}.
102
+ */
103
+ export function projectPoint(
104
+ point: Vector3,
105
+ camera: OrbitCamera,
106
+ viewport: OrbitViewport,
107
+ ): ProjectedPoint {
108
+ const [right, up, towards] = rotatePoint(point, camera);
109
+ const { scale, centerX, centerY } = viewport;
110
+ return {
111
+ x: centerX + right * scale,
112
+ y: centerY - up * scale,
113
+ depth: towards,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * The camera a drag leaves behind.
119
+ *
120
+ * The pitch is held inside its limit rather than allowed to wrap, so a reader
121
+ * who keeps dragging upwards comes to rest looking down at the box instead of
122
+ * watching it turn upside down.
123
+ * @param camera - Where the reader was standing.
124
+ * @param dx - How far the pointer moved across, in pixels.
125
+ * @param dy - How far it moved down, in pixels.
126
+ * @returns Where they are standing now.
127
+ */
128
+ export function orbitByDrag(
129
+ camera: OrbitCamera,
130
+ dx: number,
131
+ dy: number,
132
+ ): OrbitCamera {
133
+ const pitch = camera.pitch + dy * ORBIT_RADIANS_PER_PIXEL;
134
+ return {
135
+ yaw: camera.yaw + dx * ORBIT_RADIANS_PER_PIXEL,
136
+ pitch: Math.min(ORBIT_PITCH_LIMIT, Math.max(-ORBIT_PITCH_LIMIT, pitch)),
137
+ };
138
+ }
139
+
140
+ /**
141
+ * The longest a cube diagonal can be, which is the range `depth` spans.
142
+ *
143
+ * A dot is sized from where it sits between the near and far corners, and this
144
+ * is what that fraction is measured against — a constant rather than the
145
+ * cloud's own extent, so a dot does not change size when a group is filtered
146
+ * out from behind it.
147
+ */
148
+ export const CUBE_HALF_DIAGONAL = Math.sqrt(3);
149
+
150
+ /**
151
+ * Where a point sits between the far corner of the cube and the near one.
152
+ * @param depth - How far towards the reader the point is, in cube units.
153
+ * @returns From 0 at the back to 1 at the front.
154
+ */
155
+ export function depthFraction(depth: number): number {
156
+ if (!Number.isFinite(depth)) return depth > 0 ? 1 : 0;
157
+ const fraction = (depth / CUBE_HALF_DIAGONAL + 1) / 2;
158
+ return Math.min(1, Math.max(0, fraction));
159
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * The axes a symmetric three by three matrix describes.
3
+ *
4
+ * The two-dimensional outline solves the same problem in four lines, because a
5
+ * symmetric two by two matrix has a closed form anybody can read. Three by
6
+ * three also has one — the eigenvalues are the roots of a cubic whose
7
+ * discriminant is never negative for a symmetric matrix, so they come out of a
8
+ * cosine rather than out of an iteration — and having it here is what lets the
9
+ * cloud fit an ellipsoid without the package depending on a matrix library.
10
+ */
11
+
12
+ import type { Vector3 } from './orbitCamera.ts';
13
+
14
+ /** A symmetric three by three matrix, as its six distinct entries. */
15
+ export interface SymmetricMatrix3 {
16
+ /** Row 1, column 1. */
17
+ xx: number;
18
+ /** Row 1, column 2, which is also row 2, column 1. */
19
+ xy: number;
20
+ /** Row 1, column 3, which is also row 3, column 1. */
21
+ xz: number;
22
+ /** Row 2, column 2. */
23
+ yy: number;
24
+ /** Row 2, column 3, which is also row 3, column 2. */
25
+ yz: number;
26
+ /** Row 3, column 3. */
27
+ zz: number;
28
+ }
29
+
30
+ /** The axes of a symmetric three by three matrix, longest first. */
31
+ export interface SymmetricEigen3 {
32
+ /** The three eigenvalues, largest first. Never below zero for a covariance. */
33
+ values: readonly [number, number, number];
34
+ /** A unit eigenvector per eigenvalue, in the same order, always orthonormal. */
35
+ vectors: readonly [Vector3, Vector3, Vector3];
36
+ }
37
+
38
+ /**
39
+ * A residue this far below the largest eigenvalue is read as zero.
40
+ *
41
+ * Squaring and summing coordinates leaves a few parts in 1e16 where the exact
42
+ * answer is nothing at all, so a flat group — every sample on one plane, which
43
+ * is what four samples in three dimensions always are — would otherwise be
44
+ * given a sliver of thickness that differs from machine to machine.
45
+ */
46
+ const FLAT_FLOOR = 1e-12;
47
+
48
+ /**
49
+ * The axes a symmetric three by three matrix describes.
50
+ *
51
+ * The eigenvectors are orthonormalised rather than taken as they come out of
52
+ * the cross products, because a repeated eigenvalue — a group that is round in
53
+ * two of its three directions, which is the common case for a small one —
54
+ * leaves that plane's two directions undetermined, and any orthogonal pair in
55
+ * it is as good an answer as any other. What must never happen is two nearly
56
+ * parallel vectors, which would draw an ellipsoid collapsed to a fan.
57
+ * @param matrix - The matrix, as its six distinct entries.
58
+ * @returns See {@link SymmetricEigen3}. A matrix with an entry that is not finite gives three zero eigenvalues on the standard axes.
59
+ */
60
+ export function symmetricEigen3(matrix: SymmetricMatrix3): SymmetricEigen3 {
61
+ const { xx, xy, xz, yy, yz, zz } = matrix;
62
+ if (
63
+ !Number.isFinite(xx) ||
64
+ !Number.isFinite(xy) ||
65
+ !Number.isFinite(xz) ||
66
+ !Number.isFinite(yy) ||
67
+ !Number.isFinite(yz) ||
68
+ !Number.isFinite(zz)
69
+ ) {
70
+ return NOTHING;
71
+ }
72
+
73
+ const values = eigenvalues(matrix);
74
+ const largest = Math.max(Math.abs(values[0]), Math.abs(values[2]));
75
+ const raw: Vector3[] = [];
76
+ for (let i = 0; i < 3; i++) {
77
+ raw.push(eigenvector(matrix, values[i] as number, largest));
78
+ }
79
+ return { values, vectors: orthonormalise(raw) };
80
+ }
81
+
82
+ /**
83
+ * The three eigenvalues, largest first.
84
+ * @param matrix - The matrix, as its six distinct entries.
85
+ * @returns The eigenvalues, in descending order.
86
+ */
87
+ function eigenvalues(
88
+ matrix: SymmetricMatrix3,
89
+ ): readonly [number, number, number] {
90
+ const { xx, xy, xz, yy, yz, zz } = matrix;
91
+ const offDiagonal = xy * xy + xz * xz + yz * yz;
92
+ if (offDiagonal === 0) {
93
+ const diagonal = [xx, yy, zz].toSorted(descending);
94
+ return [
95
+ diagonal[0] as number,
96
+ diagonal[1] as number,
97
+ diagonal[2] as number,
98
+ ];
99
+ }
100
+
101
+ const mean = (xx + yy + zz) / 3;
102
+ const spread =
103
+ ((xx - mean) * (xx - mean) +
104
+ (yy - mean) * (yy - mean) +
105
+ (zz - mean) * (zz - mean) +
106
+ 2 * offDiagonal) /
107
+ 6;
108
+ const radius = Math.sqrt(spread);
109
+ if (!(radius > 0)) return [mean, mean, mean];
110
+
111
+ // The matrix shifted to a zero trace and scaled to a unit radius; half its
112
+ // determinant is the cosine of three times the angle the three roots sit at.
113
+ const half =
114
+ determinant3({
115
+ xx: (xx - mean) / radius,
116
+ xy: xy / radius,
117
+ xz: xz / radius,
118
+ yy: (yy - mean) / radius,
119
+ yz: yz / radius,
120
+ zz: (zz - mean) / radius,
121
+ }) / 2;
122
+ const angle = Math.acos(Math.min(1, Math.max(-1, half))) / 3;
123
+
124
+ const first = mean + 2 * radius * Math.cos(angle);
125
+ const third = mean + 2 * radius * Math.cos(angle + (2 * Math.PI) / 3);
126
+ return [first, 3 * mean - first - third, third];
127
+ }
128
+
129
+ function descending(a: number, b: number): number {
130
+ return b - a;
131
+ }
132
+
133
+ function determinant3(matrix: SymmetricMatrix3): number {
134
+ const { xx, xy, xz, yy, yz, zz } = matrix;
135
+ return (
136
+ xx * (yy * zz - yz * yz) -
137
+ xy * (xy * zz - yz * xz) +
138
+ xz * (xy * yz - yy * xz)
139
+ );
140
+ }
141
+
142
+ /**
143
+ * A direction the matrix only stretches.
144
+ *
145
+ * Two rows of `matrix - λI` span the plane the eigenvector is normal to, so
146
+ * their cross product is that eigenvector. Which two rows is not a free choice:
147
+ * a row can be zero, or two rows can be nearly parallel, and the cross product
148
+ * of those is numerical dust — so all three pairs are tried and the longest
149
+ * answer is kept.
150
+ * @param matrix - The matrix.
151
+ * @param value - The eigenvalue.
152
+ * @param scale - The size of the matrix, to judge "too short" against.
153
+ * @returns A unit vector, or the zero vector when the shifted matrix vanishes, which is a repeated eigenvalue and leaves the direction to the orthonormalising step.
154
+ */
155
+ function eigenvector(
156
+ matrix: SymmetricMatrix3,
157
+ value: number,
158
+ scale: number,
159
+ ): Vector3 {
160
+ const { xx, xy, xz, yy, yz, zz } = matrix;
161
+ const rows: Vector3[] = [
162
+ [xx - value, xy, xz],
163
+ [xy, yy - value, yz],
164
+ [xz, yz, zz - value],
165
+ ];
166
+
167
+ let best: Vector3 = ZERO;
168
+ let bestLength = 0;
169
+ for (let i = 0; i < 3; i++) {
170
+ for (let j = i + 1; j < 3; j++) {
171
+ const candidate = cross(rows[i] as Vector3, rows[j] as Vector3);
172
+ const length = norm(candidate);
173
+ if (length > bestLength) {
174
+ best = candidate;
175
+ bestLength = length;
176
+ }
177
+ }
178
+ }
179
+ // A cross product of two rows is quadratic in the matrix, so the length it
180
+ // has to beat is too.
181
+ if (bestLength <= FLAT_FLOOR * Math.max(scale * scale, Number.MIN_VALUE)) {
182
+ return ZERO;
183
+ }
184
+ return scaleVector(best, 1 / bestLength);
185
+ }
186
+
187
+ /**
188
+ * Three orthonormal directions, keeping the ones that are worth keeping.
189
+ *
190
+ * A vector that survives is normalised against the ones already accepted; one
191
+ * that does not is replaced by whatever direction is still free, which is what
192
+ * gives a round group and a flat one a complete frame instead of a degenerate
193
+ * one.
194
+ * @param raw - The directions as the cross products left them.
195
+ * @returns Three unit vectors, mutually orthogonal.
196
+ */
197
+ function orthonormalise(
198
+ raw: readonly Vector3[],
199
+ ): readonly [Vector3, Vector3, Vector3] {
200
+ const kept: Vector3[] = [];
201
+ for (const vector of raw) kept.push(against(vector, kept));
202
+ for (let i = 0; i < 3; i++) {
203
+ if (kept[i] !== ZERO) continue;
204
+ kept[i] = against(freeDirection(kept), kept);
205
+ }
206
+ return [kept[0] as Vector3, kept[1] as Vector3, kept[2] as Vector3];
207
+ }
208
+
209
+ /**
210
+ * One vector with every accepted direction taken out of it, then normalised.
211
+ * @param vector - The direction to keep only what is new about.
212
+ * @param accepted - The directions already settled on.
213
+ * @returns The unit remainder, or the zero vector when nothing was left of it.
214
+ */
215
+ function against(vector: Vector3, accepted: readonly Vector3[]): Vector3 {
216
+ let [x, y, z] = vector;
217
+ for (const other of accepted) {
218
+ const along = x * other[0] + y * other[1] + z * other[2];
219
+ x -= along * other[0];
220
+ y -= along * other[1];
221
+ z -= along * other[2];
222
+ }
223
+ const length = norm([x, y, z]);
224
+ if (!(length > 1e-6)) return ZERO;
225
+ return [x / length, y / length, z / length];
226
+ }
227
+
228
+ /**
229
+ * A standard axis that the accepted directions do not already cover.
230
+ * @param accepted - The directions already settled on.
231
+ * @returns The axis least accounted for by them.
232
+ */
233
+ function freeDirection(accepted: readonly Vector3[]): Vector3 {
234
+ let best: Vector3 = AXES[0];
235
+ let leastCovered = Number.POSITIVE_INFINITY;
236
+ for (const axis of AXES) {
237
+ let covered = 0;
238
+ for (const other of accepted) {
239
+ const along =
240
+ axis[0] * other[0] + axis[1] * other[1] + axis[2] * other[2];
241
+ covered += along * along;
242
+ }
243
+ if (covered < leastCovered) {
244
+ leastCovered = covered;
245
+ best = axis;
246
+ }
247
+ }
248
+ return best;
249
+ }
250
+
251
+ function cross(a: Vector3, b: Vector3): Vector3 {
252
+ return [
253
+ a[1] * b[2] - a[2] * b[1],
254
+ a[2] * b[0] - a[0] * b[2],
255
+ a[0] * b[1] - a[1] * b[0],
256
+ ];
257
+ }
258
+
259
+ function norm(vector: Vector3): number {
260
+ return Math.hypot(vector[0], vector[1], vector[2]);
261
+ }
262
+
263
+ function scaleVector(vector: Vector3, by: number): Vector3 {
264
+ return [vector[0] * by, vector[1] * by, vector[2] * by];
265
+ }
266
+
267
+ const ZERO: Vector3 = [0, 0, 0];
268
+ const AXES: readonly [Vector3, Vector3, Vector3] = [
269
+ [1, 0, 0],
270
+ [0, 1, 0],
271
+ [0, 0, 1],
272
+ ];
273
+ const NOTHING: SymmetricEigen3 = { values: [0, 0, 0], vectors: AXES };
@@ -0,0 +1,97 @@
1
+ import type { ReactElement } from 'react';
2
+ import { memo } from 'react';
3
+
4
+ import { CUBE_ARMS, CUBE_ORIGIN, cubeEdges } from '../core/cubeFrame.ts';
5
+ import type { OrbitCamera, OrbitViewport } from '../core/orbitCamera.ts';
6
+ import { projectPoint } from '../core/orbitCamera.ts';
7
+
8
+ import { projectSegments } from './cloudGeometry.ts';
9
+
10
+ /** What {@link CloudFrameLayer} draws. */
11
+ export interface CloudFrameLayerProps {
12
+ /** Where the reader is standing. */
13
+ camera: OrbitCamera;
14
+ /** Where the middle of the cube is, and how large it is drawn. */
15
+ viewport: OrbitViewport;
16
+ /** What the axis running left to right is called. */
17
+ xLabel: string;
18
+ /** What the axis running bottom to top is called. */
19
+ yLabel: string;
20
+ /** What the axis running away from the reader is called. */
21
+ zLabel: string;
22
+ }
23
+
24
+ /**
25
+ * The box the cloud sits in, and the three arms that name its axes.
26
+ *
27
+ * A cloud of dots on an empty background has no orientation: turn it and
28
+ * nothing says it turned, and a dot near the top of the picture could be near
29
+ * or far. The frame is what fixes that, and it is why the projection can stay
30
+ * orthographic — it carries the depth that perspective would otherwise have
31
+ * to, and unlike perspective it distorts no distance the reader is there to
32
+ * compare.
33
+ *
34
+ * All twelve edges are drawn, not the three at the back: which edges are
35
+ * hidden changes as the box turns, and a frame that kept rearranging itself
36
+ * would read as three axes rather than as one box.
37
+ * @param props - See {@link CloudFrameLayerProps}.
38
+ * @returns The frame.
39
+ */
40
+ export const CloudFrameLayer = memo(function CloudFrameLayer(
41
+ props: CloudFrameLayerProps,
42
+ ): ReactElement {
43
+ const { camera, viewport, xLabel, yLabel, zLabel } = props;
44
+
45
+ const edges = projectSegments(EDGES, camera, viewport);
46
+ const origin = projectPoint(CUBE_ORIGIN, camera, viewport);
47
+ const names = { x: xLabel, y: yLabel, z: zLabel };
48
+
49
+ return (
50
+ <g data-layer="frame" pointerEvents="none">
51
+ {edges.map((edge, at) => (
52
+ <line
53
+ key={at}
54
+ x1={edge.x1}
55
+ y1={edge.y1}
56
+ x2={edge.x2}
57
+ y2={edge.y2}
58
+ stroke="var(--border)"
59
+ strokeWidth={1}
60
+ />
61
+ ))}
62
+ {CUBE_ARMS.map((arm) => {
63
+ const end = projectPoint(arm.end, camera, viewport);
64
+ const label = projectPoint(arm.label, camera, viewport);
65
+ return (
66
+ <g key={arm.axis}>
67
+ <line
68
+ x1={origin.x}
69
+ y1={origin.y}
70
+ x2={end.x}
71
+ y2={end.y}
72
+ stroke="var(--border-strong)"
73
+ strokeWidth={1.5}
74
+ />
75
+ <text
76
+ x={label.x}
77
+ y={label.y}
78
+ textAnchor="middle"
79
+ dominantBaseline="central"
80
+ fontSize={ARM_LABEL_SIZE}
81
+ fontWeight={600}
82
+ fill="var(--text-muted)"
83
+ >
84
+ {names[arm.axis]}
85
+ </text>
86
+ </g>
87
+ );
88
+ })}
89
+ </g>
90
+ );
91
+ });
92
+
93
+ /** Cut once, because the corners of a cube do not depend on the camera. */
94
+ const EDGES = cubeEdges();
95
+
96
+ /** The same size a chart writes an axis title at, so the two read alike. */
97
+ const ARM_LABEL_SIZE = 11;
@@ -0,0 +1,132 @@
1
+ import type { ReactElement } from 'react';
2
+ import { memo } from 'react';
3
+
4
+ import type { ScreenPoints } from '../../scatter/core/screenPoints.ts';
5
+ import { depthFraction } from '../core/orbitCamera.ts';
6
+
7
+ /** What {@link CloudPointLayer} draws. */
8
+ export interface CloudPointLayerProps {
9
+ /** Where every sample sits, in the figure's pixels. */
10
+ points: ScreenPoints;
11
+ /** How far towards the reader each one is, in cube units. */
12
+ depths: Float64Array;
13
+ /** The rows, furthest first, which is the order they are painted in. */
14
+ order: Uint32Array;
15
+ /**
16
+ * Which group each sample belongs to, as an index into `colors`. An entry of
17
+ * `-1`, or one outside the range, is a sample in no group.
18
+ * @default undefined — every sample is one crowd
19
+ */
20
+ groupOf?: ArrayLike<number>;
21
+ /**
22
+ * The colour of each group, in group order.
23
+ * @default undefined — every sample takes `fallbackColor`
24
+ */
25
+ colors?: readonly string[];
26
+ /**
27
+ * How strongly each group is drawn, in the same order.
28
+ * @default undefined — every group is drawn at full strength
29
+ */
30
+ opacities?: readonly number[];
31
+ /**
32
+ * Colour of a sample belonging to no group.
33
+ * @default 'var(--text-faint)'
34
+ */
35
+ fallbackColor?: string;
36
+ /**
37
+ * Radius of a dot at the middle of the cube, in pixels. A dot at the front
38
+ * is drawn larger and one at the back smaller, about this.
39
+ * @default 3.5
40
+ */
41
+ radius?: number;
42
+ /**
43
+ * The index from which samples are drawn as outlines rather than filled,
44
+ * which is how one the model was fitted on is told from one placed into it
45
+ * afterwards.
46
+ * @default undefined — every sample is filled
47
+ */
48
+ outlinedFrom?: number;
49
+ }
50
+
51
+ /**
52
+ * Every sample as one dot, painted from the back of the box forwards.
53
+ *
54
+ * Two things carry the depth, and neither costs an event handler: the order,
55
+ * so a dot in front covers one behind it, and the size, so a dot at the front
56
+ * of the box is a fifth larger than the same dot at the back. Both are read
57
+ * from the projection rather than from the data, so a turn changes them and a
58
+ * filter does not.
59
+ *
60
+ * Like the flat cloud's own layer, this one carries no handler and no closure
61
+ * per dot: every gesture lands on one transparent rectangle above it and is
62
+ * answered from a coordinate array.
63
+ * @param props - See {@link CloudPointLayerProps}.
64
+ * @returns The cloud.
65
+ */
66
+ export const CloudPointLayer = memo(function CloudPointLayer(
67
+ props: CloudPointLayerProps,
68
+ ): ReactElement {
69
+ const { points, depths, order, groupOf, colors, opacities } = props;
70
+ const { fallbackColor = 'var(--text-faint)', radius = 3.5 } = props;
71
+ const { outlinedFrom } = props;
72
+
73
+ const xs = points.x;
74
+ const ys = points.y;
75
+ const count = Math.min(xs.length, ys.length, order.length);
76
+ const hollowFrom = outlineStart(outlinedFrom, count);
77
+
78
+ const dots: ReactElement[] = [];
79
+ for (let step = 0; step < count; step++) {
80
+ const index = order[step] as number;
81
+ const x = xs[index];
82
+ const y = ys[index];
83
+ if (x === undefined || y === undefined) continue;
84
+ if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
85
+ const group = groupOf?.[index] ?? -1;
86
+ const color = colors?.[group] ?? fallbackColor;
87
+ const alpha = opacities?.[group] ?? 1;
88
+ const hollow = index >= hollowFrom;
89
+ dots.push(
90
+ <circle
91
+ key={index}
92
+ cx={round(x)}
93
+ cy={round(y)}
94
+ r={round(radius * depthScale(depths[index] as number))}
95
+ fill={hollow ? 'none' : color}
96
+ stroke={hollow ? color : undefined}
97
+ strokeWidth={hollow ? OUTLINE_WIDTH : undefined}
98
+ opacity={alpha === 1 ? undefined : alpha}
99
+ />,
100
+ );
101
+ }
102
+
103
+ return <g data-layer="points">{dots}</g>;
104
+ });
105
+
106
+ /**
107
+ * How much larger a dot is drawn for sitting near the front of the box.
108
+ *
109
+ * A fifth from back to front. Enough that the eye reads it as depth when it is
110
+ * looking for depth, and little enough that nobody mistakes it for a value:
111
+ * a dot size that carried a measurement would be a fourth dimension nothing
112
+ * labelled.
113
+ * @param depth - How far towards the reader the sample is, in cube units.
114
+ * @returns The factor to draw its radius at.
115
+ */
116
+ function depthScale(depth: number): number {
117
+ return 0.9 + DEPTH_RANGE * depthFraction(depth);
118
+ }
119
+
120
+ function outlineStart(outlinedFrom: number | undefined, count: number): number {
121
+ if (outlinedFrom === undefined || !Number.isFinite(outlinedFrom)) {
122
+ return count;
123
+ }
124
+ return Math.max(0, Math.floor(outlinedFrom));
125
+ }
126
+
127
+ const DEPTH_RANGE = 0.4;
128
+
129
+ /** The same weight the flat cloud draws a hollow dot at, so the two read alike. */
130
+ const OUTLINE_WIDTH = 1.5;
131
+
132
+ const round = (value: number): number => Math.round(value * 100) / 100;