react-cheminfo 0.49.0 → 0.51.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 (269) hide show
  1. package/README.md +23 -0
  2. package/bin/check-seo.mjs +27 -2
  3. package/lib/clipboard/core/index.d.ts +0 -1
  4. package/lib/clipboard/core/index.d.ts.map +1 -1
  5. package/lib/clipboard/core/index.js +0 -1
  6. package/lib/clipboard/core/index.js.map +1 -1
  7. package/lib/clipboard/ui/CopyButton.d.ts +6 -0
  8. package/lib/clipboard/ui/CopyButton.d.ts.map +1 -1
  9. package/lib/clipboard/ui/CopyButton.js +5 -2
  10. package/lib/clipboard/ui/CopyButton.js.map +1 -1
  11. package/lib/color/core/index.d.ts +3 -0
  12. package/lib/color/core/index.d.ts.map +1 -1
  13. package/lib/color/core/index.js +2 -0
  14. package/lib/color/core/index.js.map +1 -1
  15. package/lib/color/core/interpolate.d.ts +4 -2
  16. package/lib/color/core/interpolate.d.ts.map +1 -1
  17. package/lib/color/core/interpolate.js +34 -12
  18. package/lib/color/core/interpolate.js.map +1 -1
  19. package/lib/color/core/stops.d.ts +43 -0
  20. package/lib/color/core/stops.d.ts.map +1 -0
  21. package/lib/color/core/stops.js +66 -0
  22. package/lib/color/core/stops.js.map +1 -0
  23. package/lib/color/core/tone.d.ts +25 -0
  24. package/lib/color/core/tone.d.ts.map +1 -0
  25. package/lib/color/core/tone.js +67 -0
  26. package/lib/color/core/tone.js.map +1 -0
  27. package/lib/color/core/uniform.d.ts +36 -0
  28. package/lib/color/core/uniform.d.ts.map +1 -0
  29. package/lib/color/core/uniform.js +61 -0
  30. package/lib/color/core/uniform.js.map +1 -0
  31. package/lib/color/ui/ColorScaleEditor.d.ts +5 -6
  32. package/lib/color/ui/ColorScaleEditor.d.ts.map +1 -1
  33. package/lib/color/ui/ColorScaleEditor.js +74 -84
  34. package/lib/color/ui/ColorScaleEditor.js.map +1 -1
  35. package/lib/color/ui/ColorScaleSelect.d.ts +16 -0
  36. package/lib/color/ui/ColorScaleSelect.d.ts.map +1 -1
  37. package/lib/color/ui/ColorScaleSelect.js +69 -17
  38. package/lib/color/ui/ColorScaleSelect.js.map +1 -1
  39. package/lib/color/ui/ColorScaleTone.d.ts +17 -0
  40. package/lib/color/ui/ColorScaleTone.d.ts.map +1 -0
  41. package/lib/color/ui/ColorScaleTone.js +55 -0
  42. package/lib/color/ui/ColorScaleTone.js.map +1 -0
  43. package/lib/color/ui/ColorScaleTrack.d.ts +29 -0
  44. package/lib/color/ui/ColorScaleTrack.d.ts.map +1 -0
  45. package/lib/color/ui/ColorScaleTrack.js +143 -0
  46. package/lib/color/ui/ColorScaleTrack.js.map +1 -0
  47. package/lib/download/core/copyFigure.d.ts +14 -0
  48. package/lib/download/core/copyFigure.d.ts.map +1 -0
  49. package/lib/download/core/copyFigure.js +18 -0
  50. package/lib/download/core/copyFigure.js.map +1 -0
  51. package/lib/download/core/figureHtml.js +1 -0
  52. package/lib/download/core/figureHtml.js.map +1 -1
  53. package/lib/download/core/figureHtmlMarkup.d.ts +10 -0
  54. package/lib/download/core/figureHtmlMarkup.d.ts.map +1 -1
  55. package/lib/download/core/figureHtmlMarkup.js +7 -1
  56. package/lib/download/core/figureHtmlMarkup.js.map +1 -1
  57. package/lib/download/core/index.d.ts +2 -0
  58. package/lib/download/core/index.d.ts.map +1 -1
  59. package/lib/download/core/index.js +1 -0
  60. package/lib/download/core/index.js.map +1 -1
  61. package/lib/download/ui/FigureDownloadPanel.d.ts +2 -3
  62. package/lib/download/ui/FigureDownloadPanel.d.ts.map +1 -1
  63. package/lib/download/ui/FigureDownloadPanel.js +1 -1
  64. package/lib/download/ui/FigureDownloadPanel.js.map +1 -1
  65. package/lib/download/ui/figureDownloadHint.d.ts +1 -1
  66. package/lib/download/ui/figureDownloadHint.d.ts.map +1 -1
  67. package/lib/download/ui/figureDownloadHint.js.map +1 -1
  68. package/lib/download/ui/index.d.ts +0 -1
  69. package/lib/download/ui/index.d.ts.map +1 -1
  70. package/lib/download/ui/useFigureActions.d.ts +2 -11
  71. package/lib/download/ui/useFigureActions.d.ts.map +1 -1
  72. package/lib/download/ui/useFigureActions.js +4 -23
  73. package/lib/download/ui/useFigureActions.js.map +1 -1
  74. package/lib/download/ui/useFigureTask.d.ts +21 -0
  75. package/lib/download/ui/useFigureTask.d.ts.map +1 -0
  76. package/lib/download/ui/useFigureTask.js +27 -0
  77. package/lib/download/ui/useFigureTask.js.map +1 -0
  78. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  79. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  80. package/lib/locales/de.json +28 -2
  81. package/lib/locales/en.json +28 -2
  82. package/lib/locales/es.json +28 -2
  83. package/lib/locales/fr.json +28 -2
  84. package/lib/locales/it.json +28 -2
  85. package/lib/molecule3d/core/index.d.ts +1 -1
  86. package/lib/molecule3d/core/index.d.ts.map +1 -1
  87. package/lib/molecule3d/core/index.js +1 -1
  88. package/lib/molecule3d/core/index.js.map +1 -1
  89. package/lib/molecule3d/core/settings.d.ts +15 -0
  90. package/lib/molecule3d/core/settings.d.ts.map +1 -1
  91. package/lib/molecule3d/core/settings.js +25 -0
  92. package/lib/molecule3d/core/settings.js.map +1 -1
  93. package/lib/molecule3d/ui/Molecule3DExport.d.ts +3 -0
  94. package/lib/molecule3d/ui/Molecule3DExport.d.ts.map +1 -1
  95. package/lib/molecule3d/ui/Molecule3DExport.js +10 -16
  96. package/lib/molecule3d/ui/Molecule3DExport.js.map +1 -1
  97. package/lib/molecule3d/ui/Molecule3DOptions.d.ts +3 -1
  98. package/lib/molecule3d/ui/Molecule3DOptions.d.ts.map +1 -1
  99. package/lib/molecule3d/ui/Molecule3DOptions.js +22 -6
  100. package/lib/molecule3d/ui/Molecule3DOptions.js.map +1 -1
  101. package/lib/molecule3d/ui/MoleculeCanvas3D.d.ts.map +1 -1
  102. package/lib/molecule3d/ui/MoleculeCanvas3D.js +4 -3
  103. package/lib/molecule3d/ui/MoleculeCanvas3D.js.map +1 -1
  104. package/lib/molecule3d/ui/exportMoleculeImage.d.ts +9 -0
  105. package/lib/molecule3d/ui/exportMoleculeImage.d.ts.map +1 -1
  106. package/lib/molecule3d/ui/exportMoleculeImage.js +15 -0
  107. package/lib/molecule3d/ui/exportMoleculeImage.js.map +1 -1
  108. package/lib/molecule3d/ui/moleculeViewer3DProps.d.ts +2 -1
  109. package/lib/molecule3d/ui/moleculeViewer3DProps.d.ts.map +1 -1
  110. package/lib/molecule3d/ui/useImageExport.d.ts +7 -1
  111. package/lib/molecule3d/ui/useImageExport.d.ts.map +1 -1
  112. package/lib/molecule3d/ui/useImageExport.js +12 -3
  113. package/lib/molecule3d/ui/useImageExport.js.map +1 -1
  114. package/lib/overlay/ui/FigureBar.d.ts +41 -0
  115. package/lib/overlay/ui/FigureBar.d.ts.map +1 -0
  116. package/lib/overlay/ui/FigureBar.js +43 -0
  117. package/lib/overlay/ui/FigureBar.js.map +1 -0
  118. package/lib/overlay/ui/OverlayBar.d.ts +13 -4
  119. package/lib/overlay/ui/OverlayBar.d.ts.map +1 -1
  120. package/lib/overlay/ui/OverlayBar.js +2 -2
  121. package/lib/overlay/ui/OverlayBar.js.map +1 -1
  122. package/lib/overlay/ui/OverlayBarStrip.d.ts +2 -1
  123. package/lib/overlay/ui/OverlayBarStrip.d.ts.map +1 -1
  124. package/lib/overlay/ui/OverlayBarStrip.js +9 -5
  125. package/lib/overlay/ui/OverlayBarStrip.js.map +1 -1
  126. package/lib/overlay/ui/OverlayIconButton.d.ts +8 -2
  127. package/lib/overlay/ui/OverlayIconButton.d.ts.map +1 -1
  128. package/lib/overlay/ui/OverlayIconButton.js +2 -1
  129. package/lib/overlay/ui/OverlayIconButton.js.map +1 -1
  130. package/lib/overlay/ui/index.d.ts +4 -0
  131. package/lib/overlay/ui/index.d.ts.map +1 -1
  132. package/lib/overlay/ui/index.js +2 -0
  133. package/lib/overlay/ui/index.js.map +1 -1
  134. package/lib/overlay/ui/overlayBarShape.d.ts +18 -0
  135. package/lib/overlay/ui/overlayBarShape.d.ts.map +1 -1
  136. package/lib/overlay/ui/overlayStyles.d.ts +30 -0
  137. package/lib/overlay/ui/overlayStyles.d.ts.map +1 -1
  138. package/lib/overlay/ui/overlayStyles.js +52 -0
  139. package/lib/overlay/ui/overlayStyles.js.map +1 -1
  140. package/lib/overlay/ui/useFigureBarAwake.d.ts +12 -0
  141. package/lib/overlay/ui/useFigureBarAwake.d.ts.map +1 -0
  142. package/lib/overlay/ui/useFigureBarAwake.js +51 -0
  143. package/lib/overlay/ui/useFigureBarAwake.js.map +1 -0
  144. package/lib/pages.d.ts +3 -0
  145. package/lib/pages.d.ts.map +1 -0
  146. package/lib/pages.js +2 -0
  147. package/lib/pages.js.map +1 -0
  148. package/lib/periodic/core/elements.d.ts +6 -3
  149. package/lib/periodic/core/elements.d.ts.map +1 -1
  150. package/lib/periodic/core/elements.js +9 -3
  151. package/lib/periodic/core/elements.js.map +1 -1
  152. package/lib/periodic/core/index.d.ts +2 -2
  153. package/lib/periodic/core/index.d.ts.map +1 -1
  154. package/lib/periodic/core/index.js +1 -1
  155. package/lib/periodic/core/index.js.map +1 -1
  156. package/lib/periodic/core/layout.d.ts +33 -3
  157. package/lib/periodic/core/layout.d.ts.map +1 -1
  158. package/lib/periodic/core/layout.js +14 -0
  159. package/lib/periodic/core/layout.js.map +1 -1
  160. package/lib/periodic/ui/BlockStrip.d.ts +20 -0
  161. package/lib/periodic/ui/BlockStrip.d.ts.map +1 -0
  162. package/lib/periodic/ui/BlockStrip.js +48 -0
  163. package/lib/periodic/ui/BlockStrip.js.map +1 -0
  164. package/lib/periodic/ui/ElementCell.d.ts.map +1 -1
  165. package/lib/periodic/ui/ElementCell.js +9 -8
  166. package/lib/periodic/ui/ElementCell.js.map +1 -1
  167. package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
  168. package/lib/periodic/ui/PeriodicTable.js +3 -1
  169. package/lib/periodic/ui/PeriodicTable.js.map +1 -1
  170. package/lib/periodic/ui/PeriodicTableHelp.d.ts +33 -0
  171. package/lib/periodic/ui/PeriodicTableHelp.d.ts.map +1 -0
  172. package/lib/periodic/ui/PeriodicTableHelp.js +107 -0
  173. package/lib/periodic/ui/PeriodicTableHelp.js.map +1 -0
  174. package/lib/periodic/ui/cellStyles.d.ts +3 -2
  175. package/lib/periodic/ui/cellStyles.d.ts.map +1 -1
  176. package/lib/periodic/ui/cellStyles.js +3 -2
  177. package/lib/periodic/ui/cellStyles.js.map +1 -1
  178. package/lib/periodic/ui/index.d.ts +2 -0
  179. package/lib/periodic/ui/index.d.ts.map +1 -1
  180. package/lib/periodic/ui/index.js +1 -0
  181. package/lib/periodic/ui/index.js.map +1 -1
  182. package/lib/periodic/ui/periodicTableProps.d.ts +5 -4
  183. package/lib/periodic/ui/periodicTableProps.d.ts.map +1 -1
  184. package/lib/periodic/ui/tableLayout.d.ts +3 -1
  185. package/lib/periodic/ui/tableLayout.d.ts.map +1 -1
  186. package/lib/periodic/ui/tableLayout.js +8 -3
  187. package/lib/periodic/ui/tableLayout.js.map +1 -1
  188. package/lib/seo/core/trackingMiddleware.d.ts +27 -0
  189. package/lib/seo/core/trackingMiddleware.d.ts.map +1 -0
  190. package/lib/seo/core/trackingMiddleware.js +33 -0
  191. package/lib/seo/core/trackingMiddleware.js.map +1 -0
  192. package/lib/share/ui/ShareDialogContent.d.ts +3 -2
  193. package/lib/share/ui/ShareDialogContent.d.ts.map +1 -1
  194. package/lib/share/ui/ShareDialogContent.js +5 -6
  195. package/lib/share/ui/ShareDialogContent.js.map +1 -1
  196. package/lib/share/ui/ShareLinkBar.d.ts +8 -2
  197. package/lib/share/ui/ShareLinkBar.d.ts.map +1 -1
  198. package/lib/share/ui/ShareLinkBar.js +6 -5
  199. package/lib/share/ui/ShareLinkBar.js.map +1 -1
  200. package/lib/share/ui/SharePreview.d.ts.map +1 -1
  201. package/lib/share/ui/SharePreview.js +2 -2
  202. package/lib/share/ui/SharePreview.js.map +1 -1
  203. package/lib/structure/ui/StructurePicturePane.js +3 -3
  204. package/lib/structure/ui/StructurePicturePane.js.map +1 -1
  205. package/package.json +2 -1
  206. package/src/clipboard/core/index.ts +0 -1
  207. package/src/clipboard/ui/CopyButton.tsx +12 -1
  208. package/src/color/core/index.ts +9 -0
  209. package/src/color/core/interpolate.ts +51 -22
  210. package/src/color/core/stops.ts +98 -0
  211. package/src/color/core/tone.ts +79 -0
  212. package/src/color/core/uniform.ts +67 -0
  213. package/src/color/ui/ColorScaleEditor.tsx +115 -141
  214. package/src/color/ui/ColorScaleSelect.tsx +160 -36
  215. package/src/color/ui/ColorScaleTone.tsx +103 -0
  216. package/src/color/ui/ColorScaleTrack.tsx +239 -0
  217. package/src/download/core/copyFigure.ts +20 -0
  218. package/src/download/core/figureHtml.ts +1 -0
  219. package/src/download/core/figureHtmlMarkup.ts +14 -1
  220. package/src/download/core/index.ts +2 -0
  221. package/src/download/ui/FigureDownloadPanel.tsx +8 -11
  222. package/src/download/ui/figureDownloadHint.ts +1 -2
  223. package/src/download/ui/index.ts +0 -1
  224. package/src/download/ui/useFigureActions.ts +6 -42
  225. package/src/download/ui/useFigureTask.ts +46 -0
  226. package/src/locales/de.json +28 -2
  227. package/src/locales/en.json +28 -2
  228. package/src/locales/es.json +28 -2
  229. package/src/locales/fr.json +28 -2
  230. package/src/locales/it.json +28 -2
  231. package/src/molecule3d/core/index.ts +2 -0
  232. package/src/molecule3d/core/settings.ts +35 -0
  233. package/src/molecule3d/ui/Molecule3DExport.tsx +18 -17
  234. package/src/molecule3d/ui/Molecule3DOptions.tsx +51 -3
  235. package/src/molecule3d/ui/MoleculeCanvas3D.tsx +11 -3
  236. package/src/molecule3d/ui/exportMoleculeImage.ts +20 -0
  237. package/src/molecule3d/ui/moleculeViewer3DProps.ts +2 -1
  238. package/src/molecule3d/ui/useImageExport.ts +25 -3
  239. package/src/overlay/ui/FigureBar.tsx +96 -0
  240. package/src/overlay/ui/OverlayBar.tsx +15 -5
  241. package/src/overlay/ui/OverlayBarStrip.tsx +34 -22
  242. package/src/overlay/ui/OverlayIconButton.tsx +10 -2
  243. package/src/overlay/ui/index.ts +4 -0
  244. package/src/overlay/ui/overlayBarShape.ts +19 -0
  245. package/src/overlay/ui/overlayStyles.ts +61 -0
  246. package/src/overlay/ui/useFigureBarAwake.ts +56 -0
  247. package/src/pages.ts +2 -0
  248. package/src/periodic/core/elements.ts +8 -3
  249. package/src/periodic/core/index.ts +8 -1
  250. package/src/periodic/core/layout.ts +45 -3
  251. package/src/periodic/ui/BlockStrip.tsx +81 -0
  252. package/src/periodic/ui/ElementCell.tsx +9 -8
  253. package/src/periodic/ui/PeriodicTable.tsx +4 -1
  254. package/src/periodic/ui/PeriodicTableHelp.tsx +205 -0
  255. package/src/periodic/ui/cellStyles.ts +3 -2
  256. package/src/periodic/ui/index.ts +5 -0
  257. package/src/periodic/ui/periodicTableProps.ts +5 -4
  258. package/src/periodic/ui/tableLayout.ts +9 -3
  259. package/src/seo/core/trackingMiddleware.ts +48 -0
  260. package/src/share/ui/ShareDialogContent.tsx +9 -13
  261. package/src/share/ui/ShareLinkBar.tsx +16 -4
  262. package/src/share/ui/SharePreview.tsx +6 -2
  263. package/src/structure/ui/StructurePicturePane.tsx +3 -3
  264. package/styles/chrome.css +43 -32
  265. package/lib/clipboard/core/copyPng.d.ts +0 -13
  266. package/lib/clipboard/core/copyPng.d.ts.map +0 -1
  267. package/lib/clipboard/core/copyPng.js +0 -19
  268. package/lib/clipboard/core/copyPng.js.map +0 -1
  269. package/src/clipboard/core/copyPng.ts +0 -20
