Skip to main content

Add a function to choose a colour for an ePub file

ID
8294391
date
2025-01-25 18:41:35+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
b53d323
message
Add a function to choose a colour for an ePub file
changed files
3 files, 116 additions, 9 deletions

Changed files

index.html (3061 → 3148)

diff --git a/index.html b/index.html
index 9e46001..8683a30 100644
--- a/index.html
+++ b/index.html
@@ -91,7 +91,9 @@
               const ficInfo = getFicInfo(contentOpfContent);
               console.debug(`Got fic info: ${JSON.stringify(ficInfo)}`);
 
-              reportSuccess(`Found fic info: ${JSON.stringify(ficInfo)}`)
+              reportSuccess(`
+                <span style="color: ${chooseColour(ficInfo.fandom)}">
+                Found fic info: ${JSON.stringify(ficInfo)}`)
             })
             .catch(function(error) {
               reportError(`Unable to read ePub file: ${error}`);

static/app.js (2208 → 4489)

diff --git a/static/app.js b/static/app.js
index adc581a..b180dac 100644
--- a/static/app.js
+++ b/static/app.js
@@ -1,7 +1,9 @@
-/* The `content.opf` file contains XML metadata about an ePub file.
+/**
+ * FInd the path to `content.opf` inside a zip file.
  *
- * Look inside an ePub file unpacked by JSZip, and return the path
- * to the `content.opf` file it contains (if any).
+ * The `content.opf` file contains XML metadata about an ePub file.
+ * It's normally stored at the root of the ePub, but find it in case
+ * it's been stuffed inside a subdirectory.
  *
  * This returns the path, or `null` if there's no such file in
  * the ePub.
@@ -18,8 +20,10 @@ function findContentOpfPath(zip) {
 
 
 
-/* Given the contents of the `content.opf` file, return certain key
- * information like the title, author, and fandom. */
+/**
+ * Given the contents of the `content.opf` file, return certain key
+ * information like the title, author, and fandom.
+ */
 function getFicInfo(xml) {
   const parser = new DOMParser();
   const xmlDoc = parser.parseFromString(xml, "text/xml");
@@ -77,6 +81,93 @@ function getFicInfo(xml) {
 
 
 
+/**
+ * Choose a colour to represent the fics in this fandom.
+ *
+ * This is a random dark shade; the only important thing is that we
+ * can call it reproducibly to get the same colour if this is called
+ * the same time for the same fandom.
+ *
+ *
+ */
+function chooseColour(fandom) {
+  
+  // This is a simple hashing function which gets us a random-ish
+  // hue in the random 0–359.
+  let hash = 0;
+  for (let i = 0; i < fandom.length; i++) {
+    const char = fandom.charCodeAt(i);
+    hash = ((hash << 5) - hash) + char;
+    hash = hash & hash;
+  }
+  
+  const hue = (Math.abs(hash) % 360) / 360;
+  
+  // The saturation/lightness values are chosen to get a dark-ish
+  // shade that will look good with white text.
+  const saturation = 1.0;
+  const lightness = 0.25;
+  
+  // Convert to rgb.
+  let [red, green, blue] = hslToRgb(hue, saturation, lightness);
+  
+  // Convert to a hex string.
+  return `#${numToHex(red)}${numToHex(green)}${numToHex(blue)}`;
+}
+
+
+
+/**
+ * Converts an HSL color value to RGB. Conversion formula
+ * adapted from https://en.wikipedia.org/wiki/HSL_color_space.
+ * Assumes h, s, and l are contained in the set [0, 1] and
+ * returns r, g, and b in the set [0, 255].
+ *
+ * @param   {number}  h       The hue
+ * @param   {number}  s       The saturation
+ * @param   {number}  l       The lightness
+ * @return  {Array}           The RGB representation
+ *
+ * This function is by Gary Tan and is taken from Stack Overflow:
+ * https://stackoverflow.com/a/9493060/1558022
+ */
+function hslToRgb(h, s, l) {
+  let r, g, b;
+
+  if (s === 0) {
+    r = g = b = l; // achromatic
+  } else {
+    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+    const p = 2 * l - q;
+    r = hueToRgb(p, q, h + 1/3);
+    g = hueToRgb(p, q, h);
+    b = hueToRgb(p, q, h - 1/3);
+  }
+
+  return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
+}
+
+function hueToRgb(p, q, t) {
+  if (t < 0) t += 1;
+  if (t > 1) t -= 1;
+  if (t < 1/6) return p + (q - p) * 6 * t;
+  if (t < 1/2) return q;
+  if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
+  return p;
+}
+
+
+
+/**
+ * Convert a number to a 2-digit hex string.
+ */
+function numToHex(n) {
+  const hex = n.toString(16);
+  return hex.length === 1 ? '0' + hex : hex;
+}
+
+
+
 if (typeof module !== 'undefined') {
-  module.exports = { findContentOpfPath, getFicInfo };
+  module.exports = { chooseColour, findContentOpfPath, getFicInfo };
 }

test/app.js (3885 → 4108)

diff --git a/test/app.js b/test/app.js
index 429fd89..bbc5684 100644
--- a/test/app.js
+++ b/test/app.js
@@ -1,4 +1,8 @@
-const { findContentOpfPath, getFicInfo } = require('../static/app.js')
+const {
+  chooseColour,
+  findContentOpfPath,
+  getFicInfo,
+} = require('../static/app.js')
 const JSZip = require('../static/jszip.min.js');
 
 const fs = require("fs");
@@ -120,4 +124,14 @@ QUnit.test('it finds the key info from a `content.opf file`', (assert) => {
         fandom: 'Operation Mincemeat: A New Musical - SpitLip',
       }
     );
-});
\ No newline at end of file
+});
+
+
+
+
+
+QUnit.module('chooseColour');
+
+QUnit.test('it chooses hte colour in a reproducible way', (assert) => {
+  assert.equal(chooseColour('Operation Mincemeat: A New Musical - SpitLip'), '#800600');
+});