ui/src/form/color.js

639 lines
14 KiB
JavaScript
Raw Normal View History

2023-03-23 19:42:15 +08:00
/**
2023-03-27 19:34:30 +08:00
* {color组件}
2023-03-23 19:42:15 +08:00
* @author chensbox <chensbox@foxmail.com>
* @date 2023/03/20 15:17:25
*/
import {
css,
html,
bind,
unbind,
Component,
outsideClick,
2023-03-27 10:26:57 +08:00
clearOutsideClick,
2023-03-23 19:42:15 +08:00
nextTick,
2023-11-24 18:38:06 +08:00
offset,
styleMap
2023-05-25 17:00:17 +08:00
} from 'wkit'
2023-03-23 19:42:15 +08:00
2023-11-24 18:38:06 +08:00
const EV_OPTION = { once: true }
const DOC = document
const ROOT = document.documentElement
2023-03-23 19:42:15 +08:00
// 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) {
2023-11-24 18:38:06 +08:00
if (this.#show) {
2023-03-27 10:26:57 +08:00
this.#calc(val)
2023-03-23 19:42:15 +08:00
}
}
},
2023-11-24 18:38:06 +08:00
disabled: false
2023-03-23 19:42:15 +08:00
}
static styles = [
css`
:host {
display: inline-flex;
}
2023-11-24 18:38:06 +08:00
.container {
2023-03-23 19:42:15 +08:00
position: relative;
2023-11-24 18:38:06 +08:00
width: 32px;
height: 32px;
2023-03-23 19:42:15 +08:00
}
.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%;
2023-11-24 18:38:06 +08:00
border: 1px solid var(--color-grey-2);
border-radius: 3px;
2023-03-23 19:42:15 +08:00
cursor: pointer;
transition: box-shadow 0.15s linear;
span {
width: 100%;
height: 100%;
border: 3px solid #fff;
2023-11-24 18:38:06 +08:00
border-radius: 3px;
background: var(--value);
2023-03-23 19:42:15 +08:00
outline: none;
}
&:focus-within {
box-shadow: 0 0 0 2px var(--color-plain-a);
}
}
`,
// .color-panel
css`
.color-panel {
2023-11-24 18:38:06 +08:00
display: var(--show, none);
2023-03-23 19:42:15 +08:00
position: absolute;
2023-11-24 18:38:06 +08:00
z-index: 1;
2023-03-23 19:42:15 +08:00
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;
2023-11-24 18:38:06 +08:00
background: linear-gradient(180deg, transparent, #000),
linear-gradient(90deg, #fff, transparent), var(--scene);
2023-03-23 19:42:15 +08:00
.thumb {
position: absolute;
2023-11-24 18:38:06 +08:00
z-index: 1;
left: var(--x);
top: var(--y);
2023-03-23 19:42:15 +08:00
width: 0;
height: 0;
&::after {
display: block;
width: 10px;
height: 10px;
2023-11-24 18:38:06 +08:00
border: 1px solid #fff;
2023-03-23 19:42:15 +08:00
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;
2023-11-24 18:38:06 +08:00
top: var(--ht);
2023-03-23 19:42:15 +08:00
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;
2023-11-24 18:38:06 +08:00
background: linear-gradient(90deg, transparent, var(--alpha));
2023-03-23 19:42:15 +08:00
}
.thumb {
position: absolute;
2023-11-24 18:38:06 +08:00
left: var(--at);
2023-03-23 19:42:15 +08:00
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);
}
}
}
2023-11-24 18:38:06 +08:00
`,
css`
:host([disabled]) {
opacity: 0.6;
.preview {
cursor: not-allowed;
&:focus-within {
box-shadow: unset;
}
}
}
2023-03-23 19:42:15 +08:00
`
]
2023-11-24 18:38:06 +08:00
#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 }
}
2023-03-27 10:26:57 +08:00
#calc(val) {
var isHex
var rgb
val = val.toLowerCase()
if (!val) {
return
}
isHex = /^#[0-9a-f]{3,6}$/.test(val)
if (isHex) {
2023-11-24 18:38:06 +08:00
Object.assign(this.#cache.rgba, hex2rgb(val), { a: 100 })
2023-03-27 10:26:57 +08:00
} else {
var res = val.match(/rgba?\((\d+),\s*?(\d+),\s*?(\d+)[,\s]*?([\d\.]+)?\)/)
if (res) {
2023-11-24 18:38:06 +08:00
this.#cache.rgba = { r: +res[1], g: +res[2], b: +res[3], a: 100 }
2023-03-27 10:26:57 +08:00
if (res[4] !== undefined) {
2023-11-24 18:38:06 +08:00
this.#cache.rgba.a = ~~(res[4] * 100)
2023-03-27 10:26:57 +08:00
}
} else {
return
}
}
2023-11-24 18:38:06 +08:00
this.#cache.hsb = rgb2hsb(this.#cache.rgba)
2023-03-27 10:26:57 +08:00
}
2023-03-23 19:42:15 +08:00
toggleColorPanel() {
2023-11-24 18:38:06 +08:00
if (this.disabled) {
return
}
this.#show = true
2023-03-27 10:26:57 +08:00
this.#updateView()
2023-03-23 19:42:15 +08:00
}
// 透明度变化
2023-11-24 18:38:06 +08:00
#changeAlpha(ev) {
let a = +ev.target.value
let { r, g, b } = this.#cache.rgba
2023-03-23 19:42:15 +08:00
2023-11-24 18:38:06 +08:00
this.#cache.rgba.a = a
2023-03-23 19:42:15 +08:00
this.#updateView()
}
// 色彩池变化
2023-11-24 18:38:06 +08:00
#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 }
2023-03-23 19:42:15 +08:00
Object.assign(rgba, hsb2rgb(hsb))
2023-11-24 18:38:06 +08:00
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))
2023-03-23 19:42:15 +08:00
2023-11-24 18:38:06 +08:00
this.#cache.hsb = hsb
this.#cache.rgba = rgba
2023-03-23 19:42:15 +08:00
this.#updateView()
}
2023-11-24 18:38:06 +08:00
#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 })
}
2023-03-23 19:42:15 +08:00
#updateView() {
2023-11-24 18:38:06 +08:00
var { hsb, rgba } = this.#cache
2023-03-23 19:42:15 +08:00
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() {
// 更新一次视图
2023-11-24 18:38:06 +08:00
this.#updateView()
2023-03-23 19:42:15 +08:00
// 点击外部区别时,还原之前的颜色值
2023-11-24 18:38:06 +08:00
this._outsideFn = outsideClick(this, ev => {
this.#close()
2023-03-23 19:42:15 +08:00
})
}
2023-03-27 10:26:57 +08:00
unmounted() {
clearOutsideClick(this._outsideFn)
}
2023-03-23 19:42:15 +08:00
render() {
2023-11-24 18:38:06 +08:00
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
})
2023-03-23 19:42:15 +08:00
return html`
2023-11-24 18:38:06 +08:00
<main class="container" style=${styles}>
2023-03-23 19:42:15 +08:00
<section class="preview alpha-bg" @click=${this.toggleColorPanel}>
2023-11-24 18:38:06 +08:00
<span tabindex=${this.disabled ? -1 : 0}></span>
2023-03-23 19:42:15 +08:00
</section>
2023-11-24 18:38:06 +08:00
<div class="color-panel">
2023-03-23 19:42:15 +08:00
<section class="dashboard">
2023-11-24 18:38:06 +08:00
<div class="scene" @mousedown.prevent=${this.#sceneMousedown}>
<i class="thumb"></i>
2023-03-23 19:42:15 +08:00
</div>
2023-11-24 18:38:06 +08:00
<div class="pool" @mousedown.prevent=${this.#poolMousedown}>
<i class="thumb"></i>
2023-03-23 19:42:15 +08:00
</div>
</section>
2023-11-24 18:38:06 +08:00
2023-03-23 19:42:15 +08:00
<section class="alpha-box alpha-bg">
2023-11-24 18:38:06 +08:00
<div class="bar"></div>
<i class="thumb"></i>
2023-03-23 19:42:15 +08:00
<input
class="alpha"
max="100"
min="0"
step="1"
type="range"
2023-11-24 18:38:06 +08:00
@input=${this.#changeAlpha}
2023-03-23 19:42:15 +08:00
/>
</section>
2023-11-24 18:38:06 +08:00
2023-03-23 19:42:15 +08:00
<section class="input-box">
2023-11-24 18:38:06 +08:00
<input class="input" :value=${this.#value} maxlength="25" />
<a class="clear" @click=${_ => this.#close(true)}>清除</a>
<a tabindex="0" class="submit" @click=${this.#submit}>确定</a>
2023-03-23 19:42:15 +08:00
</section>
</div>
2023-11-24 18:38:06 +08:00
</main>
2023-03-23 19:42:15 +08:00
`
}
}
Color.reg('color')