@@ -3,6 +3,7 @@ import { clamp } from '../../format/core/clamp.ts';
3
3
  import type { ReadableInkOptions } from './contrast.ts';
4
4
  import { readableInk } from './contrast.ts';
5
5
  import { parseHexColor, toHexColor } from './hex.ts';
6
+ import type { HsvColor } from './hsv.ts';
6
7
  import { hsvToRgb, rgbToHsv, wrapHue } from './hsv.ts';
7
8
 
8
9
  const HALF_TURN = 180;
@@ -33,8 +34,10 @@ export interface ColorStop {
33
34
  * - `hsv` turns along the colour wheel the short way round, so two anchors
34
35
  * already describe a ramp that keeps its saturation instead of fading
35
36
  * through the grey in the middle of the straight line between them.
36
- * - `hsv-long` turns the other way round, which is how two anchors of the same
37
- * hue draw a full rainbow.
37
+ * - `hsv-long` turns the whole scale one way round the wheel: the long way
38
+ * between its two ends, which is how two anchors draw a rainbow. Each anchor
39
+ * in between is reached by turning that same way, never by a turn of its own,
40
+ * so adding one where the scale already has its colour changes nothing.
38
41
  */
39
42
  export type ColorInterpolation = 'rgb' | 'hsv' | 'hsv-long';
40
43
 
@@ -94,7 +97,11 @@ export function colorAt(scale: ColorScale, position: number): string {
94
97
  if (at > end.position) continue;
95
98
  const span = end.position - start.position;
96
99
  const ratio = span <= 0 ? 0 : (at - start.position) / span;
97
- return mix(start.color, end.color, ratio, interpolation);
100
+ return mix(start.color, end.color, ratio, {
101
+ interpolation,
102
+ turn: longTurn(first.color, last.color),
103
+ whole: stops.length === MINIMUM_SAMPLES,
104
+ });
98
105
  }
99
106
  return toHexColor(parseHexColor(last.color));
100
107
  }
