poline
Version:
color palette generator mico-lib
439 lines (379 loc) • 12.8 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Poline Picker Example</title>
<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: #202124;
--bg: var(--light);
--onBg: var(--dark);
background: var(--bg);
color: var(--onBg);
font-family: 'Work Sans', sans-serif;
font-weight: 300;
font-size: 0.9rem;
accent-color: var(--c0);
}
body {
font-family: 'Work Sans', sans-serif;
margin: 0;
padding: 2rem;
background: #f0f0f0;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
label {
display: flex;
margin: 0;
font-size: .8rem;
justify-content: space-between;
border: 1px solid #e0e0e0;
padding: .5rem 1.5rem;
background-color: var(--bg);
}
label:first-of-type {
padding-top: 1.5rem;
}
label + label {
border-top: none;
margin-top: -1px;
background-color: var(--bg);
}
label:last-of-type {
padding-bottom: 1.5rem;
}
label + button {
margin-top: 1.5rem;
}
label i {
text-align: right;
font-style: normal;
}
label .t {
display: block;
margin: 0;
font-size: 1rem;
flex: 0 1 auto;
font-family: 'Aboreto', cursive;
}
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: 1em 1.75em;
border: none;
cursor: pointer;
width: 100%;
margin-top: 1.5rem;
}
.controls {
margin-top: 2rem;
margin-bottom: 2rem;
width: 360px;
}
poline-picker {
width: 300px;
height: 300px;
--poline-picker-line-color: #333;
--poline-picker-bg-color: #fff;
}
#colors,
#colors-oklch,
#colors-lch {
display: flex;
margin-top: .2rem;
width: 300px;
height: 10px;
gap: 0.1em;
}
.color-swatch {
flex: 1 0 auto;
}
h1 {
color: #333;
font-family: 'Aboreto', cursive;
font-size: 2.5rem;
margin: 0;
padding: 0;
font-weight: normal;
letter-spacing: -0.05em;
margin-left: -0.06em;
}
h2 {
font-family: 'Aboreto', cursive;
font-size: 1rem;
margin: 1rem 0 0 0;
font-weight: normal;
}
.instructions {
color: #666;
max-width: 400px;
line-height: 1.5;
text-align: center;
}
.key {
display: inline-block;
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 3px;
padding: 2px 6px;
font-family: monospace;
font-size: 0.85em;
}
.hide {
display: none;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: var(--dark);
--onBg: var(--light);
}
body {
background: #1a1a1a;
}
.instructions {
color: #ccc;
}
h1, h2 {
color: var(--light);
}
.key {
background: #333;
border-color: #555;
color: var(--light);
}
}
</style>
</head>
<body>
<div class="container">
<h1>Poline Picker</h1>
<p class="instructions">
Experience the magic of "<strong>poline</strong>" for yourself, dear color explorer.
Drag existing anchor points to adjust colors. Press <span class="key">P</span> to add a new point at cursor position,
and press <span class="key">⌫</span> to remove the last selected anchor point.
Try using the <span class="key">←</span> and <span class="key">→</span> keys to change the hue of all colors
</p>
<div class="controls">
<label>
<span class="t">Steps</span>
<i><input type="range" min="1" max="15" value="5" id="steps"></i>
</label>
<label>
<span class="t">Invert Lightness</span>
<i><input type="checkbox" id="invertedLightness"></i>
</label>
<label>
<span class="t">Closed Loop</span>
<i><input type="checkbox" id="closedLoop"></i>
</label>
<label class="hide">
<span class="t">Allow Add Points</span>
<i><input type="checkbox" id="allowAddPoints"></i>
</label>
<label>
<span><span class="t">Position fn X</span> (Hue / Light)</span>
<i><select id="positionFunctionX">
</select></i>
</label>
<label>
<span><span class="t">Position fn Y</span> (Hue / Light)</span>
<i><select id="positionFunctionY">
</select></i>
</label>
<label>
<span><span class="t">Position fn Z</span> (Saturation)</span>
<i><select id="positionFunctionZ">
</select></i>
</label>
<button id="randomize">Randomize Anchors</button>
</div>
<poline-picker interactive></poline-picker>
<article aria-label="Color Swatches">
<h2>HSL</h2>
<div id="colors"></div>
<h2>OKLch</h2>
<div id="colors-oklch"></div>
<h2>Lch</h2>
<div id="colors-lch"></div>
</article>
</div>
<script type="module">
import { Poline, positionFunctions } from "./picker.mjs";
const picker = document.querySelector("poline-picker");
const colorsContainer = document.getElementById("colors");
const colorsOklchContainer = document.getElementById("colors-oklch");
const colorsLchContainer = document.getElementById("colors-lch");
// Control elements
const stepsInput = document.getElementById("steps");
const allowAddPointsCheckbox = document.getElementById("allowAddPoints");
const invertedLightnessCheckbox = document.getElementById("invertedLightness");
const closedLoopCheckbox = document.getElementById("closedLoop");
const positionFunctionXSelect = document.getElementById("positionFunctionX");
const positionFunctionYSelect = document.getElementById("positionFunctionY");
const positionFunctionZSelect = document.getElementById("positionFunctionZ");
const randomizeButton = document.getElementById("randomize");
// Initialize poline
const poline = new Poline({
anchorColors: [
[0, 1, 0.5],
[180, 1, 0.5],
],
numPoints: 5,
positionFunctionX: positionFunctions.smoothStepPosition,
positionFunctionY: positionFunctions.smoothStepPosition,
positionFunctionZ: positionFunctions.linearPosition,
closedLoop: false,
invertedLightness: false,
});
// Populate position function selects
const positionSelects = [positionFunctionXSelect, positionFunctionYSelect, positionFunctionZSelect];
const functionNames = Object.keys(positionFunctions);
positionSelects.forEach(select => {
functionNames.forEach(name => {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
select.appendChild(option);
});
});
// Set initial values
positionFunctionXSelect.value = 'smoothStepPosition';
positionFunctionYSelect.value = 'smoothStepPosition';
positionFunctionZSelect.value = 'linearPosition';
picker.setPoline(poline);
let lastMousePosition = { x: 0, y: 0 };
let lastSelectedPoint = null;
function createColorSwatches(container, colors) {
container.innerHTML = "";
colors.forEach(color => {
const swatch = document.createElement("div");
swatch.className = "color-swatch";
swatch.style.backgroundColor = color;
swatch.title = color;
container.appendChild(swatch);
});
}
function updatePolineAndColors() {
picker.setPoline(poline);
updateColors();
}
function updateColors() {
createColorSwatches(colorsContainer, poline.colorsCSS);
createColorSwatches(colorsOklchContainer, poline.colorsCSSoklch);
createColorSwatches(colorsLchContainer, poline.colorsCSSlch);
}
// Track mouse position and selected points
picker.addEventListener("pointermove", (e) => {
const rect = picker.getBoundingClientRect();
lastMousePosition = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
});
picker.addEventListener("pointerdown", (e) => {
const rect = picker.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = (e.clientY - rect.top) / rect.height;
const closestAnchor = poline.getClosestAnchorPoint({
xyz: [x, y, null],
maxDistance: 0.1,
});
if (closestAnchor) {
lastSelectedPoint = closestAnchor;
}
});
// Listen for the custom event from the picker
picker.addEventListener("poline-change", updateColors);
// Control event listeners
stepsInput.addEventListener("input", (e) => {
poline.numPoints = parseInt(e.target.value);
updatePolineAndColors();
});
allowAddPointsCheckbox.addEventListener("change", (e) => {
picker.setAllowAddPoints(e.target.checked);
});
invertedLightnessCheckbox.addEventListener("change", (e) => {
poline.invertedLightness = e.target.checked;
updatePolineAndColors();
});
closedLoopCheckbox.addEventListener("change", (e) => {
poline.closedLoop = e.target.checked;
updatePolineAndColors();
});
// Position function handlers with shared logic
const createPositionFunctionHandler = (property) => (e) => {
poline[property] = positionFunctions[e.target.value];
updatePolineAndColors();
};
positionFunctionXSelect.addEventListener("change", createPositionFunctionHandler('positionFunctionX'));
positionFunctionYSelect.addEventListener("change", createPositionFunctionHandler('positionFunctionY'));
positionFunctionZSelect.addEventListener("change", createPositionFunctionHandler('positionFunctionZ'));
randomizeButton.addEventListener("click", () => {
poline.anchorPoints.forEach(anchor => {
anchor.hsl = [
(anchor.color[0] + (-90 + Math.random() * 180)) % 360,
Math.random(),
anchor.color[2] + (-.05 + Math.random() * .1),
];
});
poline.updateAnchorPairs();
updatePolineAndColors();
});
// Keyboard shortcuts
document.addEventListener("keydown", (e) => {
if (e.key === "p" || e.key === "P") {
const newPoint = picker.addPointAtPosition(lastMousePosition.x, lastMousePosition.y);
if (newPoint) {
lastSelectedPoint = newPoint;
}
updateColors();
}
if (e.key === "Backspace" || e.key === "Delete") {
if (lastSelectedPoint && poline.anchorPoints.length > 2) {
try {
poline.removeAnchorPoint({ point: lastSelectedPoint });
lastSelectedPoint = null;
updatePolineAndColors();
} catch (error) {
console.log("Cannot remove anchor point:", error.message);
}
}
}
if (e.key === "ArrowLeft") {
poline.shiftHue(-4);
updatePolineAndColors();
}
if (e.key === "ArrowRight") {
poline.shiftHue(4);
updatePolineAndColors();
}
});
// Initial color update
updateColors();
</script>
</body>
</html>