poline
Version:
color palette generator mico-lib
1,587 lines (1,453 loc) • 98.8 kB
HTML
<!DOCTYPE html>
<html lang="en" class="is-loading">
<head>
<meta charset="utf-8" />
<!-- Primary Meta Tags -->
<title>Poline — Esoteric Color Palette Generation Library</title>
<meta name="title" content="Poline — Esoteric Color Palette Generation Library">
<meta name="description"
content="Poline is lightweight, dependency free and fast JavaScript function written in TypeScript. It draws lines between anchors over polar HSL coordinates to generate pleasing color palettes.">
<link rel="icon" type="image/x-icon">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://meodai.github.io/poline/">
<meta property="og:title" content="Poline — Esoteric Color Palette Generation Library">
<meta property="og:description"
content="poline is lightweight, dependency free and fast JavaScript function written in TypeScript. It draws lines between anchors over polar coordinates to generate pleasing color palettes.">
<meta property="og:image" content="https://meodai.github.io/poline/socialfb.png">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://meodai.github.io/poline/">
<meta property="twitter:title" content="Poline — Esoteric Color Palette Generation Library">
<meta property="twitter:description"
content="poline is lightweight, dependency free and fast JavaScript function written in TypeScript. It draws lines between anchors over polar coordinates to generate pleasing color palettes.">
<meta property="twitter:image" content="https://meodai.github.io/poline/socialfb.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/atom-one-light.min.css" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/atom-one-dark.min.css" media="(prefers-color-scheme: dark)">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Aboreto&family=Work+Sans:wght@300;400&display=swap" rel="stylesheet">
<style>
:root {
--light: #fff;
--dark: #18191b;
--grey-light: color-mix(in oklab, var(--light) 98%, var(--dark) 2%);
--grey: color-mix(in oklab, var(--light) 95%, var(--dark) 15%);
--bg: var(--light);
--onBg: var(--dark);
--cbgc: var(--grey-light);
--line: var(--grey);
--singleColorSlice: calc(360deg / var(--c-length, 1) / 2 * -1);
background: var(--bg);
color: var(--onBg);
font-family: 'Work Sans', sans-serif;
font-weight: 300;
font-size: 0.9rem;
-webkit-font-smoothing: subpixel-antialiased;
font-size: calc(0.4rem + 0.5vw);
accent-color: var(--c0);
scrollbar-width: thin;
scrollbar-color: var(--onBg) var(--bg);
}
.l-wrap::after {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
height: -webkit-fill-available;
background: var(--bg);
transition: opacity 0.5s ease;
opacity: 0;
pointer-events: none;
}
.is-loading .l-wrap::after {
opacity: 1;
pointer-events: all;
}
.is-loading .l-demo {
border-left: 0;
}
::-moz-selection {
color: var(--c1);
background: var(--c0);
}
::selection {
color: var(--c1);
background: var(--c0);
}
a {
color: var(--onBg);
}
a:hover {
text-decoration: none;
}
strong {
font-weight: 400;
}
strong.t {
font-weight: 300;
}
pre code.hljs {
background: var(--cbgc);
scrollbar-width: thin;
scrollbar-color: var(--onBg) var(--cbgc);
}
h1, h2, h3, .t, .wheel__huelabel {
font-family: 'Aboreto', cursive;
}
h1 {
font-size: 5rem;
margin: 0;
padding: 0;
font-weight: normal;
letter-spacing: -0.05em;
margin-left: -0.06em;
}
h2 {
font-size: 2.5rem;
margin: 0;
padding: 0;
font-weight: normal;
letter-spacing: -0.05em;
margin-left: -0.06em;
}
.subtitle {
margin-top: 10vmin;
}
.t-intro {
margin: 3em 0 1em;
}
p {
line-height: 1.5;
}
.poline-picker {
z-index: 4;
--diameter: min(60vmin, 60%);
position: absolute;
width: var(--diameter);
height: var(--diameter);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.poline-picker__circprev {
width: 12vmin;
height: 12vmin;
background: conic-gradient(from var(--singleColorSlice) at 50% 50%, var(--prev));
border-radius: 50%;
position: absolute;
top: 100%;
left: 50%;
z-index: 3;
transition: transform 0.5s cubic-bezier(0.3, 0.7, 0, 1);
box-shadow: 0 0 0 0.7vmin var(--bg);
pointer-events: none;
transform: translate(-50%, -50%) scale(0.4);
&::after {
content: '';
background: var(--bg);
position: absolute;
top: 50%;
left: 50%;
width: 15%;
height: 15%;
border-radius: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 0 0 1px var(--onBg);
}
}
.picker {
position: relative;
width: 100%;
aspect-ratio: 1;
--s: .4;
--l: .5;
--minL: #000;
--maxL: #fff;
}
.is-loading .picker {
pointer-events: none;
}
.is-loaded .picker {
pointer-events: all;
}
.picker::before {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
background: radial-gradient(closest-side, var(--minL), rgb(from var(--minL) r g b / 0), var(--maxL)),
conic-gradient(from 90deg, var(--grad));
z-index: 2;
}
svg {
position: relative;
z-index: 2;
overflow: visible !important;
width: 100%;
}
.wheel__line {
stroke: var(--onBg);
stroke-width: 0.15;
stroke-dasharray: var(--length) var(--length);
stroke-dashoffset: var(--length);
pointer-events: none;
fill: none;
animation: dasharray .8s cubic-bezier(.3,.7,0,1) forwards;
animation-delay: 2s;
}
.is-loaded .wheel__line {
animation: none;
stroke-dashoffset: 0;
}
@keyframes dasharray {
0% {
stroke-dashoffset: var(--length);
}
100% {
stroke-dashoffset: 0;
}
}
.wheel__point {
stroke: var(--onBg);
stroke-width: 0.15;
/*stroke-dasharray: var(--circ) var(--circ);
stroke-dashoffset: calc(var(--circ) - var(--circ) * var(--s));*/
}
.wheel__point {
pointer-events: none;
}
.wheel__anchor {
cursor: grab;
animation: anchor .3s cubic-bezier(0.3, 0.7, 0, 1) forwards;
animation-delay: 3s;
opacity: 0;
stroke-dasharray: 12.5 12.5;
stroke-dashoffset: calc(12.5 - 12.5 * var(--s));
stroke-dashoffset: 12.5;
stroke: transparent;
stroke-width: 0.2;
fill: var(--bg);
stroke: var(--onBg);
}
.is-loaded .wheel__anchor {
animation: none;
opacity: 1;
}
@keyframes anchor {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.wheel__huelabel {
user-select: none;
pointer-events: none;
box-sizing: border-box;
position: absolute;
top: 50%;
left: 50%;
font-size: 1.7vmin;
text-align: left;
pointer-events: none;
transition: font-size 0.3s cubic-bezier(.3,.7,0,1);
width: 10%;
animation: dropIn 1.5s cubic-bezier(.3,.7,0,1);
animation-delay: calc(var(--i) * .5s);
animation-fill-mode: forwards;
opacity: 0;
transform: scale(5) translate(-50%, -50%) rotate(calc(var(--i, 0) * 360deg)) translateX(540%) translateX(100%);
will-change: transform, opacity, font-size;
z-index: 2;
}
@keyframes dropIn {
0% {
opacity: 0;
transform: scale(5) translate(-50%, -50%) rotate(calc(var(--i, 0) * 360deg)) translateX(540%) translateX(100%);
}
15% {
opacity: 0;
}
100% {
opacity: 1;
transform: scale(1) translate(-50%, -50%) rotate(calc(var(--i, 0) * 360deg)) translateX(540%) translateX(100%);
}
}
.wheel__huelabel b {
display: block;
font-weight: normal;
animation-delay: calc(var(--i) * .5s);
animation-fill-mode: forwards;
animation: dropIndent 1.5s cubic-bezier(.3,.7,0,1);
transition: font-size 0.1s cubic-bezier(.3,.7,0,1);
}
.wheel__huelabel--flipped b {
/*transform: rotate(180deg);*/
}
@keyframes dropIndent {
0% {
transform: translateX(4000%);
}
100% {
transform: translateX(0%);
}
}
.picker .wheel__point {
animation: dropIn2 1.6s cubic-bezier(.3,.7,0,1);
animation-delay: calc(.6s + var(--i) * 100ms);
animation-fill-mode: forwards;
opacity: 0;
fill: var(--bg);
}
.is-loaded .wheel__point {
animation: none;
opacity: 1;
fill: var(--c);
}
@keyframes dropIn2 {
0% {
opacity: 0;
transform: scale(5);
fill: var(--bg);
}
85% {
fill: var(--bg);
}
100% {
opacity: 1;
transform: scale(1);
fill: var(--c);
}
}
.poline-picker__circprev {
animation: scalein .5s cubic-bezier(.3,.7,0,1.2);
animation-delay: 2.7s;
animation-fill-mode: forwards;
transform: translate(-50%, -0%) scale(0) rotate(-720deg);
}
@keyframes scalein {
0% {
transform: translate(-50%, -0%) scale(0) rotate(-720deg);
}
100% {
transform: translate(-50%, 50%) scale(0.4);
}
}
.picker::before {
animation: scaleInWheel 3s cubic-bezier(.3,.7,0,1);
animation-delay: 0s;
animation-fill-mode: forwards;
transform: scale(0);
/*filter: grayscale(1);*/
will-change: transform, opacity;
}
@keyframes scaleInWheel {
0% {
opacity: 0;
transform: scale(0) rotate(-720deg);
/*filter: grayscale(1);*/
}
10% {
opacity: 0;
}/*
20% {
filter: grayscale(1);
}*/
100% {
opacity: 1;
transform: scale(1) rotate(0deg);
/*filter: grayscale(0);*/
}
}
.picker:hover .wheel__huelabel {
font-size: 1vmin;
}
.picker:hover .wheel__huelabel--active b {
font-size: 1.7vmin;
}
.wheel__huelabel::before,
.wheel__huelabel::after {
content: "";
position: absolute;
}
.wheel__huelabel::after {
overflow: hidden;
width: 35%;
bottom: 50%;
aspect-ratio: 1;
right: auto;
left: -90%;
background: hsl(calc(360 * var(--i, 0)), 100%, 70%);
border-radius: 50%;
border: 1px solid var(--minL);
transform: translate(-50%, 50%) scale(0);
animation: scaleInD .5s cubic-bezier(.3,.7,0,1);
animation-delay: calc(1.2s + var(--i) * .5s);
animation-fill-mode: forwards;
--scale: .5;
box-shadow: 0 0 0 3px var(--maxL);
}
.wheel__huelabel--active::after {
--scale: .5;
transform: translate(-50%, 50%) scale(0);
}
.picker:hover .wheel__huelabel--active::after {
--scale: .8;
}
@keyframes scaleInD {
0% {
transform: translate(-50%, 50%) scale(0);
}
100% {
transform: translate(-50%, 50%) scale(var(--scale));
}
}
.is-loaded .wheel__huelabel::after {
animation: none;
transform: translate(-50%, 50%) scale(var(--scale));
transition: transform 0.3s cubic-bezier(.3,.7,0,1);
}
.wheel__huelabel::before {
height: 1px;
left: -60%;
right: 125%;
background: var(--onBg);
top: 50%;
transform: translateY(-50%) scale(.7) scaleX(0);
transform-origin: 0 0;
animation: reveal 1s cubic-bezier(.3,.7,0,1);
animation-delay: calc(1.6s + var(--i) * .5s);
animation-fill-mode: forwards;
}
@keyframes reveal {
0% {
transform: translateY(-50%) scale(.6) scaleX(0);
}
100% {
transform: translateY(-50%) scale(.6) scaleX(2.2);
}
}
label {
display: flex;
margin: 1.5rem 0 0;
font-size: .8rem;
justify-content: space-between;
border: 1px solid var(--grey);
padding: 1.5rem;
}
label + label {
border-top: none;
margin-top: -1px;
background-color: var(--bg);
}
label + button {
margin-top: 1.5rem;
}
label i {
text-align: right;
font-style: normal;
}
label .t {
display: block;
margin: 0;
font-size: 1.25rem;
flex: 0 1 auto;
}
select {
display: block;
padding: 0;
border: none;
font: inherit;
font-family: inherit;
font-size: 1rem;
text-decoration: underline;
background: transparent;
text-align: right;
color: var(--onBg);
}
button {
font-family: 'Aboreto', cursive;
background-color: var(--onBg);
color: var(--bg);
padding: 1.1em 1.8em;
border: none;
}
.l-wrap {
display: flex;
}
.l-menu {
flex: 0 0 40%;
width: 40%;
box-sizing: border-box;
padding: 0 8rem 20vh 8rem;
}
.l-demo {
z-index: 2;
position: fixed;
width: 60%;
height: 100%;
right: 0;
border-left: 1px solid var(--line);
}
.drawer {
--preview-width: 2rem;
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: var(--preview-width);
transform: translateX(calc(-100% + var(--preview-width)));
}
.drawer__preview {
position: absolute;
right: 0;
bottom: 0;
top: 0;
width: var(--preview-width);
/*background: linear-gradient(0deg, var(--prev));*/
}
.draw {
display: flex;
flex-direction: column-reverse;
position: absolute;
inset: 0;
}
.draw__item {
background-color: var(--c);
background: color-mix(in oklab, var(--c) 75%, var(--bg) 25%);
flex: 1 0 auto;
transform: translateX(-100%) scaleX(1) scale(1.01);
animation: draw 1s cubic-bezier(.3,.7,0,1);
animation-delay: calc(1.8s + var(--i) * .4s);
animation-fill-mode: forwards;
transform-origin: 100% 0;
&::after {
content: '';
position: absolute;
inset: 0;
background: var(--c);
transform: translateX(-25%);
transition: 400ms transform cubic-bezier(.3, .7, 0, 1);
animation: drawAfter 1s cubic-bezier(.3,.7,0,1);
}
}
.is-loaded .draw__item {
animation-duration: .5s;
animation-delay: calc(var(--i) * .5s);
&::after {
transform: translateX(0);
}
}
@keyframes draw {
0% {
transform: translateX(-100%) scaleX(1) scale(1.01);
}
60% {
transform: translateX(105%) scaleX(2.5) scale(1.01);
}
100% {
transform: translateX(0) scaleX(1) scale(1.01);
}
}
@keyframes drawAfter {
0%, 100% {
transform: translateX(0);
}
60% {
transform: translateX(-25%);
}
}
.drawer__preview::before,
.drawer__preview::after {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
background: linear-gradient(0deg, var(--prev-smooth));
}
.drawer__preview::after {
right: 0;
width: 1rem;
left: auto;
display: none;
}
.drawer__preview::before {
z-index: -1;
transform: translateX(1rem) scaleY(1.1) translateZ(0);
filter: blur(15px);
opacity: 0.5;
right: 0;
opacity: 0;
transition: .2s opacity linear;
display: none;
}
.is-loaded .drawer__preview::before {
opacity: 1;
}
.l-sec {
min-height: 20vh;
opacity: 1;
transition: 400ms opacity linear;
}
.l-sec + .l-sec {
margin-top: 10vh;
}
.l-sec--intro {
padding-top: 32vh;
display: flex;
align-items: center;
}
.l-sec__inner {
width: 100%;
}
.l-sec--active {
opacity: 1;
}
.l-sec__preview {
position: relative;
display: block;
height: 1.75rem;
background-image: linear-gradient(90deg, var(--prev-smooth)),
linear-gradient(90deg, var(--prev));
background-size: 100% 50%;
background-position: 0 0, 0 100%;
background-repeat: no-repeat;
margin: 0;
}
.l-sec__preview::after {
content: '';
position: absolute;
left: 0;
right: 0;
top: calc(50% - 1px);
height: 2px;
background: var(--bg);
}
.l-sec__preview figcaption {
display: none;
}
.color-at-sample {
height: .75rem;
background: var(--color-at);
}
.l-sec__controls {
margin-top: 2rem;
margin-bottom: 2rem;
}
code {
position: relative;
display: block;
font-size: .85rem;
color: var(--onBg);
background: var(--cbgc);
padding: 1.5em 1.2em;
border-radius: 0.2em;
border: 1px solid var(--line);
background-color: var(--bg);
}
.code-example {
position: relative;
margin-top: 1em;
margin-bottom: 1em;
& > button {
opacity: 0;
position: absolute;
top: 0em;
right: 1em;
z-index: 2;
font-size: 0.75em;
padding: 0.5em 2ch;
background: var(--cbgc);
box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.1),
0 0 0 1px var(--bg);
transition: box-shadow 0.1s linear;
border-radius: 2px;
transform: translateY(-250%);
transition: opacity 0.1s linear,
transform 0.5s cubic-bezier(.3,.7,0,1),
color 0.15s linear,
box-shadow 0.2s ease-out,
padding-right 0.2s cubic-bezier(.3,.7,0,1);
transition-delay: 0s, 0.15s, 0s, 0s, .1s;
line-height: 1;
font-family: inherit;
color: transparent;
border: none;
overflow: hidden;
&.copied {
color: var(--onBg);
padding-right: 2.25em;
transition-delay: 0s, 0.15s, 0s, 0s, 0s;
&::after {
transform: translateY(-40%) scaleY(.6);
opacity: 1;
transition: transform 0.3s cubic-bezier(.3, .7, 0, 1), opacity 0.2s linear;
}
}
&::after {
position: absolute;
top: 50%;
right: 1em;
content: '√';
display: inline-block;
transform: translateY(100%) scaleY(.3);
transition: transform 0.08s cubic-bezier(.3, .7, 0, 1), opacity 0.2s linear;
opacity: 0;
}
&:hover {
box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.2),
0 0 0 2px var(--bg);
}
&:active {
box-shadow: inset 0 0 0 1px var(--c0),
0 0 0 1px var(--bg);
}
}
}
.code-example:hover > button {
opacity: 1;
transform: translateY(-50%);
transition-delay: 0.05s, 0s, 0.1s;
color: var(--onBg);
}
.code-ex {
position: relative;
}
.code-ex::before {
opacity: 1;
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, var(--prev-smooth));
z-index: -1;
opacity: 0;
transform: translateX(0);
margin: 0;
}
.l-sec--active .code-ex::before {
opacity: 1;
transform: translateX(-1em);
transition: 200ms transform linear;
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
}
.toc {
font-size: 1em;
list-style-type: upper-roman;
font-family: 'Aboreto', cursive;
margin: 3em 0 0;
padding: 0;
}
.toc a {
font-size: 1em;
font-family: 'Work Sans', sans-serif;
}
.toc li + li {
margin: 0.5em 0 0;
}
.key {
--keyline: color-mix(in oklab, var(--line) 95%, var(--onBg) 5%);
display: inline-block;
width: 1.5em;
height: 1.5em;
border-radius: 0.2em;
border: 1px solid var(--keyline);
box-shadow: 0 2px 0 var(--keyline);
background: var(--bg);
text-align: center;
}
.export__title {
font-size: 2em;
margin: 0;
margin-bottom: 1em;
}
.export__list {
list-style-type: none;
margin: 0;
padding: 0;
font-size: 1.4em;
}
.export__item {
display: flex;
align-items: center;
}
.export__item + .export__item {
margin-top: 1.25em;
}
.export__sample {
position: relative;
width: 2.5em;
height: 2.5em;
flex: 0 0 2.5em;
border-radius: 0.2em;
border: 1px solid var(--onBg);
background: var(--c);
margin-right: 1em;
box-shadow: inset 0 0 0 2px color-mix(in oklab, var(--c) 75%, var(--light));
}
.export__sample::before {
content: '';
position: absolute;
inset: 50% 2px 2px;
border-radius: 0.2em;
background: var(--cHex);
}
.export__name,
.export__hex {
display: block;
font-size: .7em;
overflow: hidden;
.wrap {
--delay: calc((1 - var(--i)) * 150ms);
opacity: 0;
display: block;
animation: slidein 0.2s cubic-bezier(.3,.7,0,1);
animation-fill-mode: forwards;
}
}
.export__name {
font-size: 1em;
font-family: 'Aboreto', cursive;
}
.export__hex .wrap {
--offset: -70%;
transform: translateY(var(--offset));
animation-delay: calc(var(--delay) + 0.1s);
}
.export__name .wrap {
--offset: 250%;
transform: translateY(var(--offset));
animation-delay: var(--delay);
animation-duration: 0.4s;
}
@keyframes slidein {
0% {
opacity: 0;
transform: translateY(var(--offset));
}
20% {
opacity: 0;
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.ellogo {
display: block;
width: 100%;
max-width: 7em;
margin-top: 1.5em;
margin-bottom: 1em;
}
.hidden {
opacity: 0;
overflow: hidden;
height: 0;
}
.support {
font-size: 1.4em;
margin-top: 10vmin;
flex-wrap: wrap;
}
.support p {
font-size: .8em;
}
.support__title {
flex: 0 0 100%;
}
.kofi {
display: block;
width: 2.5em;
height: 2.5em;
margin-right: 1em;
margin-left: -0.4em;
margin-top: -0.2em;
}
.kofi:hover img {
animation: shakeup 0.5s ease;
}
.kofi img {
width: 100%;
transform-origin: 50% 50%;
}
@media (orientation: portrait) {
:root {
font-size: calc(0.6rem + 0.5vw);
padding: 0;
margin: 0;
overflow: hidden;
}
body {
padding: 0;
margin: 0;
}
h1, h2, h3 {
text-align: center;
}
h2, h3 {
margin-bottom: 1em;
}
.l-wrap {
display: block;
height: 100%;
height: -moz-available; /* WebKit-based browsers will ignore this. */
height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
height: fill-available;
}
.l-menu {
position: absolute;
width: 100%;
max-height: 100%;
padding: 50vh 10vw 20vh 10vw;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
}
.l-sec--intro {
padding-top: 0;
}
.l-demo {
--diameter: 27vh;
position: absolute;
width: 100%;
height: 40vh;
z-index: 10;
border-left: none;
transform: translateY(max(calc(var(--scroll-y, 0px) * -1), -20vh));
transform: none;
pointer-events: none;
}
.l-demo::before {
content: '';
position: absolute;
inset: 0;
background: rgb(from var(--bg) r g b / 0.5);
backdrop-filter: blur(5px);
margin-top: -1vh;
clip-path: circle(19vh at 50% 50%);
}
.poline-picker {
--diameter: 27vh;
margin-top: -1vh;
pointer-events: all;
}
.drawer {
position: absolute;
z-index: 1;
width: 100%;
bottom: 0;
top: auto;
height: calc(var(--preview-width) / 1.5);
background: none;
transform: none;
}
.draw {
position: absolute;
flex-direction: row;
}
.drawer__preview {
position: relative;
width: 100%;
inset: 0;
height: 100%;
background: linear-gradient(90deg, var(--prev));
}
.draw__item {
transform: translateY(100%) scaleY(1) scaleX(1.01);
animation: draw 1s cubic-bezier(.3,.7,0,1);
animation-delay: calc(1.8s + var(--i) * .4s);
animation-fill-mode: forwards;
transform-origin: 0% 0%;
&::after {
transform: translateY(25%);
animation: drawAfter 1s cubic-bezier(.3,.7,0,1);
}
}
.is-loaded .draw__item {
animation-duration: .5s;
animation-delay: calc(var(--i) * .5s);
&::after {
transform: translateY(0);
}
}
@keyframes draw {
0% {
transform: translateY(100%) scaleY(1) scaleX(1.01);
}
60% {
transform: translateY(-70%) scaleY(2) scaleX(1.01);
}
100% {
transform: translateY(0) scaleY(1) scaleX(1.01);
}
}
@keyframes drawAfter {
0%, 100% {
transform: translateY(0);
}
60% {
transform: translateY(25%);
}
}
.is-loaded .poline-picker__circprev {
transform: translate(-50%, max(-50%, calc(50% + -1 * var(--scroll-y, 0px)))) scale(0.4);
animation: none;
transition: none;
}
.ellogo {
margin-left: auto;
margin-right: auto;
}
.kofi {
margin: 1em auto;
}
}
@keyframes shakeup {
0%, 100% {
transform: translateY(0) rotate(0);
}
10%, 30%, 50%, 70%, 90% {
transform: translateY(-10px) rotate(-6deg);
}
20%, 40%, 60%, 80% {
transform: translateY(-8px) rotate(10deg);
}
}
@media (prefers-color-scheme: dark) {
:root {
--bg: var(--dark);
--onBg: var(--light);
--cbgc: color-mix(in oklab, var(--dark) 95%, var(--light) 5%);
--line: color-mix(in oklab, var(--light) 12%, var(--dark) 88%);
}
label {
border-color: var(--line);
}
.wheel__line {
stroke: var(--dark);
}
.wheel__point {
stroke: var(--dark);
}
.key {
background: var(--light);
color: var(--dark);
}
}
</style>
</head>
<body>
<main class="l-wrap">
<section class="l-menu" data-menu>
<div class="l-sec l-sec--intro">
<div class="l-sec__inner" data-section="intro">
<h1>Poline</h1>
<!--h2>Esoteric Palette Generator Interpolating HSL in cartesian space</h2-->
<p class="t-intro">
"<strong class="t">poline</strong>" is an enigmatic color palette generation library, that harnesses the mystical witchcraft of polar coordinates. Its
methodology, defying conventional color science, is steeped in the esoteric knowledge of the early 20th century. This
magical technology defies explanation, drawing lines between anchors to produce visually striking and otherworldly
palettes. It is an indispensable tool for the modern generative sorcerer, and a delight for the eye.
</p>
</div>
</div>
<aside class="l-sec">
<div class="l-sec__inner" aria-label="Table of Contents">
<p>
The tome of "<strong class="t">Poline</strong>" documentation is a comprehensive guide to the arcane arts of color generation,
you will gain an understanding of how this tool creates its mystical and captivating palettes through the following sections.
</p>
<ol data-toc class="toc"></ol>
</div>
</aside>
<article class="l-sec">
<div class="l-sec__inner">
<h2>Terminology & Working Principles</h2>
<p>
The name "<strong class="t">Poline</strong>" <strong aria-label="pronunciation for Poline">/ˈpoʊlaɪn/</strong> represents the essence of the library - <strong>a polar line</strong>.
The combination of these two words symbolizes the process of creating a palette by drawing lines between <strong>anchor</strong> points.
This unique moniker encapsulates the heart and soul of this micro-library written in TypeScript.
<p>
<p>
In "<strong class="t">Poline</strong>", <strong>anchors</strong> represent the points that the lines are drawn
between.
The number of <strong>points</strong> determines the number of colors generated between
each pair of anchors. The more points, the more colors generated. The positions of these points are determined by
<strong>position functions</strong>.
</p>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="summoning">
<h2 id="summoning">Summoning</h2>
<p>
The use of "<strong class="t">Poline</strong>" begins with the invocation of its command, which can be performed with or without arguments.
If called without, the tool will generate a mesmerizing palette featuring two randomly selected <strong>anchors.</strong>
</p>
<p>
On the other hand, one can choose to provide their own <strong>anchor</strong> points,
represented as a list of <strong>hsl</strong> values, for a more personal touch.
The power to shape and mold the colors lies in your hands.
</p>
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="summoning"></code></pre>
</div>
<p>
To create a palette, "<strong class="t">Poline</strong>" requires at least two anchor points, but the number of anchors you can provide is limitless.
The key to remember is that the more anchors you provide, the more challenging it becomes to generate a harmonious color
palette.
</p>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="points">
<h2 id="points">Points</h2>
<p>
The magic of "<strong class="t">Poline</strong>" is revealed through its technique
of drawing lines between anchor points. The richness of the palette is determined
by the number of <strong>points</strong>, with each connection producing a unique color.
</p>
<p>
As shown in the illustration, increasing the number of <strong>points</strong> will yield an even greater array of colors.
By default, four points are used, but this can easily be adjusted through the 'numPoints' property on your Poline
instance, as demonstrated in the code example.
</p>
<!--p>
As demonstrated on the illustration, "<strong class="t">Poline</strong>"
works by drawing lines between the <strong>anchors</strong>. The number of <strong>points</strong>
determines the number of colors generated between each pair of anchors.
The more <strong>points</strong> you have, the more colors you will get.
</p-->
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="points"></code></pre>
</div>
<p>
The resulting palette is a product of points multiplied by the number of anchor pairs.
It can be changed after initialization by setting the <strong>numPoints</strong> property on your "<strong class="t">Poline</strong>" instance.
</p>
<div class="l-sec__controls">
<label>
<span class="t">Steps</span>
<i><input type="range" min="1" max="15" value="4" data-steps></i>
</label>
</div>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="anchors">
<h2 id="anchors">Anchors</h2>
<p>
At the heart of "<strong class="t">Poline</strong>" lies the concept of <strong>anchors</strong>, the fixed points that serve as the foundation for the creation of
color palettes. <strong>Anchors</strong> are represented as a <strong>list of hsl</strong> values, which consist of three components: <strong>hue</strong> [0…360],
<strong>saturation</strong> [0…1], and <strong>lightness</strong> [0…1].
</p>
<p>
The choice is yours, whether to provide your own anchor points during
initialization or to allow "<strong class="t">Poline</strong>" to generate a random selection for you by omitting the 'anchorColors' argument. The
versatility of "<strong class="t">Poline</strong>" extends beyond its initial setup, as you can also add anchors to your palette at any time using
the '<strong>addAnchorPoint</strong>' method. This method accepts either a <strong>color</strong> as HSL array values or an array of <strong>X, Y, Z</strong> coordinates,
further expanding the possibilities of your color creation.
</p>
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="anchors"></code></pre>
</div>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="UpdatingAnchors">
<h2 id="UpdatingAnchors">Updating Anchors</h2>
<p>
With this feature, you have the power to fine-tune your palette and make adjustments as your creative vision
evolves. So whether you are looking to make subtle changes or bold alterations, "<strong class="t">Poline</strong>" is
always ready to help you
achieve your desired result.
</p>
<p>
The ability to update existing <string>anchors</string> is made possible through the '<strong>updateAnchorPoint</strong>' method.
This method accepts the <strong>reference to the anchor</strong> you wish to modify and either a color in the form of <strong>HSL</strong>
representation or an <strong>XYZ</strong> position array.
</p>
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="UpdatingAnchors"></code></pre>
</div>
<div class="l-sec__controls">
<button data-randomize>Randomize Positions</button>
</div>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="positionFunction">
<h2 id="positionFunction">Position Function</h2>
<p>
The <strong>position function</strong> in "<strong class="t">Poline</strong>" plays a crucial role in determining the <strong>distribution of colors between the anchors</strong>.
It works similar to easing functions and can be imported from the "<strong class="t">Poline</strong>" module.
</p>
<p>
A position function is a mathematical function that maps a value <strong>between 0 and 1</strong> to another value between 0 and 1.
By definition the same position function for all axes "<strong class="t">Poline</strong>" will draw a straight line between the anchors.
The chosen function will determine the distribution of colors between the anchors.
</p>
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="positionFunction"></code></pre>
</div>
<p>
If none is provided, "<strong class="t">Poline</strong>" will use the default function, which is a sinusoidal function.
</p>
<p>
The following position functions are available and can be included by importing the <strong>positionFunctions</strong> object from the "<strong class="t">Poline</strong>" module:
<ul>
<li>linearPosition</li>
<li>exponentialPosition</li>
<li>quadraticPosition</li>
<li>cubicPosition</li>
<li>quarticPosition</li>
<li>sinusoidalPosition <strong>(default)</strong></li>
<li>asinusoidalPosition</li>
<li>arcPosition</li>
</ul>
</p>
<div class="l-sec__controls">
<label>
<span><span class="t">Position Fn</span></span>
<i>
<select data-select="all">
</select>
</i>
</label>
</div>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="positionFunctions">
<h2 id="positionFunctions">Arcs</h2>
<p>
By defining <strong>different position functions for each axis</strong>, you can control the distribution of colors along each axis
(<strong>positionFunctionX</strong>, <strong>positionFunctionY</strong>, <strong>positionFunctionZ</strong>).
This will draw different arcs and create a diverse range of color palettes.
</p>
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="positionFunctions"></code></pre>
</div>
<!--p>
The <strong>position functions</strong> are used to determine the position of the
<strong>points</strong> between the <strong>anchors</strong>. The <strong>position functions</strong>
can be the same for XYZ (positionFunction) or different for each axis (positionFunctionX, positionFunctionY, positionFunctionZ).
</p>
<p>
They function a lot like an easing function and can be imported from the <strong class="t">poline</strong> module.
</p-->
<!--p>
Any function that takes a <strong>number between 0 and 1</strong> and returns a number between 0 and 1 can be used as a <strong>position function</strong>.
The second argument is called `reverse`. It is a boolean set to true on every second connection between anchors.
</p-->
<div class="l-sec__controls">
<label>
<span><span class="t">Position fn X</span>(Hue / Light)</span>
<i><select data-select="x">
</select></i>
</label>
<label>
<span><span class="t">Position fn Y</span>(Hue / Light)</span>
<i><select data-select="y">
</select></i>
</label>
<label>
<span><span class="t">Position fn Z</span> (Saturation)</span>
<i><select data-select="z">
</select></i>
</label>
</div>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="closedLoop">
<h2 id="closedLoop">Looping Palette</h2>
<p>
By default, the palette is not a closed loop. This means that the last color generated is not the same as the first color.
If you want the palette to be a closed loop, you can set the <strong>closedLoop</strong> argument to true.
</p>
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="closedLoop"></code></pre>
</div>
<p>
It is also possible to close the loop after the fact by setting <strong>poline.closedLoop = true|false</strong>.
</p>
<div class="l-sec__controls">
<label>
<span class="t">Closed Loop</span>
<i><input type="checkbox" checked="checked" data-loop></i>
</label>
</div>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="hueShift">
<h2 id="hueShift">Hue Shifting</h2>
<p>
With the power of hue shifting, "<strong class="t">Poline</strong>" provides yet another level of customization.
This feature allows you to <strong>shift the hue</strong> of the colors generated by a certain amount, giving you the ability to animate your
palette or create similar color combinations with different hues."
</p>
<p>
"<strong class="t">poline</strong>" supports hue shifting. This means that the hue of the colors will be shifted by a certain amount.
This can be useful if you want to animate the palette or generate a palette that looks similar to your current palette but using different hues.
</p>
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="hueShift"></code></pre>
</div>
<p>
The amount is a int or float between -Infinity and Infinity. It will permanently shift the hue of all colors in the palette.
</p>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="closestAnchor">
<h2 id="closestAnchor">Closest Anchor</h2>
<p>
In some situations, you might want to know which anchor is closest to a certain position or color.
This method is used in the visualizer to select the closest anchor on click.
</p>
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="closestAnchor">poline.getClosestAnchorPoint(
{xyz: [x, y, null], maxDistance: .1}
)</code></pre>
<p>
The <strong>maxDistance</strong> argument is optional and will return null if the closest anchor is further away
than the maxDistance.
</p>
<p>
Any of the <strong>xyz</strong> or <strong>hsl</strong> components can be null. If they are <strong>null</strong>, they will be ignored.
</p>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="getColors">
<h2 id="getColors">Color List</h2>
<p>
The '<strong class="t">poline</strong>' instance returns all colors as an array of <strong>hsl</strong> arrays or alternatively as an array of <strong>CSS</strong> strings, either formatted in <strong>HSL</strong> or stretched to <strong>OKlch</strong> or <strong>lch</strong>.
</p>
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="getColors">poline.colors
poline.colorsCSS
poline.colorsCSSlch
poline.colorsCSSoklch</code></pre></div>
<div class="l-sec__controls">
<figure class="l-sec__preview">
<figcaption>Colors as HSL</figcaption>
</figure>
</div>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="getColorAt">
<h2 id="getColorAt">Color At Position</h2>
<p>
The <strong>getColorAt</strong> method allows you to sample any color along the entire color journey by providing a position between 0 and 1.
This treats all segments as one continuous path, respecting the easing functions for each axis.
</p>
<p>
Position <strong>0</strong> returns the color at the very beginning, <strong>0.5</strong> returns the color at the middle of the entire journey,
and <strong>1</strong> returns the color at the very end. The method accounts for all easing functions and segment transitions.
</p>
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="getColorAt"></code></pre>
</div>
<div class="l-sec__controls">
<label>
<span class="t">Position</span>
<i><input type="range" min="0" max="1" value=".5" step="0.001" data-colorat></i>
</label>
<div class="l-sec__controls">
<div class="color-at-sample"></div>
</div>
</div>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="removeAnchor">
<h2 id="removeAnchor">Remove Anchors</h2>
<p>
To remove an anchor, you can use the <strong>removeAnchorPoint</strong> method.
It either takes an <strong>anchor</strong> reference or an <strong>index</strong> as an argument.
</p>
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="removeAnchor"></code></pre>
</div>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="invertedLightness">
<h2 id="invertedLightness">Invert lightness</h2>
<p>
"<strong class="t">Poline</strong>"'s default lightness setting places 0 in the center of the circle and 1 at its edges. However, you have the option
to invert the lightness, flipping the scheme so that 0 resides at the edges and 1 at the center.
</p>
<p>
Like the turning of night into day, or the unfolding of a magical spell, "<strong class="t">Poline</strong>"'s inverted lightness feature imbues your
palettes with a mystical quality that will unleash your inner wizard.
</p>
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="invertedLightness"></code></pre>
</div>
<div class="l-sec__controls">
<label>
<span class="t">Invert Lightness</span>
<i><input type="checkbox" data-invertlightness></i>
</label>
</div>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="colorSpace">
<h2 id="colorSpace">Color Model</h2>
<p>
To keep the library as lightweight as possible, "<strong class="t">poline</strong>" only supports the <strong>hsl</strong> color model out of the box.
However, it is easily possible to use other color models by using a library like <a href="https://culorijs.org/api/" target="_blank" rel="noopener noreferrer">culori</a>.
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="colorSpace"></code></pre>
</div>
<div class="l-sec__controls">
<label>
<span class="t">Current Model</span>
<i><select data-models>
</select></i>
</label>
</div>
</div>
</article>
<article class="l-sec">
<div class="l-sec__inner" data-section="installation">
<h2 id="installation">Installation</h2>
<p>
"<strong class="t">poline</strong>" is available as an <a href="https://www.npmjs.com/package/poline" target="_blank" rel="noopener noreferrer">npm package</a>.
Alternatively you can clone it on <a href="https://github.com/meodai/poline" target="_blank" rel="noopener noreferrer">GitHub</a>.
</p>
<div class="code-example">
<button data-copy-code>Copy Code</button>
<pre class="code-ex"><code class="language-js" data-code="installation">npm install poline</code></pre>
</div>
<p>
You can also use the <a href="https://unpkg.com/poline" target="_blank" rel="noopener noreferrer">unpkg CDN</a> to include the library in your project.
</p>
<p>
I recommend using the <strong