class ColorPicker { constructor(wrapper) { this.pointerDrag = false; this.hueDrag = false; this.opacityDrag = false; this.currentSaturation = 0; this.currentValue = 0; this.currentHue = 0; this.currentAlpha = 1; this.currentX; this.currentY; this.initialX; this.initialY; this.xOffset = 0; this.yOffset = 0; // this.element = wrapper; this.wrapper = createElement("div", { classList: ["amber-color-picker"] }); this.preview = createElement("div", { classList: ["neo-preview"] }); this.input = createElement("input", { }); this.dialog = createElement("div", { classList: ["amber-palette"], css: { display: "none" } }); this.inner = createElement("div", { classList: ["amber-inner"] }); this.palette_area = createElement("div", { classList: ["amber-palette-area"] }); this.palette_pointer = createElement("div", { }); this.hue_area = createElement("div", { classList: ["amber-hue-area"] }); this.hue_pointer = createElement("div", { }); this.alpha_area = createElement("div", { classList: ["amber-alpha-area"] }); this.alpha_pointer = createElement("div", { }); this.desc = createElement("div", { innerHTML: "rgb(0,0,0), rgba(0,0,0,0) 등 css 에서 지원하는 색상 형식 입력 가능", css: { "color":"#A0A0A0", "font-size":"9px", "line-height":"16px", "padding": "4px" } }); this.recentcolor = createElement("div", { classList: ["amber-recent-color"] }); this.palette_area.appendChild(this.palette_pointer); this.hue_area.appendChild(this.hue_pointer); this.alpha_area.appendChild(this.alpha_pointer); this.inner.appendChild(this.palette_area); this.inner.appendChild(this.hue_area); this.inner.appendChild(this.alpha_area); this.inner.appendChild(this.recentcolor); this.dialog.appendChild(this.inner); this.dialog.appendChild(this.desc); this.wrapper.appendChild(this.input); this.wrapper.appendChild(this.preview); this.wrapper.appendChild(this.dialog); for (let j = 0; j < wrapper.attributes.length; j++) { const attr = wrapper.attributes[j]; if (attr.name == "class") continue; this.input.setAttribute(attr.name, attr.value); } wrapper.parentNode.replaceChild(this.wrapper, wrapper); this.input.addEventListener("change", (evt) => { this.procEvent(evt, true); }); this.input.addEventListener("keyup", (evt) => { this.procEvent(evt); }); this.input.addEventListener("focus", (evt) => { this.openDialog(); }); this.palette_area.addEventListener("mousedown", (evt) => { this.pointerDrag = true; this.movePointer(evt); }); this.hue_area.addEventListener("mousedown", (evt) => { this.hueDrag = true; this.moveHue(evt); }); this.alpha_area.addEventListener("mousedown", (evt) => { this.opacityDrag = true; this.moveAlpha(evt); }); window.addEventListener("mouseup", (evt) => { this.pointerDrag = false; this.hueDrag = false; this.opacityDrag = false; }); window.addEventListener("mousemove", (evt) => { this.movePointer(evt); this.moveHue(evt); this.moveAlpha(evt); }); this.init(); } init() { this.checkFocusLeave(() => { this.dialog.style.setProperty('display', 'none'); this.addPaletteColor(this.input.value); }); this.regex = /[ㅁㅠㅊㅇㄷㄹ]/g; this.replacementMap = { 'ㅁ': 'a', 'ㅠ': 'b', 'ㅊ': 'c', 'ㅇ': 'd', 'ㄷ': 'e', 'ㄹ': 'f' }; this.setColor(1); this.getSavedPalette(); } getSavedPalette() { const d = localStorage.getItem('amber-palette'); if (d) { try { this.saved_palette = JSON.parse(d); this.prepareSavedPalette(); } catch(ex) { console.error(ex); } } else { this.saved_palette = []; localStorage.setItem('amber-palette', JSON.stringify([])); } } addPaletteColor(color) { const index = this.saved_palette.indexOf(color.toLowerCase()); if (index > -1) { this.saved_palette.splice(index, 1); } this.saved_palette.unshift(color.toLowerCase()); if (this.saved_palette.length > 45) { this.saved_palette.pop(); } localStorage.setItem('amber-palette', JSON.stringify(this.saved_palette)); } prepareSavedPalette() { this.recentcolor.innerHTML = ""; for(const e of this.saved_palette) { const col = createElement("div", { css: { "--background": e }, attributes: { title: e } }); col.addEventListener("click", (evt) => { this.input.value = e; this.setColor(1); }); this.recentcolor.appendChild(col); } } openDialog() { this.dialog.style.setProperty('display', 'flex'); this.getSavedPalette(); } movePointer(evt) { if (this.pointerDrag) { evt.preventDefault(); let rect = this.palette_area.getBoundingClientRect(); let cstyle = getComputedStyle(this.palette_area); let newX = evt.clientX - rect.left; let newY = evt.clientY - rect.top; newX = Math.max(0, Math.min(parseInt(cstyle.width), newX)); newY = Math.max(0, Math.min(parseInt(cstyle.height), newY)); this.currentSaturation = newX / parseInt(cstyle.width); this.currentValue = 1 - (newY / parseInt(cstyle.height)); const newRgb = this.hsvToRgba(this.currentHue, this.currentSaturation, this.currentValue); this.input.value = this.rgbToHex(newRgb.r, newRgb.g, newRgb.b) + (Math.max(0, Math.min(255, Math.floor(this.currentAlpha * 255)))).toString(16).padStart(2, '0'); this.setColor(); this.currentX = newX; this.currentY = newY; this.setTranslate(this.currentX, this.currentY, this.palette_pointer); } } moveHue(evt) { if (this.hueDrag) { evt.preventDefault(); let rect = this.hue_area.getBoundingClientRect(); let cstyle = getComputedStyle(this.hue_area); let newY = evt.clientY - rect.top; const my = parseInt(cstyle.height); newY = Math.max(0, Math.min(my, newY)); this.currentHue = (1 - (newY / my)) * 360; const newRgb = this.hsvToRgba(this.currentHue, this.currentSaturation, this.currentValue); this.input.value = this.rgbToHex(newRgb.r, newRgb.g, newRgb.b) + (Math.max(0, Math.min(255, Math.floor(this.currentAlpha * 255)))).toString(16).padStart(2, '0'); this.setColor(); this.setTranslateY(newY, this.hue_pointer); } } moveAlpha(evt) { if (this.opacityDrag) { evt.preventDefault(); let rect = this.alpha_area.getBoundingClientRect(); let cstyle = getComputedStyle(this.alpha_area); let newY = evt.clientY - rect.top; const my = parseInt(cstyle.height); newY = Math.max(0, Math.min(my, newY)); this.currentAlpha = 1 - Math.min(1, Math.max(0, newY / my)); const newRgb = this.hsvToRgba(this.currentHue, this.currentSaturation, this.currentValue); this.input.value = this.rgbToHex(newRgb.r, newRgb.g, newRgb.b) + (Math.max(0, Math.min(255, Math.floor(this.currentAlpha * 255)))).toString(16).padStart(2, '0'); this.setColor(); this.setTranslateY(newY, this.alpha_pointer); } } setTranslate(xPos, yPos, el) { el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`; } setTranslateY(yPos, el) { el.style.transform = `translateY(${yPos}px)`; } checkFocusLeave(callback) { const self = this; function isFocusInside(elem) { return self.wrapper.contains(elem == undefined ? document.activeElement : elem); } let wasFocusInside = isFocusInside(); document.addEventListener('focusin', checkFocus); document.addEventListener('mousedown', checkFocus); document.addEventListener('touchstart', checkFocus); function checkFocus(event) { setTimeout(() => { const isFocusInsideNow = isFocusInside(event.target); if (wasFocusInside && !isFocusInsideNow) { callback(); } wasFocusInside = isFocusInsideNow; }, 0); } return function cleanup() { document.removeEventListener('focusin', checkFocus); document.removeEventListener('mousedown', checkFocus); document.removeEventListener('touchstart', checkFocus); }; } setColor(c) { const val = this.procInput(this.input.value); if (val) { if (c) { const tar = this.rgbToHsv(val.r, val.g, val.b); this.currentHue = tar.h; this.currentSaturation = tar.s; this.currentValue = tar.v; this.currentAlpha = val.a; } const t1 = this.hsvToRgba(this.currentHue, 0, 1, 1); const t2 = this.hsvToRgba(this.currentHue, 1, 1, 1); let cstyle = getComputedStyle(this.palette_area); if (c) { const hsv = this.rgbToHsv(val.r, val.g, val.b); const mx = parseInt(cstyle.width); const my = parseInt(cstyle.height); const x = Math.min(mx, Math.max(0, (mx * hsv.s))); const y = Math.min(my, Math.max(0, my - (my * hsv.v))); this.setTranslate(x, y, this.palette_pointer); this.setTranslateY(my - ((hsv.h / 360) * my), this.hue_pointer); this.setTranslateY(my - (this.currentAlpha * my), this.alpha_pointer); } this.palette_area.style.background = `linear-gradient(to right, rgb(${t1.r},${t1.g},${t1.b}), rgb(${t2.r},${t2.g},${t2.b}))`; this.preview.style.setProperty('--background', `rgba(${val.r},${val.g},${val.b},${val.a.toFixed(2)})`); this.alpha_area.style.setProperty('--background', `linear-gradient(to bottom, rgba(${val.r},${val.g},${val.b},1), rgba(${val.r},${val.g},${val.b},0))`); } } procEvent(evt, validate = false) { const cursorPosition = this.input.selectionStart; const val = this.procInput(this.input.value); if (val) { this.setColor(1); } if (validate) { if (val) { this.wrapper.style.setProperty('background', '#FFFFFF'); this.wrapper.style.setProperty('border-color', '#F2F2F0'); if (val.type == "hex") { if (this.input.value[0] != '#') { this.input.value = `#${this.input.value}`; } } this.addPaletteColor(this.input.value); } else { this.wrapper.style.setProperty('background', '#FFDDDD'); this.wrapper.style.setProperty('border-color', '#EEA0A0'); } } } procInput(input) { input = input.replace(/\s/g, ''); // RGB and RGBA let match = input.match(/^rgba?\((\d+),(\d+),(\d+)(?:,([\d.]+))?\)$/); if (match) { return { r: parseInt(match[1]), g: parseInt(match[2]), b: parseInt(match[3]), a: match[4] ? parseFloat(match[4]) : 1, type: "rgb/rgba" }; } // HSV and HSVA match = input.match(/^hsva?\((\d+),(\d+)%,(\d+)%(?:,([\d.]+))?\)$/); if (match) { let h = parseInt(match[1]) / 360; let s = parseInt(match[2]) / 100; let v = parseInt(match[3]) / 100; let a = match[4] ? parseFloat(match[4]) : 1; return this.hsvToRgb(h, s, v, a); } // HSL and HSLA match = input.match(/^hsla?\((\d+),(\d+)%,(\d+)%(?:,([\d.]+))?\)$/); if (match) { let h = parseInt(match[1]) / 360; let s = parseInt(match[2]) / 100; let l = parseInt(match[3]) / 100; let a = match[4] ? parseFloat(match[4]) : 1; return this.hslToRgb(h, s, l, a); } // HWB match = input.match(/^hwb\((\d+),(\d+)%,(\d+)%(?:,([\d.]+))?\)$/); if (match) { let h = parseInt(match[1]) / 360; let w = parseInt(match[2]) / 100; let b = parseInt(match[3]) / 100; let a = match[4] ? parseFloat(match[4]) : 1; return this.hwbToRgb(h, w, b, a); } // LAB match = input.match(/^lab\((\d+)%,([-\d.]+),([-\d.]+)(?:,([\d.]+))?\)$/); if (match) { let l = parseInt(match[1]); let a = parseFloat(match[2]); let b = parseFloat(match[3]); let alpha = match[4] ? parseFloat(match[4]) : 1; return this.labToRgb(l, a, b, alpha); } // LCH match = input.match(/^lch\((\d+)%,([-\d.]+),([-\d.]+)(?:,([\d.]+))?\)$/); if (match) { let l = parseInt(match[1]); let c = parseFloat(match[2]); let h = parseFloat(match[3]); let alpha = match[4] ? parseFloat(match[4]) : 1; return this.lchToRgb(l, c, h, alpha); } // If no match found, return null return this.procHexRgba(input); } procHexRgba(input) { input = input.replace(/^#/, ''); if (input.length != 4 && input.length != 8 && input.length != 3 && input.length != 6) { return null; } if (input.length == 3) { let val = `${input[0]}${input[0]}${input[1]}${input[1]}${input[2]}${input[2]}`; let rgb = this.hexToRgb(val); return { r: rgb.r, g: rgb.g, b: rgb.b, a: 1, type: "hex" }; } else if (input.length == 4) { let val = `${input[0]}${input[0]}${input[1]}${input[1]}${input[2]}${input[2]}`; let rgb = this.hexToRgb(`${input[0]}${input[0]}${input[1]}${input[1]}${input[2]}${input[2]}`); let alpha = parseInt(`${input[3]}${input[3]}`, 16); return { r: rgb.r, g: rgb.g, b: rgb.b, a: alpha / 255, type: "hex" }; } else if (input.length == 6) { let rgb = this.hexToRgb(input.substring(0, 6)); return { r: rgb.r, g: rgb.g, b: rgb.b, a: 1, type: "hex" }; } else if (input.length == 8) { let rgb = this.hexToRgb(input.substring(0, 6)); let alpha = parseInt(input.substring(6, 8), 16); return { r: rgb.r, g: rgb.g, b: rgb.b, a: alpha / 255, type: "hex" }; } } hsvToRgba(h, s, v, a = 1) { h /= 360; let r, g, b; 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); 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), a: a, type: "hsv/hsva" }; } hslToRgb(h, s, l, a = 1) { let r, g, b; if (s === 0) { r = g = b = l; // achromatic } else { const hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255), a: a, type: "hsl/hsla" }; } rgbToHsl(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, l = (max + min) / 2; if (max == min) { h = s = 0; // achromatic } else { let d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 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, l}; } hwbToRgb(h, w, b, a = 1) { // First convert to HSL const l = (1 - w - b) / 2; const s = l === 0 || l === 1 ? 0 : (1 - w / (1 - b)) / 2; // Then convert HSL to RGB return hslToRgb(h, s, l, a); } labToRgb(l, a, b, alpha = 1) { // LAB to XYZ let y = (l + 16) / 116; let x = a / 500 + y; let z = y - b / 200; [x, y, z] = [x, y, z].map(value => { if (Math.pow(value, 3) > 0.008856) { return Math.pow(value, 3); } else { return (value - 16 / 116) / 7.787; } }); // XYZ to RGB let r = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z; let g = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z; let b1 = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z; [r, g, b1] = [r, g, b1].map(value => { if (value > 0.0031308) { return 1.055 * Math.pow(value, 1 / 2.4) - 0.055; } else { return 12.92 * value; } }); return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b1 * 255), a: alpha, type: "lab/laba" }; } lchToRgb(l, c, h, alpha = 1) { // Convert LCH to LAB const a = c * Math.cos(h * Math.PI / 180); const b = c * Math.sin(h * Math.PI / 180); // Then convert LAB to RGB return labToRgb(l, a, b, alpha); } rgbToHsv(r, g, b) { r /= 255, g /= 255, b /= 255; const max = Math.max(r, g, b), min = Math.min(r, g, b); let h, s, v = max; const d = max - min; s = max === 0 ? 0 : d / max; if (max === min) { h = 0; } 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: h * 360, s: s, v: v }; } rgbToHex(r, g, b) { r = Math.min(255, Math.max(0, r)); g = Math.min(255, Math.max(0, g)); b = Math.min(255, Math.max(0, b)); return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); } hexToRgb(hex) { hex = hex.replace(/^#/, ''); let bigint = parseInt(hex, 16); let r = (bigint >> 16) & 255; let g = (bigint >> 8) & 255; let b = bigint & 255; return { r, g, b }; } isValidColor(color) { const s = new Option().style; s.color = color; return s.color !== ''; } parseColor(color) { const s = new Option().style; s.color = color; return { r: parseInt(s.color.match(/\d+/g)[0]), g: parseInt(s.color.match(/\d+/g)[1]), b: parseInt(s.color.match(/\d+/g)[2]), a: s.color.match(/\d+/g)[3] ? parseFloat(s.color.match(/\d+/g)[3]) : 1 }; } } document.addEventListener("DOMContentLoaded", (evt) => { document.querySelectorAll('.neo_color').forEach((e) => { new ColorPicker(e); }); });