ChromeのColorPickerを実装した
TODO: gif, refactor, github
// https://gist.github.com/mjackson/5311256 class Picker { constructor() { this.view = { canvas: document.getElementById('canvas'), colorPanel: document.getElementById('colorPanel'), dummyPalette: document.getElementById('dummyPalette'), paletteThumb: document.getElementById('paletteThumb'), dummySlider: document.getElementById('dummySlider'), sliderThumb: document.getElementById('sliderThumb'), hexInput: document.getElementById('hex-input'), } this.colorUtil = { rgbToHsv: (r, g, b) => { r /= 255, g /= 255, b /= 255; let max = Math.max(r, g, b), min = Math.min(r, g, b); let h, s, v = max; let d = max - min; s = max == 0 ? 0 : d / max; if (max === min) { h = 0; // achromatic } else { switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return [ h, s, v ]; }, hsvToRgb: (h, s, v) => { let r, g, b; let i = Math.floor(h * 6); let f = h * 6 - i; let p = v * (1 - s); let q = v * (1 - f * s); let t = v * (1 - (1 - f) * s); 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; default: break; } return { r: r * 255, g: g * 255, b: b * 255 }; }, hexToRgb: (hex) => { if (hex.length !== 6) { return } const rgb = [hex.slice(0, 2), hex.slice(2, 4), hex.slice(4, 6)] .map(str => parseInt(str, 16 )) if (rgb.includes(NaN)) { return } return rgb }, } this.state = new Proxy({hue: 0, brightness: 1, saturation: 1}, { set: (obj, prop, value) => { if (prop === 'hue') { this.didSetHue(value); this.updateView(); } obj[prop] = value; if (prop === 'brightness') { this.updateView() } return true; } }) this.handleSlider(); this.handleColorPalette(); this.handleHexInput(); this.didSetHue(0) this.updateView() } drawGradient(fillColor) { const { canvas } = this.view; const context = canvas.getContext('2d'); const rect = { x: 0, y: 0, width: canvas.clientWidth, height: canvas.clientHeight }; const whiteGradation = context.createLinearGradient(0, 0, rect.width, 0); whiteGradation.addColorStop(0, 'rgba(255, 255, 255 ,1)'); whiteGradation.addColorStop(1, 'rgba(255, 255, 255, 0)'); const blackGradation = context.createLinearGradient(0, 0, 0, rect.height); blackGradation.addColorStop(0, 'rgba(0, 0, 0, 0)'); blackGradation.addColorStop(1, 'rgba(0, 0, 0, 1)'); [fillColor, whiteGradation, blackGradation].forEach(fillColor => { context.fillStyle = fillColor; context.fillRect(rect.x, rect.y, rect.width, rect.height); }) } handleSlider() { const { dummySlider, sliderThumb } = this.view; let isSliderDragging = false; dummySlider.addEventListener('mousemove', (e) => { if (!isSliderDragging) { return } sliderThumb.style.left = `${e.offsetX - sliderThumb.clientWidth / 2}px`; this.state.hue = e.offsetX / e.toElement.clientWidth; }) dummySlider.addEventListener('mousedown', (e) => isSliderDragging = true); dummySlider.addEventListener('mouseup', (e) => isSliderDragging = false); dummySlider.addEventListener('touchmove', (e) => { e.preventDefault() if (!isSliderDragging) { return } sliderThumb.style.left = `${e.changedTouches[0].pageX - sliderThumb.clientWidth / 2}px`; this.state.hue = e.changedTouches[0].pageX / e.toElement.clientWidth; }) dummySlider.addEventListener('touchstart', (e) => isSliderDragging = true); dummySlider.addEventListener('touchend', (e) => isSliderDragging = false); } handleColorPalette() { const { dummyPalette, paletteThumb } = this.view; let isPaletteDragging = false; dummyPalette.addEventListener('mousemove', (e) => { console.log(0) if (!isPaletteDragging) { return } paletteThumb.style.left = `${e.offsetX - paletteThumb.clientWidth / 2}px`; paletteThumb.style.top = `${e.offsetY - paletteThumb.clientHeight / 2}px`; this.state.saturation = e.offsetX / e.toElement.clientWidth; this.state.brightness = 1 - e.offsetY / e.toElement.clientHeight; }) dummyPalette.addEventListener('mousedown', () => isPaletteDragging = true) dummyPalette.addEventListener('mouseup', () => isPaletteDragging = false); } handleHexInput() { const { hexInput } = this.view; hexInput.addEventListener('input', (e) => { const rgb = this.colorUtil.hexToRgb(e.target.value.slice(1)); if(rgb) { const hsv = this.colorUtil.rgbToHsv(rgb[0], rgb[1], rgb[2]); this.state.saturation = hsv[1] this.state.brightness = hsv[2] this.state.hue = hsv[0] } }) } didSetHue() { const rgb = this.colorUtil.hsvToRgb(this.state.hue, 1, 1); const color = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 1)`; const { sliderThumb } = this.view; sliderThumb.style.backgroundColor = color; this.drawGradient(color); } updateView() { const { colorPanel, paletteThumb, sliderThumb, dummyPalette, dummySlider, hexInput } = this.view; const rgb = this.colorUtil.hsvToRgb(this.state.hue, this.state.saturation, this.state.brightness); const rgbaColor = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 1)`; colorPanel.style.backgroundColor = rgbaColor; paletteThumb.style.backgroundColor = rgbaColor; // TODO: rgb to hex string hexInput.value = `#${Object.values(rgb).map(value => ("0" + parseInt(value).toString(16)).slice(-2)).join("")}`; // thumb point paletteThumb.style.left = `${dummyPalette.clientWidth * this.state.saturation - paletteThumb.clientWidth / 2}px`; paletteThumb.style.top = `${dummyPalette.clientHeight * (1 - this.state.brightness) - paletteThumb.clientHeight / 2}px`; sliderThumb.style.left = `${dummySlider.clientWidth * this.state.hue - sliderThumb.clientWidth / 2}px`; // delegate if(this.onColorChange) { this.onColorChange(rgbaColor); } } } const picker = new Picker() picker.onColorChange = (color) => console.log(color)
<html> <style> .hue-slider { position: absolute; /* width, centerY, to parent element */ top: 50%; transform : translateY(-50%); width: 100%; height: 10px; background: linear-gradient(to right, hsl(0,100%,50%), hsl(60,100%,50%), hsl(120,100%,50%), hsl(180,100%,50%), hsl(240,100%,50%), hsl(300,100%,50%), hsl(360,100%,50%) ); border-radius: 12px; } .thumb { position: absolute; top: 50%; left: -12px; /* sliderの値が0の時thumbの中央がsliderの左端の真上に来るようにsize / 2ずらす */ transform: translateY(-50%); width: 24px; height: 24px; border-radius: 15px; border: 2px solid white; cursor: pointer; } .color-panel-thumb { position: absolute; top: -12; left: -12px; width: 24px; height: 24px; border-radius: 15px; border: 2px solid white; } .hue-slider-container { position: relative; height: 50px; width: 90%; margin: auto; background-color: white; } .dummy-slider { position: relative; width: 100%; height: 100%; background-color: transparent; } .vstack { display: flex; flex-direction: column; } .hstack { display: flex; flex-direction: row; justify-content: space-between; } #color-panel { width: 100%; background-color: red; position: relative; } #color-panel > div { color: white; font-size: 30px; top: 50%; left: 50%; transform: translate(-50%,-50%); position:absolute; text-align: center; } .container { background-color: white; width: 800px; border: 1px solid gray; } .color-string-container { width: 95%; margin: auto; height: 50px; } .hex-string-container { width: 100%; position: relative; } .hex { left: 50%; transform: translate(-50%,-45%); position: absolute; width: 35px; background: #fff; font-size: 12px; text-align: center; color: #3c4043; } .border { border: 1px solid #dadce0; border-radius: 5px; height: 36px; margin: 5px auto 5px auto; } input { text-align: center; background-color: #fff; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: 78%; border: none; font-family: Roboto,Arial,sans-serif; font-size: 12px; letter-spacing: 0.15px; color: #3c4043; outline: none; } .color-palette { position: relative; } #dummyPalette { position: absolute; background-color: transparent; width: 100%; height: 100%; top: 0px; left: 0px; } </style> <body> <div class="vstack container"> <div class="hstack"> <div id="colorPanel"><div>color</div></div> <div class="color-palette"> <div id="paletteThumb" class="color-panel-thumb"></div> <canvas id="canvas" width="600px" height="300px"></canvas> <div id="dummyPalette"></div> </div> </div> <div class="hue-slider-container"> <div class="hue-slider"></div> <div id="sliderThumb" class="thumb"></div> <div id="dummySlider" class="dummy-slider"></div> </div> <div class="color-string-container"> <div class="hex-string-container"> <div class="hex">HEX</div> <input id="hex-input" value="#123456"> <div class="border"></div> </div> </div> </div> <script> </script> </body> </html>