HSL Color Wheel – Joystick + Bezel (joyCoef +/-)
by DOK_UA
HTML
<div class="card">
<div class="row">
<div class="readout" id="readout">H:0° S:50% L:50%</div>
<div class="readout" id="accuracy">50%</div>
<div class="row" style="gap:1px">
<div class="swatch" id="oldSwatch" style="background:#999"></div>
<div class="swatch" id="newSwatch"></div>
</div>
</div>
<!-- Статичні сектори. Обертається лише сірий ринг (HUE). -->
<svg id="ui" viewBox="0 0 100 120" xmlns="http://www.w3.org/2000/svg" aria-label="Color wheel with joystick">
<g id="segments" class="wheel-shadow">
<defs>
<path id="sector" d="M50,50 L50,2 A48,48 0 0 1 74,8 Z"/>
</defs>
<g>
<use href="#sector" fill="hsl(0, 100%, 50%)" transform="rotate(-15,50,50)"/>
<use href="#sector" fill="hsl(30, 100%, 50%)" transform="rotate(15,50,50)"/>
<use href="#sector" fill="hsl(60, 100%, 50%)" transform="rotate(45,50,50)"/>
<use href="#sector" fill="hsl(90, 100%, 50%)" transform="rotate(75,50,50)"/>
<use href="#sector" fill="hsl(120, 100%, 50%)" transform="rotate(105,50,50)"/>
<use href="#sector" fill="hsl(150, 100%, 50%)" transform="rotate(135,50,50)"/>
<use href="#sector" fill="hsl(180, 100%, 50%)" transform="rotate(165,50,50)"/>
<use href="#sector" fill="hsl(210, 100%, 50%)" transform="rotate(195,50,50)"/>
<use href="#sector" fill="hsl(240, 100%, 50%)" transform="rotate(225,50,50)"/>
<use href="#sector" fill="hsl(270, 100%, 50%)" transform="rotate(255,50,50)"/>
<use href="#sector" fill="hsl(300, 100%, 50%)" transform="rotate(285,50,50)"/>
<use href="#sector" fill="hsl(330, 100%, 50%)" transform="rotate(315,50,50)"/>
</g>
<circle cx="50" cy="50" r="30" fill="#2a2a2a"/>
</g>
<!-- Ринг (обертається за H) + сірий безель зі смужками -->
<g id="ring" class="handle">
<defs>
...
CSS
:root{
--bg:#1f1f1f; --panel:#2a2a2a; --text:#eaeaea;
--shadow: 0 10px 30px rgba(0,0,0,.35);
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0; display:grid; place-items:center; background:var(--bg); color:var(--text);
font:13px/1.35 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
}
.card{
background:var(--panel); padding:18px; border-radius:14px; box-shadow:var(--shadow);
display:grid; gap:12px; width:min(380px, 92vw);
}
.row{display:flex; align-items:center; justify-content:space-between; gap:10px}
.swatch{inline-size:52px; block-size:24px; border-radius:6px; border:1px solid #0008}
.readout{opacity:.9}
svg{width:100%; height:auto; display:block}
.wheel-shadow { filter: drop-shadow(0 6px 16px rgba(0,0,0,.35)); }
.joy-shadow { filter: drop-shadow(0 6px 10px rgba(0,0,0,.35)); }
.handle{ cursor: grab; touch-action: none; }
#ui{ touch-action: none; }
.handle:active{ cursor: grabbing; }
JavaScript
/* ======== STATE ======== */
let H = 0, S = 50, L = 50;
const readout = document.getElementById('readout');
const accView = document.getElementById('accuracy');
const newSwatch = document.getElementById('newSwatch');
const oldSwatch = document.getElementById('oldSwatch');
const ring = document.getElementById('ring');
const segments = document.getElementById('segments');
const joy = document.getElementById('joy');
const joyUnder = document.getElementById('joyUnder');
const newDot = document.getElementById('newColor');
const svgRoot = document.getElementById('ui');
const plusAccBtn = document.getElementById('plusAccuracy');
const minusAccBtn = document.getElementById('minusAccuracy');
const center = { x: 50, y: 50 };
const joyMaxR = 24; // межа руху джойстика
const hueSnap = 3;
/* ======== ЧУТЛИВІСТЬ ДЖОЙСТИКА ======== */
let joyCoef = 0.5; // старт: 50% (повільніше, щоб збігалось з #accuracy)
const baseS = 50, baseL = 30;
function updateAccuracyView(){
accView.textContent = `${Math.round(joyCoef * 100)}%`;
}
function clampNum(v, mn, mx){ return Math.min(mx, Math.max(mn, v)); }
/* ======== HELPERS ======== */
const clamp = (v, mn, mx)=> Math.min(mx, Math.max(mn, v));
const norm360 = (a)=> (a % 360 + 360) % 360;
const hsl = ()=> `hsl(${Math.round(H)}, ${Math.round(S)}%, ${Math.round(L)}%)`;
function setReadout(){ readout.textContent = `H:${Math.round(H)}° S:${Math.round(S)}% L:${Math.round(L)}%`; }
/* правильне перетворення в координати viewBox (100x120) */
function svgPoint(evt, svgEl){
const r = svgEl.getBoundingClientRect();
const vb = svgEl.viewBox.baseVal;
const x = ((evt.clientX - r.left) / r.width ) * vb.width + vb.x;
const y = ((evt.clientY - r.top ) / r.height) * vb.height + vb.y;
return { x, y };
}
function mouseAngleDeg(evt, svgEl){
const p = svgPoint(evt, svgEl);
const x = p.x - center.x;
const y = p.y - center.y;
let deg =...