/** * {color组件} * @author chensbox * @date 2023/03/20 15:17:25 */ import { css, html, bind, unbind, Component, outsideClick, clearOutsideClick, offset, styleMap } from 'wkit' const EV_OPTION = { once: true } const DOC = document const ROOT = document.documentElement // H: 色相, S: 饱和度, B/V: 亮度 export function hsb2rgb(hsb) { let h = hsb.h let s = Math.round((hsb.s * 255) / 100) let v = Math.round((hsb.b * 255) / 100) let r = 0 let g = 0 let b = 0 if (s === 0) { r = g = b = v } else { let t1 = v let t2 = ((255 - s) * v) / 255 let t3 = ((t1 - t2) * (h % 60)) / 60 // if (h === 360) { h = 0 } if (h < 60) { r = t1 g = t2 + t3 b = t2 } else if (h < 120) { r = t1 - t3 g = t1 b = t2 } else if (h < 180) { r = t2 g = t1 b = t2 + t3 } else if (h < 240) { r = t2 g = t1 - t3 b = t1 } else if (h < 300) { r = t2 + t3 g = t2 b = t1 } else if (h < 360) { r = t1 g = t2 b = t1 - t3 } } r = Math.round(r) g = Math.round(g) b = Math.round(b) return { r, g, b } } export function rgb2hex({ r, g, b }, a) { let hex = [r, g, b].map(it => it.toString(16).padStart(2, '0')).join('') if (a !== void 0) { hex += (~~((a / 100) * 255)).toString(16) } return hex } export function hex2rgb(hex) { let r, g, b, a hex = hex.replace(/^#/, '') switch (hex.length) { case 3: case 4: r = hex[0].repeat(2) g = hex[1].repeat(2) b = hex[2].repeat(2) a = (hex[3] || 'f').repeat(2) break case 6: case 8: r = hex.slice(0, 2) g = hex.slice(2, 4) b = hex.slice(4, 6) a = hex.slice(6, 8) || 'ff' break } r = parseInt(r, 16) g = parseInt(g, 16) b = parseInt(b, 16) a = ~~((parseInt(a, 16) * 100) / 255) return { r, g, b, a } } export function rgb2hsb({ r, g, b }) { let hsb = { h: 0, s: 0, b: 0 } let max = Math.max(r, g, b) let min = Math.min(r, g, b) let delta = max - min hsb.b = max hsb.s = max === 0 ? 0 : (delta * 255) / max if (hsb.s === 0) { hsb.h = -1 } else { if (r === max) { hsb.h = (g - b) / delta } else if (g === max) { hsb.h = 2 + (b - r) / delta } else { hsb.h = 4 + (r - g) / delta } } hsb.h *= 60 if (hsb.h < 0) { hsb.h += 360 } hsb.s *= 100 / 255 hsb.b *= 100 / 255 return hsb } export function hex2hsb(hex) { return rgb2hsb(hex2rgb(hex)) } class Color extends Component { static props = { value: { type: String, default: '', observer(val) { this.#calc(val) } }, disabled: false } static styles = [ css` :host { display: inline-flex; } .container { position: relative; width: var(--wc-color-size, 32px); height: var(--wc-color-size, 32px); -webkit-user-select: none; user-select: none; } .alpha-bg { background: linear-gradient( 45deg, var(--color-grey-1) 25%, transparent 25%, transparent 75%, var(--color-grey-1) 75%, var(--color-grey-1) ), linear-gradient( 45deg, var(--color-grey-1) 25%, transparent 25%, transparent 75%, var(--color-grey-1) 75%, var(--color-grey-1) ); background-size: 12px 12px; background-position: 0 0, 6px 6px; } .focus { transition: box-shadow 0.15s linear; &:focus-within { box-shadow: 0 0 0 2px var(--color-plain-a); } } `, // 预览 css` .preview { display: flex; width: 100%; height: 100%; border: 1px solid var(--color-grey-2); border-radius: 3px; cursor: pointer; span { width: 100%; height: 100%; border: 3px solid #fff; border-radius: 3px; background: var(--value); outline: none; } } `, // .color-panel css` .color-panel { display: var(--show, none); position: absolute; z-index: 1; left: 0; top: var(--wc-color-size, 32px); width: 310px; padding: 5px; background: var(--color-plain-1); box-shadow: 0 0 12px rgba(0, 0, 0, 0.3); } .dashboard { display: flex; justify-content: space-between; .scene { overflow: hidden; position: relative; width: 280px; height: 180px; background: linear-gradient(180deg, transparent, #000), linear-gradient(90deg, #fff, transparent), var(--scene); .thumb { position: absolute; z-index: 1; left: var(--x); top: var(--y); width: 0; height: 0; &::after { display: block; width: 10px; height: 10px; border: 1px solid #fff; border-radius: 50%; background: rgba(32, 32, 32, 0.3); transform: translate(-5px, -5px); content: ''; } } } .pool { overflow: hidden; position: relative; width: 12px; height: 180px; background: linear-gradient( to bottom, #f00 0, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 ); .thumb { position: absolute; left: 0; top: var(--ht); width: 12px; height: 0; &::after { display: block; width: 12px; height: 12px; border: 1px solid #888; border-radius: 50%; background: #fff; transform: translateY(-6px); content: ''; } } } } .alpha-box { overflow: hidden; position: relative; width: 100%; height: 12px; margin: 12px 0; .bar { position: absolute; left: 0; top: 0; width: 100%; height: 12px; background: linear-gradient(90deg, transparent, var(--alpha)); } .thumb { position: absolute; left: var(--at); top: 0; width: 0; height: 12px; &::after { display: block; width: 12px; height: 12px; border: 1px solid #888; border-radius: 50%; background: #fff; transform: translateX(-6px); content: ''; } } .alpha { position: relative; display: block; width: 100%; height: 12px; opacity: 0; } } `, css` .input-box { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; font-size: 12px; .input { width: 200px; height: 24px; padding: 0 6px; line-height: 22px; font-family: monospace; border: 1px solid var(--color-grey-2); border-radius: 3px; outline: none; color: var(--color-dark-1); &::placeholder { color: var(--color-grey-1); } } .clear, .submit { color: var(--color-dark-1); cursor: pointer; } .submit { padding: 2px 6px; border-radius: 2px; color: #fff; background: var(--color-teal-2); outline: none; transition: box-shadow 0.15s linear, background 0.15s linear; &:hover { background: var(--color-teal-1); } } } `, css` :host([size='small']) { .container { width: 24px; height: 24px; } .color-panel { top: 24px; } } `, css` :host([disabled]) { opacity: 0.6; .preview { cursor: not-allowed; &:focus-within { box-shadow: unset; } } } ` ] #show = false // 临时的value, 组件内的操作, 修改的是这个值, 避免直接修改value触发太多的计算 #value = '' #x = 0 // 场景触点的X坐标 #y = 0 // 场景触点的Y坐标 #ht = 0 // 颜色池的触点坐标 #at = 100 // 透明度条的触点坐标 #sceneBg = '#000000' #alphaBg = '#000000' #rgba = { r: 0, g: 0, b: 0, a: 100 } #hsb = { h: 0, s: 100, b: 100 } #calc(val) { let isHex val = val.toLowerCase() if (!val || val === this.#value) { return } isHex = /^#[0-9a-f]{3,8}$/.test(val) if (isHex) { Object.assign(this.#rgba, hex2rgb(val)) } else { let res = val.match(/rgba?\((\d+),\s*?(\d+),\s*?(\d+)[,\s]*?([\d\.]+)?\)/) if (res) { this.#rgba = { r: +res[1], g: +res[2], b: +res[3], a: 100 } if (res[4] !== undefined) { this.#rgba.a = ~~(res[4] * 100) } } else { return } } this.#hsb = rgb2hsb(this.#rgba) } toggleColorPanel() { if (this.disabled) { return } this.#show = true this.#updateView() } // 透明度变化 #changeAlpha(ev) { this.#rgba.a = +ev.target.value this.#updateView() } // 色彩池变化 #changeHue(h) { h = h < 0 ? 0 : h > 360 ? 360 : h let { s, b } = this.#hsb let rgba = this.#rgba let hsb = { h, s, b } Object.assign(rgba, hsb2rgb(hsb)) this.#hsb = hsb this.#rgba = rgba this.#updateView() } // #changeColor(x, y) { let hsb = this.#hsb let rgba = this.#rgba hsb.s = ~~((100 * x) / 280) hsb.b = ~~((100 * (180 - y)) / 180) Object.assign(rgba, hsb2rgb(hsb)) this.#updateView() } #sceneMousedown(ev) { let { x, y } = ev let { left, top } = offset(ev.currentTarget) let { scrollLeft, scrollTop } = ROOT let _x = left - scrollLeft let _y = top - scrollTop this.#changeColor(x - _x, y - _y) let callback = bind(DOC, 'mousemove', ({ x, y }) => { x -= _x y -= _y x = x < 0 ? 0 : x > 280 ? 280 : x y = y < 0 ? 0 : y > 180 ? 180 : y this.#changeColor(x, y) }) bind(DOC, 'mouseup', _ => unbind(DOC, 'mousemove', callback), EV_OPTION) } #poolMousedown(ev) { let { y } = ev let { top } = offset(ev.currentTarget) let { scrollTop } = ROOT let { clientHeight: h } = ev.currentTarget let _y = top - scrollTop y -= _y this.#changeHue(~~((y / h) * 360)) let callback = bind(DOC, 'mousemove', ({ y }) => { y -= _y this.#changeHue(~~((y / h) * 360)) }) bind(DOC, 'mouseup', _ => unbind(DOC, 'mousemove', callback), EV_OPTION) } #close() { if (this.#show) { this.#show = false this.#calc(this.value) this.#updateView() } } #submit() { this.value = this.#value this.#close() this.$emit('input') this.$emit('change') } #updateView() { let hsb = this.#hsb let rgba = this.#rgba let sceneBg, color, alphaBg let x, y x = Math.ceil((hsb.s * 280) / 100) y = 180 - Math.ceil((hsb.b * 180) / 100) sceneBg = '#' + rgb2hex(hsb2rgb({ h: hsb.h, s: 100, b: 100 })) alphaBg = '#' + rgb2hex(rgba) if (rgba.a < 100) { color = `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a / 100})` } else { color = alphaBg } this.#sceneBg = sceneBg this.#alphaBg = alphaBg this.#value = color this.#x = x this.#y = y this.#ht = hsb.h / 2 this.#at = rgba.a this.$requestUpdate() } mounted() { // 更新一次视图 this.#updateView() // 点击外部区别时,还原之前的颜色值 this._outsideFn = outsideClick(this, ev => { this.#close() }) } unmounted() { clearOutsideClick(this._outsideFn) } render() { let styles = styleMap({ '--show': this.#show ? 'block' : 'none', '--value': this.#value, '--scene': this.#sceneBg, '--x': this.#x + 'px', '--y': this.#y + 'px', '--ht': this.#ht + 'px', '--at': this.#at + '%', '--alpha': this.#alphaBg }) return html`
` } } Color.reg('color')