/** * {color组件} * @author chensbox * @date 2023/03/20 15:17:25 */ import { css, html, bind, unbind, Component, outsideClick, clearOutsideClick, nextTick, offset, styleMap } from 'wkit' const EV_OPTION = { once: true } const DOC = document const ROOT = document.documentElement // H: 色相, S: 饱和度, B/V: 亮度 export function hsb2rgb(hsb) { var h = hsb.h var s = Math.round((hsb.s * 255) / 100) var v = Math.round((hsb.b * 255) / 100) var r = 0 var g = 0 var b = 0 if (s === 0) { r = g = b = v } else { var t1 = v var t2 = ((255 - s) * v) / 255 var 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 }) { return [r, g, b].map(it => it.toString(16).padStart(2, '0')).join('') } export function hex2rgb(hex) { var r, g, b hex = hex.replace(/^#/, '').split('') if (hex.length === 3) { r = parseInt(hex[0] + hex[0], 16) g = parseInt(hex[1] + hex[1], 16) b = parseInt(hex[2] + hex[2], 16) } else { r = parseInt(hex[0] + hex[1], 16) g = parseInt(hex[2] + hex[3], 16) b = parseInt(hex[4] + hex[5], 16) } return { r, g, b } } export function rgb2hsb({ r, g, b }) { var hsb = { h: 0, s: 0, b: 0 } var max = Math.max(r, g, b) var min = Math.min(r, g, b) var 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) { if (this.#show) { this.#calc(val) } } }, disabled: false } static styles = [ css` :host { display: inline-flex; } .container { position: relative; width: 32px; height: 32px; } .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; } `, // 预览 css` .preview { display: flex; width: 100%; height: 100%; border: 1px solid var(--color-grey-2); border-radius: 3px; cursor: pointer; transition: box-shadow 0.15s linear; span { width: 100%; height: 100%; border: 3px solid #fff; border-radius: 3px; background: var(--value); outline: none; } &:focus-within { box-shadow: 0 0 0 2px var(--color-plain-a); } } `, // .color-panel css` .color-panel { display: var(--show, none); position: absolute; z-index: 1; left: 0; top: 38px; width: 310px; padding: 5px; background: #fff; box-shadow: 0 0 20px rgba(0, 0, 0, 0.15); } .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-radius: 50%; background: #fff; box-shadow: 0 0 3px rgba(0, 0, 0, 0.5); 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-radius: 50%; background: #fff; box-shadow: 0 0 3px rgba(0, 0, 0, 0.5); 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; .input { width: 200px; height: 24px; padding: 0 6px; line-height: 22px; font: inherit; font-size: 12px; border: 2px solid var(--color-plain-2); border-radius: 4px; outline: none; color: var(--color-dark-1); transition: box-shadow 0.15s linear; &::placeholder { color: var(--color-grey-1); } &:focus { box-shadow: 0 0 0 2px var(--color-plain-a); } } .clear, .submit { font-size: 12px; cursor: pointer; user-select: none; } .clear { color: var(--color-grey-3); } .submit { padding: 2px 6px; border-radius: 2px; color: var(--color-plain-1); background: var(--color-teal-2); outline: none; transition: box-shadow 0.15s linear, background 0.15s linear; &:hover { background: var(--color-teal-1); } &:focus { box-shadow: 0 0 0 2px var(--color-teal-a); } } } `, 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 = '#ff0000' #alphaBg = '#ff0000' #cache = { hsb: { h: 0, s: 100, b: 100 }, rgba: { r: 255, g: 0, b: 0, a: 100 } } #calc(val) { var isHex var rgb val = val.toLowerCase() if (!val) { return } isHex = /^#[0-9a-f]{3,6}$/.test(val) if (isHex) { Object.assign(this.#cache.rgba, hex2rgb(val), { a: 100 }) } else { var res = val.match(/rgba?\((\d+),\s*?(\d+),\s*?(\d+)[,\s]*?([\d\.]+)?\)/) if (res) { this.#cache.rgba = { r: +res[1], g: +res[2], b: +res[3], a: 100 } if (res[4] !== undefined) { this.#cache.rgba.a = ~~(res[4] * 100) } } else { return } } this.#cache.hsb = rgb2hsb(this.#cache.rgba) } toggleColorPanel() { if (this.disabled) { return } this.#show = true this.#updateView() } // 透明度变化 #changeAlpha(ev) { let a = +ev.target.value let { r, g, b } = this.#cache.rgba this.#cache.rgba.a = a this.#updateView() } // 色彩池变化 #changeHue(h) { h = h < 0 ? 0 : h > 360 ? 360 : h let { s, b } = this.#cache.hsb let rgba = this.#cache.rgba let hsb = { h, s, b } Object.assign(rgba, hsb2rgb(hsb)) this.#cache.hsb = hsb this.#cache.rgba = rgba this.#updateView() } // #changeColor(x, y) { let { hsb, rgba } = this.#cache hsb.s = ~~((100 * x) / 280) hsb.b = ~~((100 * (180 - y)) / 180) Object.assign(rgba, hsb2rgb(hsb)) this.#cache.hsb = hsb this.#cache.rgba = rgba 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(clear) { if (this.#show) { this.#show = false this.#calc(this.value) this.#updateView() } } #submit() { this.value = this.#value this.#close() this.$emit('change', { data: this.#value }) } #updateView() { var { hsb, rgba } = this.#cache var sceneBg, color, alphaBg var 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')