<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

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdn.jsdelivr.net/npm/tweakpane@3.0.3/dist/tweakpane.min.js