diff --git a/src/color/index.js b/src/color/index.js new file mode 100644 index 0000000..5de96c0 --- /dev/null +++ b/src/color/index.js @@ -0,0 +1,630 @@ +/** + * {滚动条组件} + * @author chensbox + * @date 2023/03/20 15:17:25 + */ + +import { + css, + html, + bind, + unbind, + Component, + outsideClick, + nextTick, + offset +} from '@bd/core' + +// 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) { + 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) + // this.cache.alphaColor = '#' + rgb2hex(this.cache.rgba) + // this.cache.scene = '#' + rgb2hex(hsb2rgb(this.cache.hsb)) + } + }, + 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; + z-index: 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; + z-index: 99; + 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; + z-index: 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; + z-index: 1; + 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; + z-index: 0; + width: 100%; + height: 12px; + background: linear-gradient(to right, transparent, #f00); + } + + .thumb { + position: absolute; + left: 100%; + top: 0; + z-index: 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; + z-index: 9; + 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); + } + } + } + ` + ] + + toggleColorPanel() { + this.$refs.panel.style.display = 'block' + this.temp = this.value + // 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('temp')) { + this.$refs.panel.style.display = '' + this.value = this.temp + delete this.temp + } + } + + mounted() { + var handleMove + + // 更新一次视图 + nextTick(_ => this.#updateView()) + + this.handleDown = 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() + }) + } + + render() { + return html` +
+
+ +
+
+
+
+ +
+
+ + +
+
+
+
+ + +
+
+ + 清除 + 确定 +
+
+
+ ` + } +} + +Color.reg('color') diff --git a/src/scroll/index.js b/src/scroll/index.js index 038be57..12f873b 100644 --- a/src/scroll/index.js +++ b/src/scroll/index.js @@ -396,6 +396,9 @@ class Scroll extends Component { } unmounted() { this.__observer.disconnect() + unbind(this.$refs.box, 'scroll', this._scrollFn) + unbind(this.$refs.y, 'mousedown', this._yBarFn) + unbind(this.$refs.x, 'mousedown', this._xBarFn) } render() {