/** * {color组件} * @author chensbox * @date 2023/03/20 15:17:25 */ import { css, html, bind, unbind, Component, outsideClick, clearOutsideClick, nextTick, offset } from 'wkit' // 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: '', attribute: false, observer(val) { if (!this.panelShow) { this.#calc(val) } } }, disabled: false, readonly: 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 } } static styles = [ css` :host { display: inline-flex; } .color-picker { position: relative; width: 36px; height: 36px; } .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: 2px solid var(--color-plain-2); border-radius: 6px; cursor: pointer; transition: box-shadow 0.15s linear; span { width: 100%; height: 100%; border: 3px solid #fff; border-radius: 6px; outline: none; } &:focus-within { box-shadow: 0 0 0 2px var(--color-plain-a); } } `, // .color-panel css` .color-panel { display: none; position: absolute; 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: #f00; &::before, &::after { position: absolute; left: 0; top: 0; width: 100%; height: 100%; content: ''; } &::before { background: linear-gradient(to right, #fff, transparent); } &::after { background: linear-gradient(to bottom, transparent, #000); } .thumb { position: absolute; width: 0; height: 0; &::after { display: block; width: 10px; height: 10px; border-radius: 50%; background: rgba(32, 32, 32, 0.3); box-shadow: 0 0 0 1px #fff; 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: 0; 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: ''; } } .hue { position: relative; display: block; width: 12px; height: 180px; appearance: slider-vertical; opacity: 0; } } } .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(to right, transparent, #f00); } .thumb { position: absolute; left: 100%; 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); } } } ` ] #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() { this.$refs.panel.style.display = 'block' this.panelShow = true this.#updateView() } // 透明度变化 alphaChange(ev) { var a = ev.target.value var { r, g, b } = this.cache.rgba this.cache.rgba.a = a this.#updateView() } // 色彩池变化 hueChange(ev) { var h = 360 - ev.target.value var { s, b } = this.cache.hsb var rgba = this.cache.rgba var hsb = { h, s, b } Object.assign(rgba, hsb2rgb(hsb)) this.cache.hsb = hsb this.cache.rgba = rgba this.#updateView() } #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() } #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() } closePanel() { if (this.hasOwnProperty('panelShow')) { this.$refs.panel.style.display = '' this.#calc(this.value) this.#updateView() delete this.panelShow } } submit() { this.$refs.panel.style.display = '' this.value = this.#value this.$emit('change', { data: this.#value }) delete this.panelShow } mounted() { var handleMove // 更新一次视图 nextTick(_ => this.#updateView()) bind(this.$refs.scene, 'mousedown', ev => { ev.preventDefault() var { pageX, pageY } = ev var { left, top } = offset(this.$refs.scene) var x = pageX - left var y = pageY - top this.#changeColor(x, y) handleMove = bind(document, 'mousemove', ev => { ev.preventDefault() var { pageX, pageY } = ev var x = pageX - left var y = pageY - top var rgb x = x < 0 ? 0 : x > 280 ? 280 : x y = y < 0 ? 0 : y > 180 ? 180 : y this.#changeColor(x, y) }) }) this.handleUp = bind(document, 'mouseup', ev => { ev.preventDefault() unbind(document, 'mousemove', handleMove) }) // 点击外部区别时,还原之前的颜色值 this._outsideFn = outsideClick(this.$refs.panel, ev => { this.closePanel() }) } unmounted() { clearOutsideClick(this._outsideFn) unbind(document, 'mouseup', this.handleUp) } render() { return html`
` } } Color.reg('color')