@@ -134,15 +141,18 @@ export function sampleScale(scale: ColorScale, count: number): string[] {
134
141
  return colors;
135
142
  }
136
143
 
137
- function mix(
138
- start: string,
139
- end: string,
140
- ratio: number,
141
- interpolation: ColorInterpolation,
142
- ): string {
144
+ interface HuePath {
145
+ interpolation: ColorInterpolation;
146
+ /** The way the long path turns: 1 up the wheel, -1 down it. */
147
+ turn: number;
148
+ /** Whether the two anchors are the whole scale, so one hue on both is a full turn. */
149
+ whole: boolean;
150
+ }
151
+
152
+ function mix(start: string, end: string, ratio: number, path: HuePath): string {
143
153
  const from = parseHexColor(start);
144
154
  const to = parseHexColor(end);
145
- if (interpolation === 'rgb') {
155
+ if (path.interpolation === 'rgb') {
146
156
  return toHexColor({
147
157
  red: from.red + (to.red - from.red) * ratio,
148
158
  green: from.green + (to.green - from.green) * ratio,
@@ -152,7 +162,8 @@ function mix(
152
162
 
153
163
  const one = rgbToHsv(from);
154
164
  const other = rgbToHsv(to);
155
- const hue = one.hue + hueStep(one.hue, other.hue, interpolation) * ratio;
165
+ const [fromHue, toHue] = pairHues(one, other);
166
+ const hue = fromHue + hueStep(fromHue, toHue, path) * ratio;
156
167
  return toHexColor(
157
168
  hsvToRgb({
158
169
  hue: wrapHue(hue),
@@ -162,15 +173,33 @@ function mix(
162
173
  );
163
174
  }
164
175
 
165
- function hueStep(
166
- from: number,
167
- to: number,
168
- interpolation: ColorInterpolation,
169
- ): number {
170
- const short = (((to - from) % TURN) + TURN + HALF_TURN) % TURN;
171
- const shortest = short - HALF_TURN;
172
- if (interpolation !== 'hsv-long') return shortest;
173
- // Two anchors of the same hue are a full turn apart the long way round,
174
- // which is what draws a rainbow from one colour back to itself.
175
- return shortest > 0 ? shortest - TURN : shortest + TURN;
176
+ // A grey, black or white has no hue of its own, so it takes the other one's:
177
+ // the path then fades to it instead of sweeping through red on the way.
178
+ function pairHues(one: HsvColor, other: HsvColor): [number, number] {
179
+ if (one.saturation === 0) return [other.hue, other.hue];
180
+ if (other.saturation === 0) return [one.hue, one.hue];
181
+ return [one.hue, other.hue];
182
+ }
183
+
184
+ function hueStep(from: number, to: number, path: HuePath): number {
185
+ if (path.interpolation !== 'hsv-long') return shortestStep(from, to);
186
+ const step = path.turn > 0 ? upStep(from, to) : -upStep(to, from);
187
+ return step === 0 && path.whole ? path.turn * TURN : step;
188
+ }
189
+
190
+ function longTurn(first: string, last: string): number {
191
+ const [from, to] = pairHues(
192
+ rgbToHsv(parseHexColor(first)),
193
+ rgbToHsv(parseHexColor(last)),
194
+ );
195
+ // Two ends of one hue are a full turn apart, taken up the wheel.
196
+ return shortestStep(from, to) > 0 ? -1 : 1;
197
+ }
198
+
199
+ function shortestStep(from: number, to: number): number {
200
+ return ((((to - from) % TURN) + TURN + HALF_TURN) % TURN) - HALF_TURN;
201
+ }
202
+
203
+ function upStep(from: number, to: number): number {
204
+ return (((to - from) % TURN) + TURN) % TURN;
176
205
  }
@@ -0,0 +1,98 @@
1
+ import { clamp } from '../../format/core/clamp.ts';
2
+
3
+ import type { ColorScale, ColorStop } from './interpolate.ts';
4
+ import { colorAt } from './interpolate.ts';
5
+ import { MAXIMUM_CUSTOM_STOPS } from './scaleText.ts';
6
+
7
+ /** How few anchors a scale may have: one at each end of what it draws. */
8
+ export const MINIMUM_CUSTOM_STOPS = 2;
9
+
10
+ // A link writes three decimals, so an anchor is held where the link puts it.
11
+ const POSITION_FACTOR = 1000;
12
+
13
+ /** The anchors after an edit, and where the edited one now stands among them. */
14
+ export interface EditedStops {
15
+ /** The anchors, from the low end. */
16
+ stops: ColorStop[];
17
+ /** The index of the anchor that was added or moved. */
18
+ index: number;
19
+ }
20
+
21
+ /**
22
+ * Add an anchor where the reader clicked, in the colour the scale already has
23
+ * there — so adding one changes nothing until it is moved or recoloured.
24
+ * @param scale - The scale being edited.
25
+ * @param position - Where to add it, from 0 at the low end to 1 at the high end.
26
+ * @returns The anchors with the new one, or `null` when the scale already holds as many as a link may carry.
27
+ */
28
+ export function addColorStop(
29
+ scale: ColorScale,
30
+ position: number,
31
+ ): EditedStops | null {
32
+ if (scale.stops.length >= MAXIMUM_CUSTOM_STOPS) return null;
33
+ const at = roundPosition(position);
34
+ const added: ColorStop = { position: at, color: colorAt(scale, at) };
35
+ return sortAround([...scale.stops, added], added);
36
+ }
37
+
38
+ /**
39
+ * Move an anchor. It may pass its neighbours: the anchors are kept in order,
40
+ * and the returned index follows the one that moved.
41
+ * @param stops - The anchors, from the low end.
42
+ * @param index - Which of them moves.
43
+ * @param position - Where to, clamped to the scale.
44
+ * @returns The anchors in their new order, and where the moved one landed.
45
+ */
46
+ export function moveColorStop(
47
+ stops: readonly ColorStop[],
48
+ index: number,
49
+ position: number,
50
+ ): EditedStops {
51
+ const stop = stops[index];
52
+ if (stop === undefined || !Number.isFinite(position)) {
53
+ return { stops: [...stops], index };
54
+ }
55
+ const moved: ColorStop = { ...stop, position: roundPosition(position) };
56
+ return sortAround(
57
+ stops.map((other, at) => (at === index ? moved : other)),
58
+ moved,
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Remove an anchor, unless the scale would be left with fewer than two.
64
+ * @param stops - The anchors, from the low end.
65
+ * @param index - Which of them to remove.
66
+ * @returns The remaining anchors, or the same ones when none may go.
67
+ */
68
+ export function removeColorStop(
69
+ stops: readonly ColorStop[],
70
+ index: number,
71
+ ): ColorStop[] {
72
+ if (stops.length <= MINIMUM_CUSTOM_STOPS) return [...stops];
73
+ return stops.filter((_, at) => at !== index);
74
+ }
75
+
76
+ /**
77
+ * Recolour an anchor.
78
+ * @param stops - The anchors, from the low end.
79
+ * @param index - Which of them to recolour.
80
+ * @param color - Its new colour.
81
+ * @returns The anchors, with that one recoloured.
82
+ */
83
+ export function recolorColorStop(
84
+ stops: readonly ColorStop[],
85
+ index: number,
86
+ color: string,
87
+ ): ColorStop[] {
88
+ return stops.map((stop, at) => (at === index ? { ...stop, color } : stop));
89
+ }
90
+
91
+ function roundPosition(position: number): number {
92
+ return Math.round(clamp(position, 0, 1) * POSITION_FACTOR) / POSITION_FACTOR;
93
+ }
94
+
95
+ function sortAround(stops: ColorStop[], edited: ColorStop): EditedStops {
96
+ const sorted = stops.toSorted((one, other) => one.position - other.position);
97
+ return { stops: sorted, index: sorted.indexOf(edited) };
98
+ }
@@ -0,0 +1,79 @@
1
+ import { clamp } from '../../format/core/clamp.ts';
2
+
3
+ import { parseHexColor, toHexColor } from './hex.ts';
4
+ import type { HsvColor } from './hsv.ts';
5
+ import { hsvToRgb, rgbToHsv } from './hsv.ts';
6
+ import type { ColorStop } from './interpolate.ts';
7
+
8
+ /** How strong and how bright a scale's colours are, taken over all its anchors. */
9
+ export interface ScaleTone {
10
+ /** How far the anchors are from grey, from 0 to 1. */
11
+ saturation: number;
12
+ /** How bright they are, from 0 for black to 1. */
13
+ value: number;
14
+ }
15
+
16
+ /**
17
+ * The saturation and the brightness of a scale's anchors, averaged: what a
18
+ * control setting both for the whole scale opens on.
19
+ * @param stops - The anchors.
20
+ * @returns Their mean saturation and mean value; both 0 when there is no anchor.
21
+ */
22
+ export function scaleTone(stops: readonly ColorStop[]): ScaleTone {
23
+ const colors = toHsv(stops);
24
+ if (colors.length === 0) return { saturation: 0, value: 0 };
25
+ let saturation = 0;
26
+ let value = 0;
27
+ for (const color of colors) {
28
+ saturation += color.saturation;
29
+ value += color.value;
30
+ }
31
+ return {
32
+ saturation: saturation / colors.length,
33
+ value: value / colors.length,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Give every anchor the same saturation, the same brightness, or both, keeping
39
+ * each one's hue — so the scale then only turns round the wheel. A grey anchor
40
+ * has no hue of its own and takes the hue of the nearest one that has.
41
+ * @param stops - The anchors.
42
+ * @param tone - What to set; a field left out keeps each anchor's own.
43
+ * @returns The recoloured anchors, in the same order.
44
+ */
45
+ export function setScaleTone(
46
+ stops: readonly ColorStop[],
47
+ tone: Partial<ScaleTone>,
48
+ ): ColorStop[] {
49
+ const colors = toHsv(stops);
50
+ const result: ColorStop[] = [];
51
+ for (let index = 0; index < stops.length; index++) {
52
+ const stop = stops[index];
53
+ const color = colors[index];
54
+ if (stop === undefined || color === undefined) continue;
55
+ const toned = hsvToRgb({
56
+ hue: hueNear(colors, index),
57
+ saturation: clamp(tone.saturation ?? color.saturation, 0, 1),
58
+ value: clamp(tone.value ?? color.value, 0, 1),
59
+ });
60
+ result.push({ ...stop, color: toHexColor(toned) });
61
+ }
62
+ return result;
63
+ }
64
+
65
+ function toHsv(stops: readonly ColorStop[]): HsvColor[] {
66
+ const colors: HsvColor[] = [];
67
+ for (const stop of stops) colors.push(rgbToHsv(parseHexColor(stop.color)));
68
+ return colors;
69
+ }
70
+
71
+ function hueNear(colors: readonly HsvColor[], index: number): number {
72
+ for (let distance = 0; distance < colors.length; distance++) {
73
+ const before = colors[index - distance];
74
+ if (before !== undefined && before.saturation > 0) return before.hue;
75
+ const after = colors[index + distance];
76
+ if (after !== undefined && after.saturation > 0) return after.hue;
77
+ }
78
+ return 0;
79
+ }
@@ -0,0 +1,67 @@
1
+ import { TOKEN } from '../../tokens/core/familyTokens.ts';
2
+
3
+ import { readableInk } from './contrast.ts';
4
+ import { normalizeHexColor } from './hex.ts';
5
+ import type { Swatch } from './interpolate.ts';
6
+
7
+ /**
8
+ * What a colour-scale picker offering one colour writes for it, and a link
9
+ * carries: every value the same colour, so the figure needs no key. Followed by
10
+ * `-` and a hex colour, e.g. `uniform-1c6e42`, it names that colour.
11
+ */
12
+ export const UNIFORM_COLOR_SCALE_ID = 'uniform';
13
+
14
+ /**
15
+ * The one colour of a uniform scale that names none: the family's border grey
16
+ * rather than white, so a cell still stands out from a white page and from the
17
+ * white ground of a saved figure.
18
+ */
19
+ export const UNIFORM_SWATCH: Swatch = {
20
+ background: TOKEN.border,
21
+ foreground: TOKEN.text,
22
+ };
23
+
24
+ const PREFIX = `${UNIFORM_COLOR_SCALE_ID}-`;
25
+
26
+ /**
27
+ * Whether a scale text asks for one colour.
28
+ * @param text - A scale id or a custom scale, as a link carries it.
29
+ * @returns `true` for {@link UNIFORM_COLOR_SCALE_ID}, with or without a colour.
30
+ */
31
+ export function isUniformColorScale(text: string | undefined): boolean {
32
+ const wanted = (text ?? '').trim().toLowerCase();
33
+ return wanted === UNIFORM_COLOR_SCALE_ID || wanted.startsWith(PREFIX);
34
+ }
35
+
36
+ /**
37
+ * The colour a uniform scale text names, and the ink to write on it.
38
+ *
39
+ * A colour with a typo in it falls back to the grey rather than failing, so a
40
+ * link written by hand still opens the figure.
41
+ * @param text - A scale text, e.g. `uniform` or `uniform-1c6e42`.
42
+ * @returns The swatch it names, or {@link UNIFORM_SWATCH}.
43
+ */
44
+ export function uniformSwatch(text: string | undefined): Swatch {
45
+ const color = uniformColor(text);
46
+ if (color === null) return UNIFORM_SWATCH;
47
+ return { background: color, foreground: readableInk(color) };
48
+ }
49
+
50
+ /**
51
+ * Write one colour as the scale text a link carries; the `#` is dropped, since
52
+ * a query string is cut in two at the first one.
53
+ * @param color - The colour, as `#rgb` or `#rrggbb`.
54
+ * @returns E.g. `uniform-1c6e42`, or {@link UNIFORM_COLOR_SCALE_ID} when the colour is not a hex colour.
55
+ */
56
+ export function formatUniformColorScale(color: string): string {
57
+ const normalized = normalizeHexColor(color);
58
+ return normalized === null
59
+ ? UNIFORM_COLOR_SCALE_ID
60
+ : `${PREFIX}${normalized.slice(1)}`;
61
+ }
62
+
63
+ function uniformColor(text: string | undefined): string | null {
64
+ const wanted = (text ?? '').trim().toLowerCase();
65
+ if (!wanted.startsWith(PREFIX)) return null;
66
+ return normalizeHexColor(`#${wanted.slice(PREFIX.length)}`);
67
+ }