<article>
<!-- <h1>FettePalette</h1> -->
<h1>Palette Slicer</h1>
<!-- <div><a href="https://meodai.github.io/fettepalette/" target="_blank">full version</a></div> -->
<aside>
<h2>HSL Slice Preview</h2>
<figure><svg data-figure viewbox="0 0 100 100"></svg></figure>
</aside>
<div class="row" style="margin-top:5px">
<!-- col 1 -->
<div class="column">
<aside style="width:100%">
<h2>Full Palette</h2>
<div data-colors></div>
</aside>
<aside style="margin-top:1rem">
<h2><strong>4 random colors sampled form palette</strong> (click to re-generate) </h2>
<div data-palette>
<b><i></i><i></i></b>
</div>
<h2 style="margin:0;padding:0;position:relative;margin-top:1rem">Gradient Ramp</h2>
<div data-ramp></div>
</aside>
</div>
<!-- col 2 -->
<div class="column">
<aside data-list>
</aside>
</div>
</div>
</article>
@import url("https://rsms.me/inter/inter.css");
:root {
font-family: "Inter", sans-serif;
}
article {
padding: 0 2rem 1rem;
background: #fff;
color: #202124;
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
overflow: auto;
display: flex;
flex-direction: column;
> aside {
margin-top: 2rem;
max-width: 24rem;
}
}
h1 {
font-size: 2rem;
font-weight: 400;
margin-top: .5em;
margin-bottom:-10px;
}
h2 {
margin: 0 0 .5rem;
font-weight: 200;
&:first-child {
margin-top: 0;
}
}
[data-colors] {
display: flex;
flex-wrap: wrap;
width: 100%;
}
[data-colors] i {
flex: 1 0 calc(var(--w, 0.11) * 100%);
width: calc(var(--w, 0.11) * 100%);
padding-top: calc(var(--w, 0.11) * 100%);
background: hsl(var(--h), calc(var(--s) * 1%), calc(var(--l) * 1%));
}
[data-palette] {
position: relative;
background: var(--col-0);
padding-top: 100%;
margin: 0;
cursor: pointer;
b {
position: absolute;
top: 50%;
left: 50%;
width: 50%;
height: 50%;
transform: translate(-50%, -50%);
background: var(--col-1);
}
i {
position: absolute;
width: 50%;
height: 50%;
right: 0;
}
i:first-child {
background: var(--col-2);
}
i:last-child {
bottom: 0;
background: var(--col-3);
}
}
figure {
margin: 0;
padding: 0;
}
[data-figure] {
background-image: linear-gradient(to top, rgba(0, 0, 0, 1), rgba(0, 0, 0, 0)),
linear-gradient(
to left,
hsl(var(--deg, 0deg), 100%, 50%),
hsl(var(--deg, 0deg), 0%, 100%)
);
}
[data-ramp] {
margin-top: .25rem;
height: 4rem;
}
[data-list] {
margin: 2rem 0.5rem;
width: 100%;
h3 {
margin-bottom: 0.5rem;
margin-top: 2rem;
display: none;
}
li {
&::before {
user-slectable: none;
content: "⬤";
color: var(--col);
}
font-family: monospace;
font-size: 0.8rem;
margin-top: 0.5em;
margin-left:1em;
width:10em
}
display: flex;
> div {
flex: 0 0 33.333%;
}
}
.tp-dfwv {
// top: 8px;
right: 30px !important;
// width: 256px!important;
}
.row {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-direction: row;
flex-wrap: wrap;
align-content: flex-start;
}
.column {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
flex-wrap: wrap;
}
View Compiled
import * as culori from "https://cdn.skypack.dev/culori@0.18.2";
// https://medium.com/@greggunn/how-to-make-your-own-color-palettes-712959fbf021
console.clear();
const hsv2hsl = (h,s,v,l=v-v*s/2, m=Math.min(l,1-l)) => [h,m?(v-l)/m:0,l];
const random = (min, max) => {
if (!max) {
max = min;
min = 0;
}
return Math.random() * (max - min) + min
};
const shuffleArray = array => {
let arr = [...array];
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
const pointOnCurve = (
curveMethod,
i,
total,
curveAccent,
min = [0,0],
max = [1,1],
) => {
const limit = Math.PI/2;
const slice = limit / total;
let x,y;
if (curveMethod === 'lamé') {
let t = i / total * limit;
const exp = 2 / (2 + (20 * curveAccent));
const cosT = Math.cos(t);
const sinT = Math.sin(t);
x = Math.sign(cosT) * ( Math.abs(cosT) ** exp );
y = Math.sign(sinT) * ( Math.abs(sinT) ** exp );
} else if (curveMethod === 'arc') { // pow
y = Math.cos(-Math.PI/2 + i * slice + curveAccent);
x = Math.sin(Math.PI/2 + i * slice - curveAccent);
} else if (curveMethod === 'pow') {
x = Math.pow(1 - i/total, 1 - curveAccent);
y = Math.pow(i/total, 1 - curveAccent);
} else if (curveMethod === 'powY') {
x = Math.pow(1 - i/total, curveAccent);
y = Math.pow(i/total, 1 - curveAccent);
} else if (curveMethod === 'powX') {
x = Math.pow(i/total, curveAccent);
y = Math.pow(i/total, 1 - curveAccent);
}
x = min[0] + Math.min(Math.max(x, 0), 1) * (max[0] - min[0]);
y = min[1] + Math.min(Math.max(y, 0), 1) * (max[1] - min[1]);
return [x, y];
}
const generateRandomColorRamp = (
total,
centerHue = random(360),
hueCycle = 0.3,
offsetTint = 0.1,
offsetShade = 0.1,
curveAccent = 0,
tintShadeHueShift = 0.1,
curveMethod = 'arc', // arc || lamé: https://observablehq.com/@daformat/draw-squircle-shapes-with-svg-javascript
offsetCurveModTint = 0.03,
offsetCurveModShade = 0.03,
minSaturationLight = [0, 0],
maxSaturationLight = [1, 1]
) => {
const baseColors = [];
const lightColors = [];
const darkColors = [];
for (let i = 1; i < (total + 1); i++) {
let [x, y] = pointOnCurve(curveMethod, i, total + 1, curveAccent, minSaturationLight, maxSaturationLight);
let h = (360 + ((-180 * hueCycle) + (centerHue + i * (360/(total + 1)) * hueCycle))) % 360;
let hsl = hsv2hsl(
h, x, y
)
baseColors.push(
[
hsl[0],
hsl[1],
hsl[2]
]
);
let [xl, yl] = pointOnCurve(curveMethod, i, total + 1, curveAccent + offsetCurveModTint, minSaturationLight, maxSaturationLight);
let hslLight = hsv2hsl(
h, xl, yl
);
lightColors.push(
[
(h + 360 * tintShadeHueShift) % 360,
hslLight[1] - offsetTint,
hslLight[2] + offsetTint
]
);
let [xd, yd] = pointOnCurve(curveMethod, i, total + 1, curveAccent - offsetCurveModShade, minSaturationLight, maxSaturationLight);
let hslDark = hsv2hsl(
h, xd, yd
);
darkColors.push(
[
(360 + (h - 360 * tintShadeHueShift)) % 360,
hslDark[1] - offsetShade,
hslDark[2] - offsetShade
]
);
}
return {
light: lightColors,
dark: darkColors,
base: baseColors,
all: [...lightColors,...baseColors,...darkColors],
}
}
const pane = new Tweakpane.Pane({
title: 'Palette Slicer Options',
expanded: true,
});
const colorModes = [
{
name: 'hsl',
components: [
{
name: 'h',
min: 0,
max: 360,
},
{
name: 's',
min: 0,
max: 1,
},
{
name: 'l',
min: 0,
max: 1,
}
]
},
{
name: 'hwb',
components: [
{
name: 'h',
min: 0,
max: 360,
},
{
name: 'w',
min: 0,
max: 1,
},
{
name: 'b',
min: 0,
max: 1,
}
]
},
{
name: 'lch',
components: [
{
name: 'l',
min: 0,
max: 100,
},
{
name: 'c',
min: 0,
max: 131.008,
},
{
name: 'h',
min: 0,
max: 360,
}
]
},
{
name: 'OKlch',
components: [
{
name: 'l',
min: 0,
max: 1,
},
{
name: 'c',
min: 0,
max: 0.322,
},
{
name: 'h',
min: 0,
max: 360,
}
]
}]
const PARAMS = {
colors: 9,
centerHue: 0,
hueCycle: 0.3,
offsetTint: 0.01,
offsetShade: 0.01,
curveAccent: 0,
tintShadeHueShift: 0.01,
colorMode: 'hsl',
curveMethod: 'lamé', //arc, pow
offsetCurveModTint: 0.03,
offsetCurveModShade: 0.03,
minSaturation: 0,
minLight: 0,
maxSaturation: 1,
maxLight: 1,
};
// `min` and `max`: slider
pane.addInput(
PARAMS, 'colors',
{min: 3, max: 35, step: 1}
);
pane.addInput(
PARAMS, 'centerHue',
{min: 0, max: 360, step: 0.1}
);
pane.addInput(
PARAMS, 'hueCycle',
{min: 0, max: 1.5, step: 0.001}
);
pane.addInput(PARAMS, 'curveMethod', {
options: {
lamé: 'lamé',
arc: 'arc',
pow: 'pow',
powY: 'powY',
powX: 'powX',
},
});
pane.addInput(
PARAMS, 'curveAccent',
{min: -0.095, max: 1, step: 0.001}
);
pane.addInput(
PARAMS, 'offsetTint',
{min: 0, max: 0.4, step: 0.001}
);
pane.addInput(
PARAMS, 'offsetShade',
{min: 0, max: 0.4, step: 0.001}
);
pane.addInput(
PARAMS, 'offsetCurveModTint',
{min: 0, max: 0.4, step: 0.0001}
);
pane.addInput(
PARAMS, 'offsetCurveModShade',
{min: 0, max: 0.4, step: 0.0001}
);
pane.addInput(
PARAMS, 'tintShadeHueShift',
{min: 0, max: 1, step: 0.001}
);
pane.addInput(
PARAMS, 'minSaturation',
{min: 0, max: 1, step: 0.001}
);
pane.addInput(
PARAMS, 'minLight',
{min: 0, max: 1, step: 0.001}
);
pane.addInput(
PARAMS, 'maxSaturation',
{min: 0, max: 1, step: 0.001}
);
pane.addInput(
PARAMS, 'maxLight',
{min: 0, max: 1, step: 0.001}
);
//colorModes
let options = {};
colorModes.forEach(mode => {
options[mode.name] = mode.name;
});
/*
pane.addInput(
PARAMS, 'colorMode',
{options}
);*/
const $pal = document.querySelector('[data-palette]');
const $picker = document.querySelector('[data-figure]');
const $ramp = document.querySelector('[data-ramp]');
let colors = [];
const palette = (colors, method) => {
let allColors = [];
const lightColors = shuffleArray( colors.light );
const mediumColors = shuffleArray( colors.base );
const darkColors = shuffleArray( colors.dark );
switch (method) {
case 'random':
allColors = shuffleArray(colors.all);
break;
case 'l2md':
allColors = [lightColors[0], mediumColors[0], darkColors[0], lightColors[1]];
break;
case 'lmd2':
allColors = [darkColors[0], mediumColors[0], lightColors[0], darkColors[1]];
break;
case 'lm2d':
allColors = [mediumColors[1], mediumColors[0], lightColors[0], darkColors[1]];
break;
}
for(let i = 0; i < 4; i++) {
$pal.style.setProperty(`--col-${i}`, `hsl(${allColors[i][0]},${allColors[i][1] * 100}%,${allColors[i][2] * 100}%)`);
}
$ramp.style.setProperty('background', `linear-gradient(90deg, ${allColors.slice(0,4).sort((f,s) => s[2] - f[2]).map(c => `hsl(${c[0]},${c[1] * 100}%,${c[2] * 100}%)`).join(',')})`)
}
function bam() {
colors = generateRandomColorRamp(
PARAMS.colors,
PARAMS.centerHue,
PARAMS.hueCycle,
PARAMS.offsetTint,
PARAMS.offsetShade,
PARAMS.curveAccent,
PARAMS.tintShadeHueShift,
PARAMS.curveMethod,
PARAMS.offsetCurveModTint,
PARAMS.offsetCurveModShade,
[PARAMS.minSaturation,PARAMS.minLight],
[PARAMS.maxSaturation,PARAMS.maxLight]
);
points(
PARAMS.colors,
PARAMS.offsetTint,
PARAMS.offsetShade,
PARAMS.curveAccent,
colors.base,
PARAMS.curveMethod,
PARAMS.offsetCurveModTint,
PARAMS.offsetCurveModShade,
[PARAMS.minSaturation,PARAMS.minLight],
[PARAMS.maxSaturation,PARAMS.maxLight]
);
/*
let shuffledcolors = colors
.map((a) => ({sort: Math.random(), value: a}))
.sort((a, b) => a.sort - b.sort)
.map((a) => a.value)*/
$picker.style.setProperty(`--deg`, `${colors.all[Math.floor(colors.all.length * .5)][0]}deg`);
/*
const mode = colorModes.find(mode => mode.name === PARAMS.colorMode);
const format = {
mode: mode.name,
}
mode.components.forEach(comp => {
format[comp.name] =
})
console.log()
*/
/*culori.formatHex({
mode: PARAMS.colorMode,
})*/
document.querySelector('[data-colors]').innerHTML = colors.all.reduce((r,c) => {
return `${r}<i style="--w: ${1/PARAMS.colors}; --h: ${c[0]}; --s: ${c[1] * 100}; --l: ${c[2] * 100}"></i>`
},'');
palette(colors, 'random');
list(colors)
}
function points (colorsInt, offsetTint, offsetShade, curveAccent, colorsArr, curveMethod, offsetCurveModTint, offsetCurveModShade, minSaturationLight, maxSaturationLight) {
$picker.innerHTML = '';
const limit = Math.PI/2;
const part = limit / (colorsInt + 1);
for (let i = 1; i < (colorsInt + 1); i++) {
let [x,y] = pointOnCurve(curveMethod,i,colorsInt + 1,curveAccent,minSaturationLight,maxSaturationLight);
let hsl = hsv2hsl(0,x,y);
let newElement = document.createElementNS("http://www.w3.org/2000/svg", 'circle');
newElement.setAttribute('cx', x * 100);
newElement.setAttribute('cy', 100 - y * 100);
newElement.setAttribute('r','3');
newElement.style.fill = `hsl(${colorsArr[i-1][0]}deg, ${hsl[1] * 100}%,${hsl[2] * 100}%)`;
newElement.style.stroke = '#212121';
newElement.style.strokeWidth = '.5px';
let [xl,yl] = pointOnCurve(curveMethod,i,colorsInt + 1,curveAccent + offsetCurveModTint,minSaturationLight,maxSaturationLight);
let newElementLight = document.createElementNS("http://www.w3.org/2000/svg", 'circle');
newElementLight.setAttribute('cx', (xl - offsetTint) * 100);
newElementLight.setAttribute('cy', 100 - (yl + offsetTint) * 100);
newElementLight.setAttribute('r','1');
newElementLight.style.fill = `#fff`;
newElementLight.style.strokeWidth = '0px';
let newElementDark = document.createElementNS("http://www.w3.org/2000/svg", 'circle');
let [xd ,yd] = pointOnCurve(curveMethod,i,colorsInt + 1,curveAccent - offsetCurveModShade,minSaturationLight,maxSaturationLight);
newElementDark.setAttribute('cx', (xd - offsetShade) * 100);
newElementDark.setAttribute('cy', 100 - (yd - offsetShade) * 100);
newElementDark.setAttribute('r','1');
newElementDark.style.fill = `#000`;
newElementDark.style.strokeWidth = '0px';
/*
const lightColors = baseColors.map(c => [(c[0] + 360 * tintShadeHueShift) % 360, c[1] - offset, c[2] + offset])
const darkColors = baseColors.map(c => [(c[0] - 360 * tintShadeHueShift) % 360, c[1] - offset, c[2] - offset])
*/
$picker.appendChild(newElement);
$picker.appendChild(newElementLight);
$picker.appendChild(newElementDark);
}
}
function list (colors) {
document.querySelector('[data-list]').innerHTML = `
<div>
<h3>Light Colors</h3>
<ol>
${colors.light.map(e => {
const hex = culori.formatHex({ mode: 'hsl', h: e[0], s: e[1], l: e[2]});
return `<li style="--col:${hex}">
${Math.floor(e[0])}° ${Math.floor(e[1] * 100)}% ${Math.floor(e[2] * 100)}%
</li>`}).join('')}
</ol>
</div>
<div>
<h3>Base Colors</h3>
<ol>
${colors.base.map(e => {
const hex = culori.formatHex({ mode: 'hsl', h: e[0], s: e[1], l: e[2]});
return `<li style="--col:${hex}">
${Math.floor(e[0])}° ${Math.floor(e[1] * 100)}% ${Math.floor(e[2] * 100)}%
</li>`}).join('')}
</ol>
</div>
<div>
<h3>Dark Colors</h3>
<ol>
${colors.dark.map(e => {
const hex = culori.formatHex({ mode: 'hsl', h: e[0], s: e[1], l: e[2]});
return `<li style="--col:${hex}">
${Math.floor(e[0])}° ${Math.floor(e[1] * 100)}% ${Math.floor(e[2] * 100)}%
</li>`}).join('')}
</ol>
</div>
`
}
$pal.addEventListener('click', () => palette(colors, 'random'));
pane.on('change', bam);
bam();
View Compiled
This Pen doesn't use any external CSS resources.