<title>Talia: A colour palette</title> <h1>Talia: A colour palette</h1> <section class="important info box"> A general-purpose colour palette to use for whatever. </section> <label> <span>Background Colour:</span> <input value="#f2f2f3" type="color" id="background-color"></input> </label> <!-- TODO: Change the base hue and offset here --> <section style="--base-hue: 280deg; --saturation: 40%; --offset: calc(360deg / 6)"> <h2>Primary</h2> <colour-palette name="primary" style="--hue: calc(var(--base-hue) - var(--offset));"></colour-palette> <section class="info box"> The main colour of the scheme. Should be used for the majority of the content. </section> <h2>Secondary</h2> <colour-palette name="secondary" style="--hue: var(--base-hue); --saturation: 30%;"></colour-palette> <section class="info box"> A secondary colour to provide some contrast to the primary colour without drawing too much attention. </section> <h2>Contrast</h2> <colour-palette name="contrast" style="--hue: calc(var(--base-hue) + var(--offset)); --saturation: 50%;"></colour-palette> <section class="info box"> This colour is intended to provide a lot more contrast to the primary colour for elements that require user attention. </section> <h2>Neutral</h2> <colour-palette name="neutral" style="--hue: 210deg; --saturation: 5%;"></colour-palette> <section class="info box"> Just a more neutral scale of gray tones </section> </section> <button id="export-gimp">Export as GIMP Palette</button> <button id="export-css">Export as CSS Variables</button> <!-- ======================================== --> <!-- ===== STYLES AND APPLICATION LOGIC ===== --> <!-- ======================================== --> <style> :root { font-size: 1em; color: hsl(220deg, 8%, 20%); font-family: "Open Sans", sans-serif; } body.dark { color: hsl(220deg, 8%, 80%); } * { box-sizing: border-box; } code { font-family: "Hack", monospace; } body { display: flex; flex-flow: column nowrap; align-items: center; background: hsl(200deg, 5%, 95%); } /* The following only does font size shenanigans! */ h1, h2 { text-align: center; font-weight: normal; font-family: "Raleway", "Quicksand", serif; } h1 { font-size: 2.4em; --border: dotted .08em currentcolor; border-bottom: var(--border); border-top: var(---border); width: calc((3rem + .6rem * 2) * 9); text-align: center; padding: .2em 0; } h2 { font-size: 2em; } colour-palette { --separation2: .6em; /* Halved separation */ display: flex; /* padding: var(--separation2); */ } colour-palette button { --radius: 50%; all: unset; margin: var(--separation2); background: hsl(var(--hue), var(--saturation, 50%), var(--lightness, 50%)); cursor: pointer; border-radius: var(--radius); overflow: hidden; transition: box-shadow 0.3s; } colour-palette button::before { content: ''; border-radius: var(--radius); background: hsl(var(--hue), var(--saturation, 50%), var(--lightness, 50%)); box-shadow: .1em .1em .8em hsla(calc(var(--hue) + 30deg), calc(100% - var(--lightness) - 10%), calc(var(--lightness) - 40%), .4) inset; width: 3em; height: 3em; display: block; transition: box-shadow 0.3s; } colour-palette button:hover { box-shadow: .4em .4em .6em #0004; } colour-palette button:hover::before { box-shadow: none; } body.dark colour-palette button:hover { box-shadow: .0em .0em .6em hsla(var(--hue), calc(var(--saturation) + 30%), 80%, 60%); } /* === Popup notification shenanigans === */ div.notification { top: 0em; border: 1px solid currentcolor; background: hsla(0deg, 0%, 100%, .8); font-size: 1em; opacity: 0; position: fixed; padding: .4em; 1em; font-weight: bold; text-align: center; } /* === Info Boxes === */ .box { --radius: .3em; width: calc((3rem + .6rem * 2) * 9); font-style: italic; border-radius: var(--radius); background: hsla(200deg, 5%, 92%, 1); box-shadow: .0em .1em .2em inset hsl(220deg, 8%, 80%); padding: 1em 2em; position: relative; margin: 2em 0em; } body.dark .box { background: hsla(200deg, 5%, 6%, 1); box-shadow: .0em .1em .2em inset #0006; } .hint, .info { --color: hsl(220deg, 40%, 50%) } .important { --color: hsl(280deg, 30%, 50%) } .box::after { display: block; position: absolute; left: 0; top: 0; height: 100%; width: .4em; background-color: var(--color, transparent); border-radius: var(--radius) 0 0 var(--radius); content: ''; } .box.hint::before { content: "Hint: "; display: inline-block; font-weight: bold; } button { all: unset; font-size: 1.2em; cursor: pointer; padding: .4em 1.2em; border-radius: 1.8em; font-weight: bold; background-color: hsl(340deg, 50%, 40%); color: hsl(340deg, 50%, 95%); margin: .4em; } /* === Utility === */ [center] { text-align: center; } </style> <script type="module"> const showMessage = function(message, color, x, y) { let box = document.createElement('div'); box.innerHTML = message; box.classList.add('notification'); if (x) box.style.left = x+"px"; if (y) box.style.top = y+"px"; document.querySelector("body").appendChild(box); box.animate([ { offset: 0, opacity: 1, transform: "translate(-50%, -10%)"}, { offset: .3, opacity: 1 }, { offset: 1, opacity: 0, transform: "translate(-50%, -200%)"}, ], { duration: 1e3 }).finished.then(() => box.remove()) box.style.setProperty('color', color); } const register = (type, name, initial=undefined) => CSS.registerProperty({ name: `--${name}`, syntax: `<${type}>`, inherits: true, initialValue: initial, }) register("angle", "hue", "0deg") register("percentage", "saturation", "100%") register("percentage", "lightness", "50%") const backgroundColourButton = document.getElementById("background-color") backgroundColourButton.addEventListener("input", event => { const rgb = backgroundColourButton.value document.body.style.background = rgb const channels = rgb .match(/[0-9a-f][0-9a-f]/g) .map(hex => parseInt(hex, 16)) .map(num => num / 255) const luminosity = channels.reduce((a,b)=>a+b) / 3 if (luminosity < 0.4) { document.body.classList.add("dark") } else { document.body.classList.remove("dark") } }) customElements.define("colour-palette", class extends HTMLElement { get colours() { return Array.from(this.querySelectorAll("button")) .map(button => getComputedStyle(button).backgroundColor) .map(colour => colour.match(/\d+/g)) .map((match, index) => [match[0], match[1], match[2], `${this.name}-${index+1}`]) } connectedCallback() { this.innerHTML = ` <button style="--lightness: 05%"></button> <button style="--lightness: 15%"></button> <button style="--lightness: 30%"></button> <button style="--lightness: 40%"></button> <button style="--lightness: 50%"></button> <button style="--lightness: 60%"></button> <button style="--lightness: 70%"></button> <button style="--lightness: 80%"></button> <button style="--lightness: 95%"></button> ` this.querySelectorAll('button').forEach(button => { button.addEventListener('click', event => { const prop = name => getComputedStyle(button) .getPropertyValue(name) .replace(/^ *| *$/g, "") const colour = `hsl(${prop("--hue")}, ${prop("--saturation")}, ${prop("--lightness")})` navigator.clipboard.writeText(colour).then(() => { const box = button.getBoundingClientRect() showMessage(`Copied to Clipboard:<br>${colour}`, colour, box.x+box.width/2, box.y+box.height/2); }); }); }); } get name() { return this.getAttribute("name") || "Unnamed" } }) const exportGimpButton = document.querySelector("#export-gimp") exportGimpButton.addEventListener("click", event => { const colours = Array.from(document.querySelectorAll("colour-palette")).map(palette => palette.colours) const template = `GIMP Palette\nName: Palette\nColumns: ${colours.length}\n#\n${colours.map(c => c.map(c=>c.join(' ')).join("\n")).join("\n#\n")}` const link = document.createElement("a") link.href = `data:text/plain;base64,${btoa(template)}` link.download = "palette.gpl" link.click() }) const hexByte = byte => byte <= 16 ? `0${byte.toString(16)}` : byte.toString(16) const exportCSSButton = document.querySelector("#export-css") exportCSSButton.addEventListener("click", event => { const colours = Array.from(document.querySelectorAll("colour-palette")) .map(palette => palette.colours.map(colour => `--${colour[3]}: #${colour.slice(0,3).map(Number).map(hexByte).join('')};`)) const template = `/*Talia Palette*/\n:root {\n${colours.map(p => p.map(c => "\t" + c).join("\n")).join("\n\n")}\n}` console.log(template) const link = document.createElement("a") link.href = `data:text/plain;base64,${btoa(template)}` link.download = "palette.css" link.click() }) </script>