main.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. // ---- Math and Utilities ----
  2. // Vector Math
  3. const vectorDot = (u, v) => u.map((x, i) => x * v[i]).reduce((x, y) => x + y);
  4. const vectorMag = v => Math.sqrt(vectorDot(v, v));
  5. // Angle Math
  6. const rad2deg = 180 / Math.PI;
  7. // Misc
  8. const clamp = (mn, v, mx) => Math.min(Math.max(v, mn), mx);
  9. const productLift =
  10. (...factors) =>
  11. (...args) =>
  12. factors
  13. .filter(fn => !!fn)
  14. .map(fn => fn(...args))
  15. .reduce((x, y) => x * y, 1);
  16. // Contrast + Shadow + Hover Colors
  17. const getContrastingTextColor = hex => {
  18. const { r, g, b } = d3.color(hex);
  19. return vectorDot([r, g, b], [0.3, 0.6, 0.1]) >= 128
  20. ? "var(--color-dark)"
  21. : "var(--color-light)";
  22. };
  23. // "Visual Importance"
  24. const calcImportance = (chroma, lightness, proportion) =>
  25. chroma +
  26. Math.tanh(100 * (chroma - 0.25)) + // penalty for being <25%
  27. Math.tanh(100 * (chroma - 0.4)) + // penalty for being <40%
  28. lightness +
  29. Math.tanh(100 * (lightness - 0.5)) + // penalty for being <50%
  30. proportion +
  31. Math.tanh(100 * (proportion - 0.05)) + // penalty for being <5%
  32. Math.tanh(100 * (proportion - 0.1)) + // penalty for being <15%
  33. Math.tanh(100 * (proportion - 0.15)) + // penalty for being <15%
  34. Math.tanh(100 * (proportion - 0.25)) + // penalty for being <25%
  35. Math.tanh(100 * (proportion - 0.8)); // penalty for being <50%
  36. // Conversions
  37. const jab2hex = jab => d3.jab(...jab).formatHex();
  38. const rgb2hex = rgb => d3.rgb(...rgb).formatHex();
  39. const jab2hue = jab => d3.jch(d3.jab(...jab)).h || 0;
  40. const rgb2hue = rgb => d3.hsl(d3.rgb(...rgb)).h || 0;
  41. const jab2lit = ([j]) => j / 100;
  42. const rgb2lit = rgb => d3.hsl(d3.rgb(...rgb)).l || 0;
  43. const jab2chroma = jab => d3.jch(d3.jab(...jab)).C / 100;
  44. const rgb2chroma = rgb => d3.jch(d3.rgb(...rgb)).C / 100;
  45. // Pre-computation
  46. const buildVectorData = (vector, toHue, toLightness, toChroma, toHex) => {
  47. const sqMag = vectorDot(vector, vector);
  48. const mag = Math.sqrt(sqMag);
  49. const unit = vector.map(c => c / mag);
  50. const hue = toHue(vector);
  51. const lightness = toLightness(vector);
  52. const chroma = toChroma(vector);
  53. const hex = toHex(vector);
  54. return { vector, sqMag, mag, unit, hue, lightness, chroma, hex };
  55. };
  56. const buildVectorDataJab = vector =>
  57. buildVectorData(vector, jab2hue, jab2lit, jab2chroma, jab2hex);
  58. const buildVectorDataRgb = vector =>
  59. buildVectorData(vector, rgb2hue, rgb2lit, rgb2chroma, rgb2hex);
  60. const buildClusterData = (
  61. size,
  62. inertia,
  63. mu1,
  64. mu2,
  65. mu3,
  66. nu1,
  67. nu2,
  68. nu3,
  69. totalSize,
  70. buildVectorDataForSpace
  71. ) => {
  72. const mu = buildVectorDataForSpace([mu1, mu2, mu3]);
  73. const nu = [nu1, nu2, nu3];
  74. const muNuAngle = rad2deg * Math.acos(vectorDot(mu.unit, nu) / vectorMag(nu));
  75. const proportion = size / totalSize;
  76. const importance = calcImportance(mu.chroma, mu.lightness, proportion);
  77. return {
  78. size,
  79. inverseSize: 1 / size,
  80. inertia,
  81. mu,
  82. nu,
  83. muNuAngle,
  84. proportion,
  85. inverseProportion: 1 / proportion,
  86. importance,
  87. };
  88. };
  89. const buildPokemonData = ([name, size, ...values]) => ({
  90. name,
  91. jab: {
  92. total: buildClusterData(size, ...values.slice(0, 7), size, buildVectorDataJab),
  93. clusters: [
  94. buildClusterData(...values.slice(7, 15), size, buildVectorDataJab),
  95. buildClusterData(...values.slice(15, 23), size, buildVectorDataJab),
  96. buildClusterData(...values.slice(23, 31), size, buildVectorDataJab),
  97. buildClusterData(...values.slice(31, 39), size, buildVectorDataJab),
  98. ].filter(c => c.size !== 0),
  99. },
  100. rgb: {
  101. total: buildClusterData(size, ...values.slice(39, 46), size, buildVectorDataRgb),
  102. clusters: [
  103. buildClusterData(...values.slice(46, 54), size, buildVectorDataRgb),
  104. buildClusterData(...values.slice(54, 62), size, buildVectorDataRgb),
  105. buildClusterData(...values.slice(62, 70), size, buildVectorDataRgb),
  106. buildClusterData(...values.slice(70, 78), size, buildVectorDataRgb),
  107. ].filter(c => c.size !== 0),
  108. },
  109. });
  110. const pokemonData = databaseV3.map(row => buildPokemonData(row));
  111. const calcScores = (data, target) => {
  112. const sigma = Math.sqrt(
  113. data.inertia - 2 * vectorDot(data.mu.vector, target.vector) + target.sqMag
  114. );
  115. const bigTheta = 1 - vectorDot(data.nu, target.unit);
  116. const rawPhi = Math.abs(data.mu.hue - target.hue);
  117. return {
  118. sigma,
  119. bigTheta,
  120. alpha: sigma * Math.pow(bigTheta, target.chroma + target.lightness),
  121. theta: rad2deg * Math.acos(vectorDot(data.mu.unit, target.unit)),
  122. phi: Math.min(rawPhi, 360 - rawPhi),
  123. delta: vectorMag(data.mu.vector.map((x, i) => x - target.vector[i])),
  124. manhattan: data.mu.vector
  125. .map((x, i) => Math.abs(x - target.vector[i]))
  126. .reduce((x, y) => x + y),
  127. ch: Math.max(...data.mu.vector.map((x, i) => Math.abs(x - target.vector[i]))),
  128. lightnessDiff: Math.abs(data.mu.lightness - target.lightness),
  129. inertia: data.inertia,
  130. variance: data.inertia - data.mu.sqMag,
  131. muNuAngle: data.muNuAngle,
  132. size: data.size,
  133. lightness: data.mu.lightness,
  134. chroma: data.mu.chroma,
  135. importance: data.importance,
  136. inverseSize: data.inverseSize,
  137. proportion: data.proportion,
  138. inverseProportion: data.inverseProportion,
  139. muHex: data.mu.hex,
  140. };
  141. };
  142. const sortOrders = {
  143. max: (a, b) => b - a,
  144. min: (a, b) => a - b,
  145. };
  146. // ---- Styling ----
  147. const rootStyle = document.querySelector(":root").style;
  148. const setColorStyles = (style, hex) => {
  149. const highlight = getContrastingTextColor(hex);
  150. style.setProperty("--highlight", highlight);
  151. style.setProperty("--background", hex);
  152. style.setProperty("--shadow-component", highlight.includes("light") ? "255" : "0");
  153. };
  154. // ---- Pokemon Display ----
  155. // pulled out bc the render uses them
  156. const metricScores = {};
  157. const bestClusterIndices = {};
  158. const objectiveValues = {};
  159. const pokemonTileTemplate = document.getElementById("pkmn-tile-template").content;
  160. const pokemonDataTemplate = document.getElementById("pkmn-data-template").content;
  161. const loadTemplateWithBinds = content => {
  162. const fragment = content.cloneNode(true);
  163. const binds = Object.fromEntries(
  164. Array.from(fragment.querySelectorAll("[bind-to]")).map(element => {
  165. const name = element.getAttribute("bind-to");
  166. element.removeAttribute("bind-to");
  167. return [name, element];
  168. })
  169. );
  170. return [fragment, binds];
  171. };
  172. const getSpriteName = (() => {
  173. const stripForm = [
  174. "flabebe",
  175. "floette",
  176. "florges",
  177. "vivillon",
  178. "basculin",
  179. "furfrou",
  180. "magearna",
  181. "alcremie",
  182. ];
  183. return pokemon => {
  184. pokemon = pokemon
  185. .replace("-alola", "-alolan")
  186. .replace("-galar", "-galarian")
  187. .replace("-hisui", "-hisuian")
  188. .replace("-paldea", "-paldean")
  189. .replace("-paldeanfire", "-paldean-fire") // tauros
  190. .replace("-paldeanwater", "-paldean-water") // tauros
  191. .replace("-phony", "") // sinistea and polteageist
  192. .replace("darmanitan-galarian", "darmanitan-galarian-standard")
  193. .replace("chienpao", "chien-pao")
  194. .replace("tinglu", "ting-lu")
  195. .replace("wochien", "wo-chien")
  196. .replace("chiyu", "chi-yu");
  197. if (stripForm.find(s => pokemon.includes(s))) {
  198. pokemon = pokemon.replace(/-.*$/, "");
  199. }
  200. return pokemon;
  201. };
  202. })();
  203. const formatName = name =>
  204. name
  205. .split("-")
  206. .map(part => part.charAt(0).toUpperCase() + part.substr(1))
  207. .join(" ");
  208. const renderPokemon = (list, target) => {
  209. target.innerText = "";
  210. const {
  211. sortUseWholeImage,
  212. sortUseBestCluster,
  213. sortUseClusterSize,
  214. sortUseInvClusterSize,
  215. sortUseTotalSize,
  216. sortUseInvTotalSize,
  217. } = Object.fromEntries(new FormData(document.forms.colorCalculateForm).entries());
  218. const enableTotalFlags = !!(
  219. sortUseWholeImage ||
  220. sortUseTotalSize ||
  221. sortUseInvTotalSize
  222. );
  223. const enableClusterFlags = !!(
  224. sortUseBestCluster ||
  225. sortUseClusterSize ||
  226. sortUseInvClusterSize
  227. );
  228. list.forEach(pkmnName => {
  229. const [tile, { image, name, score, ...binds }] =
  230. loadTemplateWithBinds(pokemonTileTemplate);
  231. const spriteName = getSpriteName(pkmnName);
  232. const imageErrHandler = () => {
  233. image.removeEventListener("error", imageErrHandler);
  234. image.src = `https://img.pokemondb.net/sprites/scarlet-violet/icon/${spriteName}.png`;
  235. };
  236. image.addEventListener("error", imageErrHandler);
  237. image.src = `https://img.pokemondb.net/sprites/sword-shield/icon/${spriteName}.png`;
  238. name.innerText = name.title = image.alt = formatName(pkmnName);
  239. const colorSpace = document.forms.colorSortForm.elements.colorSpace.value;
  240. score.innerText = objectiveValues[pkmnName][colorSpace].toFixed(2);
  241. const { total, clusters } = metricScores[pkmnName][colorSpace];
  242. [
  243. [clusters[0], binds.cls1Btn, binds.cls1Data],
  244. [clusters[1], binds.cls2Btn, binds.cls2Data],
  245. [clusters[2], binds.cls3Btn, binds.cls3Data],
  246. [clusters[3], binds.cls4Btn, binds.cls4Data],
  247. [total, binds.totalBtn, binds.totalData],
  248. ]
  249. .filter(([data]) => !!data)
  250. .forEach(([data, button, dataTile], index) => {
  251. button.hidden = false;
  252. button.innerText = data.muHex;
  253. button.dataset.included =
  254. enableClusterFlags && index === bestClusterIndices[pkmnName][colorSpace];
  255. setColorStyles(button.style, data.muHex);
  256. button.addEventListener("click", () => {
  257. model.setTargetColor(data.muHex);
  258. });
  259. const [tooltip, tooltipBinds] = loadTemplateWithBinds(pokemonDataTemplate);
  260. Object.entries(tooltipBinds).forEach(([metricName, target]) => {
  261. target.innerText = data[metricName].toFixed(2).replace(".00", "");
  262. });
  263. dataTile.append(tooltip);
  264. });
  265. binds.totalBtn.dataset.included = enableTotalFlags;
  266. target.append(tile);
  267. });
  268. };
  269. const colorSearchResultsTarget = document.getElementById("color-results");
  270. const nameSearchResultsTarget = document.getElementById("name-results");
  271. // ---- Name Lookup ----
  272. const lookupLimit = 24;
  273. const pokemonLookup = new Fuse(pokemonData, { keys: ["name"] });
  274. const nameSearchResults = [];
  275. const renderNameSearchResults = () => {
  276. renderPokemon(nameSearchResults, nameSearchResultsTarget);
  277. };
  278. document.forms.nameSearchForm.elements.input.addEventListener(
  279. "input",
  280. ({ target: { value } }) => {
  281. nameSearchResults.splice(
  282. 0,
  283. Infinity,
  284. ...pokemonLookup
  285. .search(value, { limit: lookupLimit })
  286. .map(({ item: { name } }) => name)
  287. );
  288. renderNameSearchResults();
  289. }
  290. );
  291. document.forms.nameSearchForm.elements.clear.addEventListener("click", () => {
  292. nameSearchResults.splice(0);
  293. document.forms.nameSearchForm.elements.input.value = "";
  294. renderNameSearchResults();
  295. });
  296. document.forms.nameSearchForm.elements.random.addEventListener("click", () => {
  297. nameSearchResults.splice(
  298. 0,
  299. Infinity,
  300. ...Array.from(
  301. { length: lookupLimit },
  302. () => pokemonData[Math.floor(Math.random() * pokemonData.length)].name
  303. )
  304. );
  305. renderNameSearchResults();
  306. });
  307. // ---- Calculation Logic ----
  308. const model = new (class {
  309. #targetColor = "";
  310. ranked = [];
  311. setTargetColor(newColor) {
  312. const hex = `#${newColor?.replace("#", "")}`;
  313. if (hex.length !== 7) {
  314. return;
  315. }
  316. setColorStyles(rootStyle, hex);
  317. const oldColor = this.#targetColor;
  318. this.#targetColor = hex;
  319. document.forms.targetColorForm.elements.colorText.value = hex;
  320. document.forms.targetColorForm.elements.colorText.dataset.lastValid = hex;
  321. document.forms.targetColorForm.elements.colorPicker.value = hex;
  322. if (oldColor) {
  323. const prevButton = document.createElement("button");
  324. prevButton.innerText = oldColor;
  325. prevButton.classList = "color-select";
  326. setColorStyles(prevButton.style, oldColor);
  327. prevButton.addEventListener("click", () => this.setTargetColor(oldColor));
  328. document.getElementById("prevColors").prepend(prevButton);
  329. }
  330. const rgb = d3.rgb(hex);
  331. const { J, a, b } = d3.jab(rgb);
  332. const targetJab = buildVectorDataJab([J, a, b]);
  333. const targetRgb = buildVectorDataRgb([rgb.r, rgb.g, rgb.b]);
  334. pokemonData.forEach(({ name, jab, rgb }) => {
  335. metricScores[name] = {
  336. jab: {
  337. total: calcScores(jab.total, targetJab),
  338. clusters: jab.clusters.map(c => calcScores(c, targetJab)),
  339. },
  340. rgb: {
  341. total: calcScores(rgb.total, targetRgb),
  342. clusters: rgb.clusters.map(c => calcScores(c, targetRgb)),
  343. },
  344. };
  345. });
  346. this.calculateObjective();
  347. }
  348. calculateObjective() {
  349. const {
  350. clusterUseClusterSize,
  351. clusterUseInvClusterSize,
  352. clusterUseTotalSize,
  353. clusterUseInvTotalSize,
  354. clusterSortOrder,
  355. sortUseWholeImage,
  356. sortUseBestCluster,
  357. sortUseClusterSize,
  358. sortUseInvClusterSize,
  359. sortUseTotalSize,
  360. sortUseInvTotalSize,
  361. } = Object.fromEntries(new FormData(document.forms.colorCalculateForm).entries());
  362. const clsMetric = document.forms.clusterMetricForm.elements.metric.value;
  363. const getClusterScore = productLift(
  364. cluster => cluster[clsMetric],
  365. clusterUseClusterSize && (cluster => cluster.size),
  366. clusterUseInvClusterSize && (cluster => cluster.inverseSize),
  367. clusterUseTotalSize && ((_, total) => total.size),
  368. clusterUseInvTotalSize && ((_, total) => total.inverseSize)
  369. );
  370. const clsSort = sortOrders[clusterSortOrder];
  371. const getBestClusterIndex = ({ total, clusters }) =>
  372. clusters
  373. .map((c, i) => [getClusterScore(c, total), i])
  374. .reduce((a, b) => (clsSort(a[0], b[0]) > 0 ? b : a))[1];
  375. Object.entries(metricScores).forEach(([name, { jab, rgb }]) => {
  376. bestClusterIndices[name] = {
  377. jab: getBestClusterIndex(jab),
  378. rgb: getBestClusterIndex(rgb),
  379. };
  380. });
  381. const metric = document.forms.sortMetricForm.elements.metric.value;
  382. const getSortScore = productLift(
  383. sortUseWholeImage && (({ total }) => total[metric]),
  384. sortUseBestCluster && (({ clusters }, i) => clusters[i][metric]),
  385. sortUseClusterSize && (({ clusters }, i) => clusters[i].size),
  386. sortUseInvClusterSize && (({ clusters }, i) => clusters[i].inverseSize),
  387. sortUseTotalSize && (({ total }) => total.size),
  388. sortUseInvTotalSize && (({ total }) => total.inverseSize)
  389. );
  390. Object.entries(metricScores).forEach(([name, { jab, rgb }]) => {
  391. objectiveValues[name] = {
  392. jab: getSortScore(jab, bestClusterIndices[name].jab),
  393. rgb: getSortScore(rgb, bestClusterIndices[name].rgb),
  394. };
  395. });
  396. renderNameSearchResults();
  397. this.rank();
  398. }
  399. rank() {
  400. const { colorSpace, sortOrder } = Object.fromEntries(
  401. new FormData(document.forms.colorSortForm).entries()
  402. );
  403. const compare = sortOrders[sortOrder];
  404. const sortFn = (a, b) =>
  405. compare(objectiveValues[a][colorSpace], objectiveValues[b][colorSpace]);
  406. this.ranked = pokemonData
  407. .map(({ name }) => name)
  408. .sort((a, b) => sortFn(a, b) || a.localeCompare(b));
  409. this.renderColorSearchResults();
  410. }
  411. renderColorSearchResults() {
  412. renderPokemon(
  413. this.ranked.slice(
  414. 0,
  415. parseInt(document.forms.colorDisplayForm.elements.resultsToDisplay.value)
  416. ),
  417. colorSearchResultsTarget
  418. );
  419. }
  420. })();
  421. // ---- Form Controls ----
  422. document.forms.targetColorForm.elements.colorText.addEventListener(
  423. "input",
  424. ({ target }) => {
  425. if (target.willValidate && !target.validity.valid) {
  426. target.value = target.dataset.lastValid || "";
  427. } else {
  428. model.setTargetColor(target.value);
  429. }
  430. }
  431. );
  432. document.forms.targetColorForm.elements.colorPicker.addEventListener(
  433. "change",
  434. ({ target }) => model.setTargetColor(target.value)
  435. );
  436. const randomizeTargetColor = () =>
  437. model.setTargetColor(
  438. d3.hsl(Math.random() * 360, Math.random(), Math.random()).formatHex()
  439. );
  440. document.forms.targetColorForm.elements.randomColor.addEventListener(
  441. "click",
  442. randomizeTargetColor
  443. );
  444. document.forms.colorDisplayForm.elements.resultsToDisplay.addEventListener(
  445. "input",
  446. ({ target: { value } }) => {
  447. document.forms.colorDisplayForm.elements.output.value = value;
  448. }
  449. );
  450. document.forms.colorDisplayForm.elements.resultsToDisplay.addEventListener("change", () =>
  451. model.renderColorSearchResults()
  452. );
  453. Array.from(document.forms.colorSortForm.elements).forEach(el =>
  454. el.addEventListener("change", () => model.rank())
  455. );
  456. const clusterRankingTitle = document.getElementById("cls-title");
  457. const clusterMetricSection = document.getElementById("cls-metric-mount");
  458. const clusterFunctionSection = document.getElementById("cls-fn");
  459. Array.from(document.forms.colorCalculateForm.elements).forEach(el =>
  460. el.addEventListener("change", () => {
  461. const { sortUseBestCluster, sortUseClusterSize, sortUseInvClusterSize } =
  462. Object.fromEntries(new FormData(document.forms.colorCalculateForm).entries());
  463. clusterRankingTitle.dataset.faded =
  464. clusterMetricSection.dataset.faded =
  465. clusterFunctionSection.dataset.faded =
  466. !(sortUseBestCluster || sortUseClusterSize || sortUseInvClusterSize);
  467. model.calculateObjective();
  468. })
  469. );
  470. // ---- Add Metric Selections ----
  471. const metricSelectTemplate = document.getElementById("metric-select-template").content;
  472. const sortMetricForm = metricSelectTemplate.cloneNode(true).firstElementChild;
  473. sortMetricForm.id = "sortMetricForm";
  474. const clusterMetricForm = metricSelectTemplate.cloneNode(true).firstElementChild;
  475. clusterMetricForm.id = "clusterMetricForm";
  476. document.getElementById("sort-metric-mount").append(sortMetricForm);
  477. document.getElementById("cls-metric-mount").append(clusterMetricForm);
  478. document.forms.sortMetricForm.elements.metricKind.value = "whole";
  479. document.forms.clusterMetricForm.elements.metricKind.value = "stat";
  480. const updateMetricSelects = form => {
  481. const kind = form.elements.metricKind.value;
  482. form.elements.whole.disabled = kind !== "whole";
  483. form.elements.mean.disabled = kind !== "mean";
  484. form.elements.stat.disabled = kind !== "stat";
  485. form.elements.metric.value = form.elements[kind].value;
  486. };
  487. const getMetricSymbol = metricName =>
  488. // terrible hack
  489. document.querySelector(`option[value=${metricName}]`).textContent.at(-2);
  490. const onMetricChange = () => {
  491. updateMetricSelects(document.forms.sortMetricForm);
  492. updateMetricSelects(document.forms.clusterMetricForm);
  493. document.forms.colorCalculateForm.elements.sortMetricSymbolP.value =
  494. document.forms.colorCalculateForm.elements.sortMetricSymbolB.value = getMetricSymbol(
  495. document.forms.sortMetricForm.elements[
  496. document.forms.sortMetricForm.elements.metricKind.value
  497. ].value
  498. );
  499. document.forms.colorCalculateForm.elements.clusterMetricSymbol.value = getMetricSymbol(
  500. document.forms.clusterMetricForm.elements[
  501. document.forms.clusterMetricForm.elements.metricKind.value
  502. ].value
  503. );
  504. };
  505. onMetricChange();
  506. document.forms.sortMetricForm.addEventListener("change", () => {
  507. onMetricChange();
  508. model.calculateObjective();
  509. });
  510. document.forms.clusterMetricForm.addEventListener("change", () => {
  511. onMetricChange();
  512. model.calculateObjective();
  513. });
  514. // ---- Pick Starting Color ----
  515. randomizeTargetColor();