form.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. const selectors = {
  2. get sortControl() {
  3. return document.forms.sortControl.elements;
  4. },
  5. get clusterControl() {
  6. return document.forms.clusterControl.elements;
  7. },
  8. get resultsToDisplay() {
  9. return selectors.sortControl.resultsToDisplay.value;
  10. },
  11. get colorSpace() {
  12. return selectors.sortControl.colorSpace.value;
  13. },
  14. get sortMetric() {
  15. return selectors.sortControl.sortMetric.value;
  16. },
  17. get sortOrder() {
  18. return selectors.sortControl.sortOrder.checked ? "max" : "min";
  19. },
  20. get scaleFactor() {
  21. return selectors.sortControl.rescaleFactor.value;
  22. },
  23. get useClusters() {
  24. return selectors.sortControl.useClusters.value;
  25. },
  26. get clusterSortMetric() {
  27. return selectors.clusterControl.sortMetric.value;
  28. },
  29. get clusterSortOrder() {
  30. return selectors.clusterControl.sortOrder.checked ? "max" : "min";
  31. },
  32. get clusterScaleFactor() {
  33. return selectors.clusterControl.rescaleFactor.value;
  34. },
  35. get prevColors() {
  36. return document.getElementById("prev-colors");
  37. },
  38. set background(hex) {
  39. document.querySelector(":root").style.setProperty("--background", hex);
  40. },
  41. set highlight(hex) {
  42. document.querySelector(":root").style.setProperty("--highlight", hex);
  43. },
  44. };
  45. const onMetricChange = (elements, skipUpdates = false) => {
  46. elements.sortOrderLabel.value = elements.sortOrder.checked
  47. ? "Maximizing"
  48. : "Minimizing";
  49. const kind = elements.metricKind.value;
  50. elements.whole.disabled = kind !== "whole";
  51. elements.mean.disabled = kind !== "mean";
  52. elements.statistic.disabled = kind !== "statistic";
  53. elements.sortMetric.value = elements[kind].value;
  54. if (!skipUpdates) {
  55. updateSort();
  56. }
  57. };
  58. const onColorChange = (inputValue, skipUpdates = false) => {
  59. const colorInput = "#" + (inputValue?.replace("#", "") ?? "FFFFFF");
  60. if (colorInput.length !== 7) {
  61. return;
  62. }
  63. const rgb = d3.color(colorInput);
  64. if (!rgb) {
  65. return;
  66. }
  67. const hex = rgb.formatHex();
  68. selectors.sortControl.colorText.value = hex;
  69. selectors.sortControl.colorPicker.value = hex;
  70. const contrast = getContrastingTextColor(hex);
  71. const newColor = document.createElement("div");
  72. newColor.innerHTML = hex;
  73. newColor.style = `
  74. color: ${contrast};
  75. background-color: ${hex};
  76. `;
  77. selectors.prevColors.prepend(newColor);
  78. selectors.background = hex;
  79. selectors.highlight = contrast;
  80. if (!skipUpdates) {
  81. updateScores(rgb);
  82. updateSort();
  83. }
  84. };
  85. const randomColor = () =>
  86. d3.hsl(Math.random() * 360, Math.random(), Math.random()).formatHex();
  87. const calcScores = (data, target) => {
  88. const sigma = Math.sqrt(
  89. data.inertia - 2 * vectorDot(data.mu.vector, target.vector) + target.sqMag
  90. );
  91. const bigTheta = 1 - vectorDot(data.nu, target.unit);
  92. const rawPhi = Math.abs(data.mu.hue - target.hue);
  93. return {
  94. sigma,
  95. bigTheta,
  96. alpha: sigma * Math.pow(bigTheta, target.chroma + target.lightness),
  97. theta: rad2deg * Math.acos(vectorDot(data.mu.unit, target.unit)),
  98. phi: Math.min(rawPhi, 360 - rawPhi),
  99. delta: vectorMag(data.mu.vector.map((x, i) => x - target.vector[i])),
  100. manhattan: data.mu.vector
  101. .map((x, i) => Math.abs(x - target.vector[i]))
  102. .reduce((x, y) => x + y),
  103. ch: Math.max(...data.mu.vector.map((x, i) => Math.abs(x - target.vector[i]))),
  104. lightnessDiff: Math.abs(data.mu.lightness - target.lightness),
  105. inertia: data.inertia,
  106. variance: data.inertia - data.mu.sqMag,
  107. muNuAngle: data.muNuAngle,
  108. size: data.size,
  109. lightness: data.mu.lightness,
  110. chroma: data.mu.chroma,
  111. importance: data.importance,
  112. inverseSize: data.inverseSize,
  113. proportion: data.proportion,
  114. inverseProportion: data.inverseProportion,
  115. };
  116. };
  117. const sortOrders = {
  118. max: (a, b) => b - a,
  119. min: (a, b) => a - b,
  120. };
  121. const scaleOptions = {
  122. none: () => [1, 1, 1],
  123. direct: (scores) => scores.clusters.map((c) => c.proportion),
  124. inverse: (scores) => scores.clusters.map((c) => c.inverseProportion),
  125. size: (scores) => [scores.total.size, scores.total.size, scores.total.size],
  126. inverseSize: (scores) => [
  127. scores.total.inverseSize,
  128. scores.total.inverseSize,
  129. scores.total.inverseSize,
  130. ],
  131. };
  132. const currentScores = {};
  133. const currentBestClusterIndices = {};
  134. let sortedData = [];
  135. const updateScores = (rgb) => {
  136. const { J, a, b } = d3.jab(rgb);
  137. const targetJab = buildVectorData([J, a, b], jab2hue, jab2lit, jab2chroma, jab2hex);
  138. const targetRgb = buildVectorData(
  139. [rgb.r, rgb.g, rgb.b],
  140. rgb2hue,
  141. rgb2lit,
  142. rgb2chroma,
  143. rgb2hex
  144. );
  145. pokemonData.forEach(({ name, jab, rgb }) => {
  146. currentScores[name] = {
  147. jab: {
  148. total: calcScores(jab.total, targetJab),
  149. clusters: jab.clusters.map((c) => calcScores(c, targetJab)),
  150. },
  151. rgb: {
  152. total: calcScores(rgb.total, targetRgb),
  153. clusters: rgb.clusters.map((c) => calcScores(c, targetRgb)),
  154. },
  155. };
  156. });
  157. };
  158. const updateSort = () => {
  159. // update cluster rankings
  160. const clusterSortOrder = sortOrders[selectors.clusterSortOrder];
  161. const clusterScaleOption = scaleOptions[selectors.clusterScaleFactor];
  162. pokemonData.forEach(({ name }) => {
  163. const { jab, rgb } = currentScores[name];
  164. // multiply scale with the intended metric, and find the index of the best value
  165. const forSpace = (clusters, scales) =>
  166. clusters
  167. .map((c, i) => [c[selectors.clusterSortMetric] * scales[i], i])
  168. .reduce((a, b) => (clusterSortOrder(a[0], b[0]) > 0 ? b : a))[1];
  169. currentBestClusterIndices[name] = {
  170. jab: forSpace(jab.clusters, clusterScaleOption(jab)),
  171. rgb: forSpace(rgb.clusters, clusterScaleOption(rgb)),
  172. };
  173. });
  174. // set up for actual sort
  175. const scaleOption = scaleOptions[selectors.scaleFactor];
  176. let valueExtractor;
  177. switch (selectors.useClusters) {
  178. case "off":
  179. valueExtractor = (name) =>
  180. currentScores[name][selectors.colorSpace].total[selectors.sortMetric];
  181. break;
  182. case "on":
  183. valueExtractor = (name) => {
  184. const index = currentBestClusterIndices[name][selectors.colorSpace];
  185. return (
  186. scaleOption(currentScores[name][selectors.colorSpace])[index] *
  187. currentScores[name][selectors.colorSpace].clusters[index][selectors.sortMetric]
  188. );
  189. };
  190. break;
  191. case "mult":
  192. valueExtractor = (name) => {
  193. const index = currentBestClusterIndices[name][selectors.colorSpace];
  194. return (
  195. currentScores[name][selectors.colorSpace].total[selectors.sortMetric] *
  196. scaleOption(currentScores[name][selectors.colorSpace])[index] *
  197. currentScores[name][selectors.colorSpace].clusters[index][selectors.sortMetric]
  198. );
  199. };
  200. break;
  201. }
  202. // update actual sorted data
  203. const sortOrder = sortOrders[selectors.sortOrder];
  204. sortedData = pokemonData
  205. .map(({ name }) => name)
  206. .sort((a, b) => sortOrder(valueExtractor(a), valueExtractor(b)));
  207. // and desplay results
  208. showResults();
  209. };
  210. const showResults = () => {
  211. // TODO
  212. console.log(sortedData.slice(0, selectors.resultsToDisplay));
  213. };
  214. window.addEventListener("load", () => {
  215. const metricTemplate = document.getElementById("metric-form-template").content;
  216. selectors.sortControl.metric.appendChild(metricTemplate.cloneNode(true));
  217. selectors.sortControl.metricKind.value = "whole";
  218. selectors.sortControl.whole.value = "alpha";
  219. onMetricChange(selectors.sortControl, true);
  220. selectors.clusterControl.metric.appendChild(metricTemplate.cloneNode(true));
  221. selectors.clusterControl.sortOrder.checked = true;
  222. selectors.clusterControl.metricKind.value = "statistic";
  223. selectors.clusterControl.statistic.value = "importance";
  224. onMetricChange(selectors.clusterControl, true);
  225. const scaleTemplate = document.getElementById("scale-form-template").content;
  226. selectors.sortControl.rescaleSection.appendChild(scaleTemplate.cloneNode(true));
  227. selectors.sortControl.rescaleFactor.value = "inverse";
  228. selectors.clusterControl.rescaleSection.appendChild(scaleTemplate.cloneNode(true));
  229. selectors.clusterControl.rescaleFactor.value = "none";
  230. document.body.addEventListener("click", ({ target: { innerText }, detail }) => {
  231. if (detail === 2 && innerText?.includes("#")) {
  232. const clickedHex = innerText?.match(/.*(#[0-9a-fA-F]{6}).*/)?.[1] ?? "";
  233. if (clickedHex) {
  234. onColorChange(clickedHex);
  235. }
  236. }
  237. });
  238. onColorChange(randomColor());
  239. });