Files
led-controller/docs/mockups/color-picker-chromium.js
Jimmy 9e2409430c Add documentation and utility modules
- Add API specification documentation
- Add system specification document
- Add UI mockups and documentation
- Add utility modules (wifi)
2026-01-11 21:34:18 +13:00

453 lines
14 KiB
JavaScript

/**
* Chromium-style Color Picker Component
* Matches native Chromium browser color picker design
*/
class ColorPickerChromium {
constructor(container, options = {}) {
this.container = typeof container === 'string' ? document.querySelector(container) : container;
this.options = {
initialColor: options.initialColor || '#FF0000',
onColorChange: options.onColorChange || null,
showHexInput: options.showHexInput !== false,
...options
};
this.currentColor = this.options.initialColor;
this.isOpen = false;
this.init();
}
init() {
this.createPicker();
this.setupEventListeners();
this.updateColor(this.options.initialColor);
}
createPicker() {
this.container.innerHTML = '';
this.container.className = 'color-picker-container';
// Color preview button
this.previewBtn = document.createElement('button');
this.previewBtn.className = 'color-picker-preview';
this.previewBtn.type = 'button';
this.previewBtn.style.backgroundColor = this.currentColor;
this.previewBtn.setAttribute('aria-label', 'Open color picker');
// Dropdown panel
this.panel = document.createElement('div');
this.panel.className = 'color-picker-panel';
this.panel.style.display = 'none';
// Main color area (hue/saturation)
this.mainArea = document.createElement('div');
this.mainArea.className = 'color-picker-main';
this.mainCanvas = document.createElement('canvas');
this.mainCanvas.width = 200;
this.mainCanvas.height = 200;
this.mainCanvas.className = 'color-picker-canvas';
this.mainArea.appendChild(this.mainCanvas);
// Main area cursor
this.mainCursor = document.createElement('div');
this.mainCursor.className = 'color-picker-cursor';
this.mainArea.appendChild(this.mainCursor);
// Hue slider
this.hueArea = document.createElement('div');
this.hueArea.className = 'color-picker-hue';
this.hueCanvas = document.createElement('canvas');
this.hueCanvas.width = 24;
this.hueCanvas.height = 200;
this.hueCanvas.className = 'color-picker-canvas';
this.hueArea.appendChild(this.hueCanvas);
// Hue slider cursor
this.hueCursor = document.createElement('div');
this.hueCursor.className = 'color-picker-hue-cursor';
this.hueArea.appendChild(this.hueCursor);
// Controls section
this.controls = document.createElement('div');
this.controls.className = 'color-picker-controls';
// Hex input
if (this.options.showHexInput) {
this.hexInput = document.createElement('input');
this.hexInput.type = 'text';
this.hexInput.className = 'color-picker-hex';
this.hexInput.placeholder = '#000000';
this.hexInput.maxLength = 7;
this.controls.appendChild(this.hexInput);
}
// RGB inputs (Chromium style - no sliders, just number inputs)
this.rgbContainer = document.createElement('div');
this.rgbContainer.className = 'color-picker-rgb';
['R', 'G', 'B'].forEach((label) => {
const wrapper = document.createElement('div');
wrapper.className = 'color-picker-rgb-item';
wrapper.dataset.channel = label.toLowerCase();
const labelEl = document.createElement('label');
labelEl.textContent = label;
const input = document.createElement('input');
input.type = 'number';
input.className = 'color-picker-rgb-input';
input.min = 0;
input.max = 255;
input.value = 0;
input.dataset.channel = label.toLowerCase();
wrapper.appendChild(labelEl);
wrapper.appendChild(input);
this.rgbContainer.appendChild(wrapper);
this[`rgb${label}`] = input;
});
this.controls.appendChild(this.rgbContainer);
// Assemble panel
const pickerArea = document.createElement('div');
pickerArea.className = 'color-picker-area';
pickerArea.appendChild(this.mainArea);
pickerArea.appendChild(this.hueArea);
this.panel.appendChild(pickerArea);
this.panel.appendChild(this.controls);
// Assemble container
this.container.appendChild(this.previewBtn);
this.container.appendChild(this.panel);
// Draw canvases
this.drawHueCanvas();
this.drawMainCanvas(1.0); // Start with full saturation
}
setupEventListeners() {
// Toggle panel
this.previewBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggle();
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!this.container.contains(e.target) && this.isOpen) {
this.close();
}
});
// Main area interaction
let isMainDragging = false;
this.mainCanvas.addEventListener('mousedown', (e) => {
isMainDragging = true;
this.handleMainAreaClick(e);
});
this.mainCanvas.addEventListener('mousemove', (e) => {
if (isMainDragging) {
this.handleMainAreaClick(e);
}
});
document.addEventListener('mouseup', () => {
isMainDragging = false;
});
// Touch support for main area
this.mainCanvas.addEventListener('touchstart', (e) => {
e.preventDefault();
isMainDragging = true;
this.handleMainAreaClick(e.touches[0]);
});
this.mainCanvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (isMainDragging) {
this.handleMainAreaClick(e.touches[0]);
}
});
this.mainCanvas.addEventListener('touchend', () => {
isMainDragging = false;
});
// Hue slider interaction
let isHueDragging = false;
this.hueCanvas.addEventListener('mousedown', (e) => {
isHueDragging = true;
this.handleHueClick(e);
});
this.hueCanvas.addEventListener('mousemove', (e) => {
if (isHueDragging) {
this.handleHueClick(e);
}
});
document.addEventListener('mouseup', () => {
isHueDragging = false;
});
// Touch support for hue slider
this.hueCanvas.addEventListener('touchstart', (e) => {
e.preventDefault();
isHueDragging = true;
this.handleHueClick(e.touches[0]);
});
this.hueCanvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (isHueDragging) {
this.handleHueClick(e.touches[0]);
}
});
this.hueCanvas.addEventListener('touchend', () => {
isHueDragging = false;
});
// Hex input
if (this.hexInput) {
this.hexInput.addEventListener('input', (e) => {
const value = e.target.value;
if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
this.updateColor(value);
}
});
this.hexInput.addEventListener('blur', (e) => {
const value = e.target.value;
if (!/^#[0-9A-Fa-f]{6}$/.test(value) && value.length > 0) {
e.target.value = this.currentColor;
}
});
}
// RGB inputs (Chromium style - only number inputs)
['R', 'G', 'B'].forEach(label => {
this[`rgb${label}`].addEventListener('input', (e) => {
let value = parseInt(e.target.value) || 0;
value = Math.max(0, Math.min(255, value)); // Clamp to 0-255
e.target.value = value;
const r = parseInt(this.rgbR.value) || 0;
const g = parseInt(this.rgbG.value) || 0;
const b = parseInt(this.rgbB.value) || 0;
const hex = this.rgbToHex(r, g, b);
this.updateColor(hex, false); // Don't update RGB inputs to avoid loop
});
this[`rgb${label}`].addEventListener('blur', (e) => {
let value = parseInt(e.target.value) || 0;
value = Math.max(0, Math.min(255, value));
e.target.value = value;
});
});
}
drawHueCanvas() {
const ctx = this.hueCanvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 0, 200);
for (let i = 0; i <= 6; i++) {
const hue = i * 60;
gradient.addColorStop(i / 6, `hsl(${hue}, 100%, 50%)`);
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 24, 200);
}
drawMainCanvas(hue) {
const ctx = this.mainCanvas.getContext('2d');
// Saturation gradient (left to right)
const satGradient = ctx.createLinearGradient(0, 0, 200, 0);
satGradient.addColorStop(0, `hsl(${hue}, 0%, 50%)`);
satGradient.addColorStop(1, `hsl(${hue}, 100%, 50%)`);
ctx.fillStyle = satGradient;
ctx.fillRect(0, 0, 200, 200);
// Brightness gradient (top to bottom)
const brightGradient = ctx.createLinearGradient(0, 0, 0, 200);
brightGradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
brightGradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
ctx.fillStyle = brightGradient;
ctx.fillRect(0, 0, 200, 200);
}
handleMainAreaClick(e) {
const rect = this.mainCanvas.getBoundingClientRect();
const x = Math.max(0, Math.min(200, e.clientX - rect.left));
const y = Math.max(0, Math.min(200, e.clientY - rect.top));
const saturation = x / 200;
const brightness = 1 - (y / 200);
this.updateColorFromHSB(this.hue, saturation, brightness);
this.updateCursor(x, y);
}
handleHueClick(e) {
const rect = this.hueCanvas.getBoundingClientRect();
const y = Math.max(0, Math.min(200, e.clientY - rect.top));
const hue = (y / 200) * 360;
this.hue = hue;
this.drawMainCanvas(hue);
this.updateHueCursor(y);
// Recalculate color with new hue
const rect2 = this.mainCanvas.getBoundingClientRect();
const x = parseFloat(this.mainCursor.style.left) || 0;
const y2 = parseFloat(this.mainCursor.style.top) || 0;
const saturation = x / 200;
const brightness = 1 - (y2 / 200);
this.updateColorFromHSB(hue, saturation, brightness);
}
updateColorFromHSB(h, s, v) {
const rgb = this.hsbToRgb(h, s, v);
const hex = this.rgbToHex(rgb.r, rgb.g, rgb.b);
this.updateColor(hex);
}
hsbToRgb(h, s, v) {
h = h / 360;
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
let r, g, b;
switch (i % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
rgbToHex(r, g, b) {
return '#' + [r, g, b].map(x => {
const hex = x.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join('').toUpperCase();
}
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
rgbToHsb(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const diff = max - min;
let h = 0;
if (diff !== 0) {
if (max === r) {
h = ((g - b) / diff) % 6) * 60;
} else if (max === g) {
h = ((b - r) / diff + 2) * 60;
} else {
h = ((r - g) / diff + 4) * 60;
}
}
if (h < 0) h += 360;
const s = max === 0 ? 0 : diff / max;
const v = max;
return { h, s, v };
}
updateColor(hex, updateInputs = true) {
this.currentColor = hex.toUpperCase();
this.previewBtn.style.backgroundColor = this.currentColor;
const rgb = this.hexToRgb(this.currentColor);
if (!rgb) return;
const hsb = this.rgbToHsb(rgb.r, rgb.g, rgb.b);
this.hue = hsb.h;
// Update main canvas
this.drawMainCanvas(this.hue);
// Update cursors
const x = hsb.s * 200;
const y = (1 - hsb.v) * 200;
this.updateCursor(x, y);
this.updateHueCursor((this.hue / 360) * 200);
// Update inputs
if (updateInputs) {
if (this.hexInput) {
this.hexInput.value = this.currentColor;
}
if (this.rgbR) {
this.rgbR.value = rgb.r;
this.rgbG.value = rgb.g;
this.rgbB.value = rgb.b;
}
}
// Callback
if (this.options.onColorChange) {
this.options.onColorChange(this.currentColor);
}
}
updateCursor(x, y) {
this.mainCursor.style.left = `${x}px`;
this.mainCursor.style.top = `${y}px`;
}
updateHueCursor(y) {
this.hueCursor.style.top = `${y}px`;
}
toggle() {
this.isOpen ? this.close() : this.open();
}
open() {
this.panel.style.display = 'block';
this.isOpen = true;
}
close() {
this.panel.style.display = 'none';
this.isOpen = false;
}
getColor() {
return this.currentColor;
}
setColor(color) {
this.updateColor(color);
}
}
// Export for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
module.exports = ColorPickerChromium;
}