fettepalette
Version:
Color ramp generator using curves within the HSV color model
1,333 lines (1,200 loc) • 45.2 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- Primary Meta Tags -->
<title>FettePalette — Color ramp generator using curves within the HSV color model</title>
<meta name="title" content="FettePalette — Color ramp generator using curves within the HSV color model">
<meta name="description" content="FettePalette is lightweight, dependency free and fast JavaScript function written in TypeScript. It generates color ramps based on a curve within the HSV color model. This page serves as preview for the variety of options the function takes.">
<meta name="keywords" content="color, colour, generative, generative-art, generative-design, palette, colorpalette">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://meodai.github.io/fettepalette/">
<meta property="og:title" content="FettePalette — Color ramp generator using curves within the HSV color model">
<meta property="og:description" content="FettePalette is lightweight, dependency free and fast JavaScript function written in TypeScript. It generates color ramps based on a curve within the HSV color model. This page serves as preview for the variety of options the function takes.">
<meta property="og:image" content="https://meodai.github.io/fettepalette/socialfb.png">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://meodai.github.io/fettepalette/">
<meta property="twitter:title" content="FettePalette — Color ramp generator using curves within the HSV color model">
<meta property="twitter:description" content="FettePalette is lightweight, dependency free and fast JavaScript function written in TypeScript. It generates color ramps based on a curve within the HSV color model. This page serves as preview for the variety of options the function takes.FettePalette is lightweight, dependency free and fast JavaScript function written in TypeScript. It generates color ramps based on a curve within the HSV color model. This page serves as preview for the variety of options the function takes.">
<meta property="twitter:image" content="https://meodai.github.io/fettepalette/socialfb.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css"
/>
<style>
:root {
font-family: "Inter", sans-serif;
background: #202124;
--sidebarwidth: 20rem;
}
.settings {
position: fixed;
width: var(--sidebarwidth);
padding: 4.6rem 2rem;
background: #202124;
color: #fff;
order: 1;
left: 0;
top: 0;
bottom: 0;
overflow-y: auto;
box-sizing: border-box;
z-index: 2;
}
.projectlink {
display: inline-block;
color: #fff;
margin-top: .75em;
}
.projectlink::after {
content: '↗';
}
.main {
margin-left: var(--sidebarwidth);
background: #fff;
padding: 4rem;
}
.section {
display: flex;
margin-top: 4rem;
max-width: calc(50rem + 12.5vw);
}
.section__text {
flex: 1 0 calc(100% - 25rem - 4rem);
order: 1;
width: calc(100% - 25rem - 4rem);
}
.section__fig {
flex: 1 1 25rem;
order: 0;
margin-right: 4rem;
}
.section code {
font-family: monospace;
font-size: 1.3em;
background: #202124;
color: #fff;
padding: 0 0.75ex 0.2ex;
}
h1 {
font-size: calc(1rem + 11vw);
font-weight: 900;
letter-spacing: -0.055em;
margin-top: 2rem;
line-height: 0.85;
margin-left: -0.045em;
}
h2,
h3,
[data-names] strong {
margin: 0 0 0.75rem;
font-size: calc(1.5rem + 2vw);
font-weight: 800;
letter-spacing: -0.04em;
line-height: 0.9;
}
h2 + p {
margin-bottom: 1rem;
}
h3 {
margin-top: 1.5em;
font-size: calc(0.5rem + 1vw);
}
pre {
font-family: monospace;
font-size: 0.8rem;
max-width: 100%;
}
a {
font-size: 1em;
margin-top: 0.4em;
font-weight: 700;
}
button {
display: block;
background: none;
padding: 0;
border: none;
border-radius: 0;
font: inherit;
font-size: inherit;
color: inherit;
}
.main button {
cursor: pointer;
font-weight: bold;
display: inline;
text-decoration: underline;
text-decoration-style: dotted;
}
[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%));*/
background: var(--c);
}
[data-palette] {
display: flex;
flex-wrap: wrap;
}
[data-palette] .palette-sample {
outline: 4px solid #fff;
}
.palette-sample {
position: relative;
background: var(--col-0);
padding-top: calc(100% / var(--x));
margin: 0;
user-select: none;
flex: 0 0 calc(100% / var(--x));
}
.palette-sample b {
position: absolute;
top: 50%;
left: 50%;
width: 50%;
height: 50%;
transform: translate(-50%, -50%);
background: var(--col-1);
}
.palette-sample i {
position: absolute;
width: 50%;
height: 50%;
right: 0;
}
.palette-sample i:first-child {
background: var(--col-2);
}
.palette-sample i:last-child {
bottom: 0;
background: var(--col-3);
}
figure {
margin: 0;
padding: 0;
}
[data-figure] {
background-image: linear-gradient(to top, black, rgba(0, 0, 0, 0)),
linear-gradient(
to left,
var(--col1),
var(--col2)
);
}
[data-ramp] {
height: 10rem;
}
[data-list] {
margin: 0 0 2rem;
display: flex;
gap: 4px;
}
.swatch {
position: relative;
font-size: 0.8rem;
line-height: 1.2;
padding-bottom: 0.5em;
}
[data-list] h3 {
display: none;
margin-top: 0;
}
.swatch::before {
user-select: none;
content: "";
background: var(--col);
display: block;
width: 100%;
height: 1.2em;
margin-bottom: 0.25rem;
}
.swatch::after {
position: absolute;
display: block;
content: "";
top: 2px;
right: 2px;
width: 2px;
height: calc(1.2em - 4px);
background: var(--coltext);
}
.color-info {
padding: 0.5rem;
}
.color-info strong {
font-size: 1rem;
display: block;
margin-bottom: 1ex;
}
.color-info button {
font-size: 0.6rem;
margin-top: 0.5em;
}
.color-info__contrast {
position: absolute;
top: 8.7rem;
left: 0.5rem;
color: var(--colbg);
font-size: 0.8em;
}
.color-info__contrast h5 {
display: none;
}
[data-list] > div {
flex: 1;
}
[data-copy] {
cursor: pointer;
}
p {
margin-top: 1em;
max-width: 22rem;
font-size: 0.8em;
letter-spacing: 0.002em;
line-height: 1.38;
font-weight: 400;
}
.pane__section,
.pane__label {
display: block;
}
.pane__label {
font-size: 0.8rem;
font-weight: 400;
line-height: 1;
margin-bottom: 0.8em;
}
.pane {
box-sizing: border-box;
background: var(--color-bg);
display: block;
cursor: default;
background: #202124;
color: #fff;
--size-gutter: 1rem;
--color-inverted: #fff;
}
.pane__section {
display: block;
}
.pane__section + .pane__section {
margin-top: calc(var(--size-gutter) * 2);
}
.pane__inputs {
display: flex;
touch-action: manipulation;
}
.pane__input--number {
flex-grow: 1;
}
.pane .pane__input--number + input[type="number"] {
display: block;
flex-basis: 4rem;
width: 4rem;
}
.pane__desc {
margin: 1em 0 3em;
font-size: 0.6em;
}
.pane select {
font-size: 0.8em;
border-radius: 2rem;
padding: 0.2rem;
}
.pane input,
.pane select {
display: block;
box-sizing: border-box;
touch-action: manipulation;
font-family: "Space Mono", monospace;
border: none;
width: auto;
}
.pane select option {
color: black;
}
.pane input[type="number"],
.pane select[type="number"] {
color: var(--color-inverted);
background: none;
border: none;
text-align: right;
font-size: 0.8em;
flex: 0 0 3rem;
width: 3rem;
}
.pane input {
background-color: transparent;
}
.pane input[type="range"] {
-webkit-appearance: none;
}
.pane input[type="range"] {
margin: 0;
padding-top: 0.7em;
margin-top: -0.7em;
}
.pane input[type="range"]:focus {
outline: none;
}
.pane input[type="range"]:focus::-webkit-slider-thumb {
background-color: var(--color-inverted);
clip-path: polygon(100% 0%, 0% 0%, 50% 100%, 50% 100%);
}
.pane input[type="range"]::-webkit-slider-runnable-track {
width: 100%;
height: 1rem;
background: transparent;
color: var(--c-black);
border-radius: 0;
border: solid var(--color-inverted);
border-width: 0 0 1px;
}
.pane input[type="range"]::-webkit-slider-thumb {
border: 2px solid transparent;
height: 0.75rem;
width: 0.5rem;
border-radius: 0;
background: var(--color-inverted);
-webkit-appearance: none;
margin-top: 0.25rem;
transition: 150ms background-color, 200ms clip-path,
200ms -webkit-clip-path;
clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
}
.pane input[type="range"]::-moz-range-track {
width: 100%;
height: 1rem;
background: transparent;
color: var(--c-black);
border-radius: 0;
border: solid var(--color-inverted);
border-width: 0 0 1px;
}
.pane input[type="range"]::-moz-range-thumb {
border: 2px solid transparent;
height: 0.75rem;
width: 0.5rem;
border-radius: 0;
background: var(--color-inverted);
-webkit-appearance: none;
margin-top: 0.25rem;
transition: 150ms background-color, 200ms clip-path,
200ms -webkit-clip-path;
clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
}
.pane input[type="range"]::-ms-track {
width: 100%;
height: 1rem;
background: transparent;
color: var(--c-black);
border-radius: 0;
border: solid var(--color-inverted);
border-width: 0 0 1px;
}
.pane input[type="range"]::-ms-fill-lower {
background: var(--color-inverted);
border: none;
border-radius: 100%;
}
.pane input[type="range"]::-ms-fill-upper {
background: var(--color-inverted);
border-radius: 100%;
box-shadow: none;
}
.pane input[type="range"]::-ms-thumb {
border: 2px solid transparent;
height: 0.75rem;
width: 0.5rem;
border-radius: 0;
background: var(--color-inverted);
-webkit-appearance: none;
margin-top: 0.25rem;
transition: 150ms background-color, 200ms clip-path,
200ms -webkit-clip-path;
clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
}
.pane select {
color: var(--color-inverted);
width: 100%;
box-sizing: border-box;
-webkit-appearance: none;
border: 0;
box-shadow: 0 1px 0 0 var(--color-inverted);
border-radius: 0;
padding: 0.25rem 1rem 0.25rem 0rem;
background-color: transparent;
background-size: 1.25em 1.25em;
background-image: conic-gradient(
var(--color-inverted) 5%,
transparent 0 95%,
var(--color-inverted) 0
);
background-repeat: no-repeat;
background-position: right 0% top 120%;
}
.pane select:focus {
outline: none;
background-color: transparent;
}
svg:not(:root) {
overflow: visible;
}
[data-names] ol {
display: flex;
margin-top: 4rem;
flex-wrap: wrap;
gap: 8px;
}
[data-names] li {
position: relative;
width: 10rem;
background: #fff;
color: #202124;
}
[data-names] li::before {
content: "";
display: block;
padding-top: 100%;
background: var(--col);
}
.color-names {
/*position: relative;
z-index: 10;*/
padding: 4rem;
padding-left: calc(var(--sidebarwidth) + 4rem);
color: #fff;
}
.color-names h2 {
color: #fff;
}
.ellogo {
display: block;
width: 40%;
margin: 4rem 0 0;
}
</style>
</head>
<body>
<article class="app">
<section class="settings">
<aside>
<h3>About</h3>
<p>
FettePalette is lightweight, dependency free and fast JavaScript
function written in TypeScript. It generates color ramps based on a
curve within the HSV color model. This page serves as preview for
the variety of options the function takes.
</p>
<a class="projectlink" href="https://github.com/meodai/fettepalette">Github</a>
</aside>
<aside>
<h3 class="code-title">Function Call</h3>
<code>
<pre data-code></pre>
</code>
</aside>
<aside>
<h3>Settings</h3>
<div class="settings__inner">
<div data-pane></div>
</div>
</aside>
<footer>
<a href="https://www.elastiq.ch/" hreflang="en" class="ellogo"><svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 352 185">
<g fill="none" fill-rule="evenodd" transform="translate(0 6)">
<path fill="#fff" fill-rule="nonzero"
d="M179.54 71.84a9 9 0 00-1.91.21 7.74 7.74 0 00-1.83.64 4 4 0 00-1.4 1.15 2.81 2.81 0 001.49 4.38 29.19 29.19 0 007 1.45 17.65 17.65 0 018.93 3.36 9.22 9.22 0 013.4 7.7c0 4.993-1.743 8.907-5.23 11.74s-8.323 4.25-14.51 4.25a21.41 21.41 0 01-8-1.36 17.6 17.6 0 01-5.53-3.4 14.1 14.1 0 01-3.28-4.51 12.64 12.64 0 01-1.19-4.68l10.55-2.55a7.32 7.32 0 002.38 4.81c1.42 1.333 3.577 2 6.47 2a13.24 13.24 0 005.19-.94 3.34 3.34 0 002.21-3.32 3.24 3.24 0 00-1.7-2.85c-1.133-.707-3.233-1.203-6.3-1.49a17.19 17.19 0 01-9.74-3.49 9.73 9.73 0 01-3.62-7.91 13.4 13.4 0 011.49-6.38 14 14 0 014-4.68 17.87 17.87 0 015.74-2.85 23.84 23.84 0 016.85-1 20.07 20.07 0 017.4 1.19 15 15 0 014.85 3 11.8 11.8 0 012.76 3.87 15.47 15.47 0 011.15 3.87l-10.45 2.72a5.27 5.27 0 00-2.13-3.62 8.32 8.32 0 00-5.04-1.31zm39.25 1.68h-11.91V63.33H221l3.91-18.55h10.72l-3.92 18.55h14.63v10.19h-16.83l-4.8 21.89.85.6 11.4-7.83 5.36 8-12.3 8.34a12 12 0 01-6.89 2.21 10.08 10.08 0 01-3.57-.64 8.74 8.74 0 01-5-4.72 9.22 9.22 0 01-.77-3.83 8 8 0 01.08-1.23c.053-.367.137-.863.25-1.49l4.67-21.3zm63.58 31a11.67 11.67 0 01-3.49 1.7 12.69 12.69 0 01-3.49.51 10.08 10.08 0 01-3.57-.64 9.25 9.25 0 01-3-1.79 8 8 0 01-2-2.81 9.16 9.16 0 01-.72-3.7 12.25 12.25 0 01.34-3l5.1-21.36-.85-.6-11.4 7.83-5.36-8 12.34-8.34a11.68 11.68 0 013.49-1.7 12.68 12.68 0 013.49-.51 10.11 10.11 0 013.57.64 9.28 9.28 0 013 1.79 8 8 0 012 2.81 9.18 9.18 0 01.72 3.7 12.32 12.32 0 01-.34 3l-5.11 21.36.85.6 11.4-7.83 5.36 8-12.33 8.34zm7.4-53.69a8 8 0 01-.64 3.19 7.68 7.68 0 01-1.74 2.55 8.57 8.57 0 01-2.59 1.7 7.82 7.82 0 01-3.11.64 7.72 7.72 0 01-3.15-.64 8.69 8.69 0 01-2.55-1.7 7.67 7.67 0 01-1.74-2.55 8.29 8.29 0 010-6.38 7.71 7.71 0 011.74-2.55 8.73 8.73 0 012.55-1.7 7.7 7.7 0 013.15-.64 7.8 7.8 0 013.11.64 8.61 8.61 0 012.59 1.7 7.72 7.72 0 011.74 2.55 8 8 0 01.64 3.18v.01zm42.8 48.58h-1.53a21.9 21.9 0 01-2.13 2.77 13 13 0 01-2.85 2.34 14.44 14.44 0 01-4 1.62 21.53 21.53 0 01-5.36.6 14 14 0 01-10.17-4.25 14.71 14.71 0 01-3.15-4.94 17.39 17.39 0 01-1.15-6.47 37.71 37.71 0 011.57-10.93 28.53 28.53 0 014.64-9.23 23.16 23.16 0 017.49-6.38 21 21 0 0110.12-2.38c3.46 0 6.057.71 7.79 2.13a10.62 10.62 0 013.53 5.19h1.53l1.28-6.13h10.72l-10.47 48.92.85.6 4.76-3.23 5.36 8-5.7 3.74a11.59 11.59 0 01-3.53 1.7 13.14 13.14 0 01-3.46.44 9.35 9.35 0 01-6.51-2.42 8.55 8.55 0 01-2.68-6.68c.017-.946.13-1.887.34-2.81l2.71-12.2zm-10.38-2.89a12.38 12.38 0 005.62-1.28 14.13 14.13 0 004.42-3.45 15.84 15.84 0 002.89-5 17 17 0 001-5.87 8.39 8.39 0 00-2.34-6.34 9 9 0 00-6.51-2.25 12.31 12.31 0 00-5.66 1.32 14 14 0 00-4.42 3.49 16.25 16.25 0 00-2.85 5 17 17 0 00-1 5.87c0 2.78.78 4.893 2.34 6.34a9.21 9.21 0 006.51 2.17z">
</path>
<path stroke="#eca6ca" stroke-linecap="round" stroke-linejoin="round" stroke-width="11.38"
d="M96.45 70.41c25.89-7.67 59 2.55 80.49-31.66 25.23-40.1 60.94-44.68 85.27-31.62 34.2 18.36 31.67 68.27 7.58 90.7-27.42 25.53-29.58 52.91-13.68 67.24 22.95 20.68 47.1-2.67 35.85-19.93-8.94-13.73-31.93-25.89-98.1 6.9-65.32 32.36-129.62 19-133.91-32.42-1.03-12.53 2.88-39.25 36.5-49.21h0z">
</path>
<path fill="#fff" fill-rule="nonzero"
d="M.3 104.52l12.41-58.55h35.31v10.72H21.65l-2.94 13.62h23.06v10.72H16.46l-2.89 13.78h25.14v10.71H.3v-1zm77.08 1.69a12.69 12.69 0 01-3.49.51 10.08 10.08 0 01-3.57-.64 9.25 9.25 0 01-3-1.79 8 8 0 01-2-2.81 9.16 9.16 0 01-.72-3.7 12.93 12.93 0 01.34-3l9.27-38.71-.85-.6-11.4 7.83-5.36-8 12.34-8.34a11.68 11.68 0 013.49-1.7 12.67 12.67 0 013.49-.51 10.09 10.09 0 013.57.64 9.26 9.26 0 013 1.79 8.05 8.05 0 012 2.81 9.17 9.17 0 01.72 3.7 14.62 14.62 0 01-.34 3L75.6 95.4l.85.6 11.4-7.83 5.36 8-12.34 8.35a11.68 11.68 0 01-3.49 1.69zm62.88-10.8l.85.6 4.08-2.89 5.36 8-5 3.4a12.39 12.39 0 01-7 2.21 9.85 9.85 0 01-5.79-1.79 7.85 7.85 0 01-3.23-5H128a15.69 15.69 0 01-1.79 2.68 9.7 9.7 0 01-2.5 2.1 14.05 14.05 0 01-3.49 1.45 17.83 17.83 0 01-4.72.55 14.23 14.23 0 01-6.13-1.32 15.05 15.05 0 01-4.89-3.66 17.4 17.4 0 01-3.28-5.49 19.3 19.3 0 01-1.19-6.89 35.69 35.69 0 011.53-10.63 26.73 26.73 0 014.42-8.64 20.46 20.46 0 0116.59-8 12.9 12.9 0 017.4 1.87 9.08 9.08 0 013.66 4.94h1.53l1.19-5.62h10.72l-6.79 32.13zm-20.55 1.11a11.54 11.54 0 009.4-4.51 15.06 15.06 0 002.42-4.81c.574-1.89.86-3.855.85-5.83a9.27 9.27 0 00-2.3-6.51 7.91 7.91 0 00-6.13-2.51 11.68 11.68 0 00-5.45 1.23 12.16 12.16 0 00-4 3.28 14.54 14.54 0 00-2.47 4.81 19.7 19.7 0 00-.85 5.83 10.06 10.06 0 002.08 6.3c1.38 1.813 3.53 2.72 6.45 2.72z">
</path>
</g>
</svg></a>
</footer>
</section>
<section class="main">
<h1>Fette­Palette</h1>
<!--p>
<mark>FettePalette</mark> is a function that returns an object containing
as many base colors, tints and shades as set in <code>colors</code>.
</p-->
<aside class="section">
<div class="section__text">
<h2>HSV Slice Preview</h2>
<p>
This figure shows the curve drawn within the HSV color model. It is drawn
using the method set in <code>curveMethod</code>. (<button data-pantrigger="curveMethod" data-panvalue="lamé">lamé</button>, <button data-pantrigger="curveMethod" data-panvalue="arc">arc</button>, <button data-pantrigger="curveMethod" data-panvalue="pow">pow</button>,
<button data-pantrigger="curveMethod" data-panvalue="powX">powX</button> or <button data-pantrigger="curveMethod" data-panvalue="powY">powY</button>) and controlled by the <code>curveAccent</code> [<button data-pantrigger="curveAccent" data-panvalue="-.05">-0.05</button>, <button data-pantrigger="curveAccent" data-panvalue="0">0.0</button>, <button data-pantrigger="curveAccent" data-panvalue=".05">0.05</button>, <button data-pantrigger="curveAccent" data-panvalue=".15">0.15</button>, <button data-pantrigger="curveAccent" data-panvalue=".5">0.5</button>, <button data-pantrigger="curveAccent" data-panvalue="1">1.0</button>].
</p>
<p>
The white dots represent the tints and the black dots the shades.
<code>offsetTint</code>, <code>offsetShade</code>,
<code>offsetCurveModTint</code> and
<code>offsetCurveModShade</code> have in imact on the curve and
the positions of those points.
</p>
<p>
The change in hue can be set with the
<code>hueCycle</code> option. <code>0</code> means the same color
for the full curve and <code>1</code>
would be a full rotation around the hue wheel.
</p>
</div>
<div class="section__fig">
<figure><svg data-figure viewbox="0 0 100 100"></svg></figure>
</div>
</aside>
<aside class="section">
<div class="section__text">
<h2>Full Palette</h2>
<p>
<code>total</code> [<button data-pantrigger="total" data-panvalue="3">3</button>, <button data-pantrigger="total" data-panvalue="9">9</button>, <button data-pantrigger="total" data-panvalue="12">12</button>]
sets the amount of base colors. However the
class returns an <code>Object</code> containing
<code>light</code>, <code>base</code> and <code>dark</code> colors
in an array of HSL coordinates. (as well as an array with all of
them called <code>all</code>)
</p>
</div>
<div class="section__fig">
<div data-colors></div>
</div>
</aside>
<aside class="section">
<div class="section__text">
<h2>Example Use</h2>
<p>click & hold to re-generate</p>
<p>
Each of those squares shows four random entires from the generated
colors. In a single square every color is unique. But because the
returned colors are segragated in <code>light</code>,
<code>base</code> and <code>dark</code>. Depending on your needs,
you could make sure to have a certain amount of each.
</p>
</div>
<div class="section__fig">
<div data-palette></div>
</div>
</aside>
<aside class="section">
<div class="section__text">
<h2>Color Ramp</h2>
<p>Showing a gradient of 4 random colors sorted by light</p>
</div>
<div class="section__fig">
<div data-ramp></div>
</div>
</aside>
<aside class="section">
<div class="section__text">
<h2>HSL Colors</h2>
<p>Full list of the generated colors.</p>
<p>
We used
<a title="culori" href="https://culorijs.org/">a library</a> to
convert the the HSL colors into different color models. We
deliberately choose to only deliver HSL
<code>[0…360, 0…1, 0…1]</code> to keep the function fast and
lightweight. There are plenty of awesome color libraries if you
need to have the colors converted to an other color model.
</p>
</div>
<div class="section__fig">
<div data-list></div>
</div>
</aside>
<p>
fork on
<a href="https://github.com/meodai/fettepalette">github</a> made by
<a href="https://www.elastiq.ch/">elastiq</a>.
</p>
</section>
<aside class="color-names">
<h2>Color Properties</h2>
<div data-names></div>
</aside>
</article>
<script src="https://cdn.jsdelivr.net/npm/culori@3.3.0/bundled/culori.umd.js"></script>
<script type="module">
import {
generateRandomColorRamp,
generateRandomColorRampParams,
hsv2hsl,
pointOnCurve,
colorToCSS,
} from "./index.mjs";
console.clear();
let outputModel = 'hsl';
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;
};
class Pane {
constructor(
$dom,
nameSpace="pane",
throttleTimer = 50
) {
this.watches = new Map();
this.nameSpace = nameSpace;
this.$dom = $dom;
this.$el = document.createElement("div");
this.$el.classList.add(nameSpace);
this.$el.addEventListener(
"input",
(e) => this._onChange(e),
{
captures: true,
passive: true,
},
true
);
$dom.appendChild(this.$el);
this.events = {};
this.timer;
}
_callCallbacks(ref) {
if (this.events.hasOwnProperty("change")) {
clearTimeout(this.timer);
this.events["change"].forEach((fn) => {
this.timer = setTimeout(() => {
fn.apply(ref || []);
}, this.throttleTimer);
});
}
}
_onChange(event) {
const $target = event.target;
if ("key" in $target.dataset) {
const key = $target.dataset.key;
const ref = this.watches.get(key).reference;
if ($target.dataset.type === "number") {
ref[key] = new Number($target.value);
} else {
ref[key] = $target.value;
}
if ("siblingid" in $target.dataset) {
document.getElementById($target.dataset.siblingid).value =
$target.value;
}
this._callCallbacks(ref);
}
}
on(event, fn) {
if (!this.events.hasOwnProperty(event)) {
this.events[event] = [];
}
this.events[event].push(fn);
}
addInput(reference, key, options) {
const $inputs = this._appendInput(reference, key, options);
this.watches.set(key, { reference, $inputs, key });
}
_inputToValue($input, ref ,key, value) {
if (!$input) {
return;
}
if ( $input.matches('select') ) {
Array.from($input.querySelectorAll('option')).forEach($input =>
$input.selected = $input.value === (value || ref[key])
);
} else {
$input.value = value || ref[key];
}
}
updateInputs(key, value) {
if (key) {
const watcher = this.watches.get(key);
watcher.$inputs.forEach($input =>
this._inputToValue(
$input,
watcher.reference,
watcher.key,
value
)
);
} else {
this.watches.forEach(watcher => {
watcher.$inputs.forEach($input =>
this._inputToValue($input, watcher.reference, watcher.key)
);
});
}
this._callCallbacks();
}
_appendInput(reference, key, options) {
let type = typeof reference[key];
const $e = document.createElement("label");
const $label = document.createElement("strong");
const $section = document.createElement("div");
let $i, $in;
if (type === "number") {
$i = document.createElement("input");
$in = document.createElement("input");
$i.setAttribute("type", "range");
$in.setAttribute("type", "number");
if (options.hasOwnProperty("min")) {
$i.setAttribute("min", options.min);
$in.setAttribute("min", options.min);
}
if (options.hasOwnProperty("max")) {
$i.setAttribute("max", options.max);
$in.setAttribute("max", options.max);
}
if (options.hasOwnProperty("step")) {
$i.setAttribute("step", options.step);
$in.setAttribute("step", options.step);
}
} else if (type === "string") {
if (options.hasOwnProperty("options")) {
$i = document.createElement("select");
options.options.forEach((option) => {
const $opt = document.createElement("option");
$opt.setAttribute("value", option);
$opt.innerHTML = option;
$i.appendChild($opt);
});
} else {
$i = document.createElement("input");
$i.setAttribute("type", "text");
}
}
$i.dataset.key = key;
$i.dataset.type = type;
$i.value = reference[key];
$i.id = `${this.nameSpace}--${key}`;
$i.classList.add(
`${this.nameSpace}__input`,
`${this.nameSpace}__input--${type}`
);
$section.appendChild($i);
$section.classList.add(
`${this.nameSpace}__inputs`,
`${this.nameSpace}__inputs--${type}`
);
if ($in) {
$in.dataset.key = key;
$in.dataset.type = type;
$in.value = reference[key];
$in.id = `${this.nameSpace}--${key}--value`;
$i.classList.add(
`${this.nameSpace}__input`,
`${this.nameSpace}__input--${type}`
);
$in.dataset.siblingid = `${this.nameSpace}--${key}`;
$i.dataset.siblingid = `${this.nameSpace}--${key}--value`;
$section.appendChild($in);
}
$label.innerHTML = key;
$label.classList.add(`${this.nameSpace}__label`);
$e.classList.add(`${this.nameSpace}__section`);
$e.appendChild($label);
$e.append($section);
this.$el.appendChild($e);
return [$i, $in];
}
}
const pane = new Pane(document.querySelector("[data-pane]"));
const PARAMS = {};
PARAMS.model = 'hsl';
Object.keys(generateRandomColorRampParams).forEach((key) => {
const param = generateRandomColorRampParams[key];
PARAMS[key] = param.default;
pane.addInput(PARAMS, key, param.props);
});
pane.addInput(PARAMS, 'model', {
options: ['hsl', 'oklch', 'lch']
});
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;
}
let localcolors = [...allColors];
$pal.innerHTML = "";
$pal.appendChild(paletteDom(localcolors, 1));
localcolors = shuffleArray(localcolors);
for (let i = 0; i < 2; i++) {
$pal.appendChild(paletteDom(localcolors, 2));
localcolors = shuffleArray(localcolors);
}
for (let i = 0; i < 4 * 2; i++) {
$pal.appendChild(paletteDom(localcolors, 4));
localcolors = shuffleArray(localcolors);
}
$ramp.style.setProperty(
"background",
`linear-gradient(90deg, ${allColors
.slice(0, 4)
.sort((a, b) => b.hsl[2] - a.hsl[2])
.map(
(c) => c.css
)
.join(",")})`
);
};
function paletteDom(colorArr, x = 1) {
const $div = document.createElement("div");
$div.classList.add("palette-sample");
$div.style.setProperty("--x", x);
$div.innerHTML = "<b><i></i><i></i></b>";
for (let i = 0; i < 4; i++) {
$div.style.setProperty(
`--col-${i}`,
colorArr[i].css
);
}
return $div;
}
function bam() {
colors = generateRandomColorRamp({
total: PARAMS.total,
centerHue: PARAMS.centerHue,
hueCycle: PARAMS.hueCycle,
offsetTint: PARAMS.offsetTint,
offsetShade: PARAMS.offsetShade,
curveAccent: PARAMS.curveAccent,
tintShadeHueShift: PARAMS.tintShadeHueShift,
curveMethod: PARAMS.curveMethod,
offsetCurveModTint: PARAMS.offsetCurveModTint,
offsetCurveModShade: PARAMS.offsetCurveModShade,
minSaturationLight: [PARAMS.minSaturation, PARAMS.minLight],
maxSaturationLight: [PARAMS.maxSaturation, PARAMS.maxLight],
colorModel: outputModel === 'hsl' ? 'hsl' : 'hsv',
});
// Mutate colors object
for (const key in colors) {
colors[key] = colors[key].map(([h, s, l]) => {
const { r, g, b } = culori.converter("rgb")({
mode: "hsl",
h,
s,
l,
});
return {
hsl: [h, s, l],
rgb: [r * 255, g * 255, b * 255],
hex: culori.formatHex({ mode: "hsl", h, s, l }),
css: colorToCSS([h, s, l], PARAMS.model),
contrast: {
white: culori.wcagContrast("#ffffff", { mode: "hsl", h, s, l }),
black: culori.wcagContrast("#000000", { mode: "hsl", h, s, l }),
},
};
});
}
document.querySelector(
"[data-code]"
).innerHTML = `generateRandomColorRamp({
total: ${PARAMS.total},
centerHue: ${PARAMS.centerHue},
hueCycle: ${PARAMS.hueCycle},
curveMethod: '${PARAMS.curveMethod}',
curveAccent: ${PARAMS.curveAccent},
offsetTint: ${PARAMS.offsetTint},
offsetShade: ${PARAMS.offsetShade},
tintShadeHueShift: ${PARAMS.tintShadeHueShift},
offsetCurveModTint: ${PARAMS.offsetCurveModTint},
offsetCurveModShade: ${PARAMS.offsetCurveModShade},
minSaturationLight: [${PARAMS.minSaturation}, ${PARAMS.minLight}],
maxSaturationLight: [${PARAMS.maxSaturation}, ${PARAMS.maxLight}],
});
`;
points(
PARAMS.total,
PARAMS.offsetTint,
PARAMS.offsetShade,
PARAMS.curveAccent,
colors.base,
PARAMS.curveMethod,
PARAMS.offsetCurveModTint,
PARAMS.offsetCurveModShade,
[PARAMS.minSaturation, PARAMS.minLight],
[PARAMS.maxSaturation, PARAMS.maxLight]
);
$picker.style.setProperty(
`--deg`,
`${colors.all[Math.floor(colors.all.length * 0.5)].hsl[0]}deg`
);
let col1 = `hsl(var(--deg, 0deg), 100%, 50%)`;
let col2 = `hsl(var(--deg, 0deg), 0%, 100%)`;
if (PARAMS.model === 'oklch') {
col1 = `oklch(.5 .4 var(--deg, 0deg))`;
col2 = `oklch(1 0 var(--deg, 0deg))`;
} else if (PARAMS.model === 'lch') {
col1 = `lch(50 150 var(--deg, 0deg))`;
col2 = `lch(100 0 var(--deg, 0deg))`;
}
$picker.style.setProperty(
`--col1`, col1
);
$picker.style.setProperty(
`--col2`, col2
)
document.querySelector("[data-colors]").innerHTML = colors.all.reduce(
(r, c) => {
return `${r}<i style="--w: ${1 / PARAMS.total}; --h: ${
c.hsl[0]
}; --s: ${c.hsl[1] * 100}; --l: ${c.hsl[2] * 100}; --c: ${c.css};"></i>`;
},
""
);
palette(colors, "random");
list(colors);
names(colors);
}
const xmlns = 'http://www.w3.org/2000/svg';
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++) {
const [x, y] = pointOnCurve(
curveMethod,
i,
colorsInt + 1,
curveAccent,
minSaturationLight,
maxSaturationLight
);
const hsl = hsv2hsl(0, x, y);
const newElement = document.createElementNS(
xmlns,
"circle"
);
newElement.setAttribute("cx", x * 100);
newElement.setAttribute("cy", 100 - y * 100);
newElement.setAttribute("r", "3");
newElement.style.fill = `hsl(${colorsArr[i - 1].hsl[0]}deg,${
hsl[1] * 100
}%,${hsl[2] * 100}%)`;
newElement.style.fill = colorsArr[i - 1].css;
newElement.style.stroke = "#202126";
newElement.style.strokeWidth = ".25px";
const [xl, yl] = pointOnCurve(
curveMethod,
i,
colorsInt + 1,
curveAccent + offsetCurveModTint,
minSaturationLight,
maxSaturationLight
);
const newElementLight = document.createElementNS(
xmlns,
"circle"
);
newElementLight.setAttribute("cx", (xl - offsetTint) * 100);
newElementLight.setAttribute("cy", 100 - (yl + offsetTint) * 100);
newElementLight.setAttribute("r", ".5");
newElementLight.style.fill = `#fff`;
newElementLight.style.strokeWidth = ".1px";
newElementLight.style.stroke = "#000";
const newElementDark = document.createElementNS(
xmlns,
"circle"
);
const [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", ".5");
newElementDark.style.fill = `#000`;
newElementDark.style.strokeWidth = ".1px";
newElementDark.style.stroke = "#fff";
$picker.appendChild(newElement);
$picker.appendChild(newElementLight);
$picker.appendChild(newElementDark);
}
}
const printColors = (arr, simple, names) =>
arr
.map(({ hsl, rgb, hex, contrast, css }, i) => {
if (simple) {
return `
<li class="swatch swatch--simple" style="--col:${css}; --coltext: ${
contrast.black > contrast.white ? "#202125" : "#fff"
}">
<div data-copy title="Click to copy">${Math.floor(
hsl[0]
)}° ${Math.floor(hsl[1] * 100)}% ${Math.floor(hsl[2] * 100)}%</div>
</li>`;
} else {
return `
<li class="full" style="--col:${css}; --coltext: ${
contrast.black < contrast.white ? "#202125" : "#fff"
}; --colbg: ${
contrast.black > contrast.white ? "#202125" : "#fff"
}">
<div class="color-info">
<strong>${names[i].name}</strong>
<button data-copy title="Click to copy">hsl(${Math.floor(
hsl[0]
)},${Math.floor(hsl[1] * 100)}%,${Math.floor(
hsl[2] * 100
)}%)</button>
<button data-copy title="Click to copy">rgb(${Math.floor(
rgb[0]
)},${Math.floor(rgb[1])},${Math.floor(rgb[2])})</button>
<button data-copy title="Click to copy">${hex}</button>
<div class="color-info__contrast">
<h5>wcag contrast:</h5>
<em><span style="color: #ffffff;"">Aa</span> ${contrast.white.toFixed(
2
)}</em>
<span style="color: #000000;">Aa</span> ${contrast.black.toFixed(
2
)}</em>
</div>
</div>
</li>
`;
}
})
.join("");
function list(colors) {
document.querySelector("[data-list]").innerHTML = `
<div>
<h3>Light Colors</h3>
<ol>${printColors(colors.light, true)}</ol>
</div>
<div>
<h3>Base Colors</h3>
<ol>${printColors(colors.base, true)}</ol>
</div>
<div>
<h3>Dark Colors</h3>
<ol>${printColors(colors.dark, true)}</ol>
</div>
`;
}
let timer;
const $names = document.querySelector("[data-names]");
function names(colors) {
$names.innerHTML = `<strong style="--col: #fff">…</strong>`;
clearTimeout(timer);
timer = setTimeout(() => {
let hexes = colors.all.map((c) => c.hex.replace("#", ""));
fetch(
`https://api.color.pizza/v1/?values=${hexes.join(
","
)}&noduplicates=true&goodnamesonly=true}`
)
.then((r) => r.json())
.then((d) => {
$names.innerHTML = `<ol>${printColors(
colors.all,
false,
d.colors
)}</ol>`;
});
}, 1000);
}
$pal.addEventListener("pointerdown", () => {
palette(colors, "random");
timer = setInterval(() => palette(colors, "random"), 100);
});
$pal.addEventListener("pointerup", () => clearInterval(timer));
pane.on("change", bam);
document.documentElement.addEventListener("pointerdown", e => {
const $target = e.target;
if($target.matches(`[data-pantrigger]`)) {
let value = !isNaN($target.dataset.panvalue) ? new Number($target.dataset.panvalue) : $target.dataset.panvalue;
PARAMS[$target.dataset.pantrigger] = value;
pane.updateInputs(
$target.dataset.pantrigger,
$target.dataset.panvalue
);
}
});
if (navigator.clipboard) {
document.querySelector("body").addEventListener("click", (e) => {
const $el = e.target;
if ($el.matches("[data-copy]")) {
const text = $el.innerText;
navigator.clipboard.writeText(text).then(() => {
$el.innerText = "copied!";
setTimeout(() => {
$el.innerText = text;
}, 500);
});
}
});
}
bam();
</script>
</body>
</html>