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');
+});