wip(color-picker): cache up-coming value to avoid shifting of pallete cursor

This commit is contained in:
07akioni 2021-04-12 22:02:21 +08:00
parent acec0c0229
commit 7617b51d16
8 changed files with 192 additions and 100 deletions

View File

@ -35,8 +35,9 @@ function useAdjustedTo (
// teleport disabled key
useAdjustedTo.tdkey = teleportDisabled
useAdjustedTo.propTo = [String, Object, Boolean] as PropType<
HTMLElement | string | boolean
>
useAdjustedTo.propTo = {
type: [String, Object, Boolean] as PropType<HTMLElement | string | boolean>,
default: undefined
}
export { useAdjustedTo }

View File

@ -6,7 +6,9 @@ import {
toRgbaString,
toHslaString
} from 'seemly'
import { h, defineComponent, PropType } from 'vue'
import { h, defineComponent, PropType, Fragment } from 'vue'
import { NInputGroup } from '../../input'
import { NSelect } from '../../select'
import ColorInputUnit from './ColorInputUnit'
import type { ColorPickerMode } from './utils'
@ -32,6 +34,20 @@ export default defineComponent({
},
setup (props) {
return {
options: [
{
label: 'rgba',
value: 'rgba'
},
{
label: 'hsla',
value: 'hsla'
},
{
label: 'hsva',
value: 'hsva'
}
],
handleUnitUpdateValue (index: number, value: number) {
let nextValueArr: any
if (props.value === null) {
@ -60,26 +76,29 @@ export default defineComponent({
const { value } = this
return (
<div class="n-color-input">
{this.mode}
<select
value={this.mode}
onChange={(e) => {
this.onUpdateMode((e.target as any).value)
<NInputGroup>
{{
default: () => (
<>
<NSelect
size="small"
value={this.mode}
options={this.options}
onUpdateValue={this.onUpdateMode as (value: string) => void}
/>
{this.mode.split('').map((v, i) => (
<ColorInputUnit
label={v.toUpperCase()}
value={value === null ? null : value[i]}
onUpdateValue={(unitValue) => {
this.handleUnitUpdateValue(i, unitValue)
}}
/>
))}
</>
)
}}
>
<option value="rgba">rgba</option>
<option value="hsla">hsla</option>
<option value="hsva">hsva</option>
</select>
{this.mode.split('').map((v, i) => (
<ColorInputUnit
label={v.toUpperCase()}
value={value === null ? null : value[i]}
onUpdateValue={(unitValue) => {
this.handleUnitUpdateValue(i, unitValue)
}}
/>
))}
</NInputGroup>
</div>
)
}

View File

@ -110,14 +110,13 @@ export default defineComponent({
},
render () {
return (
<div class="n-color-input__unit">
<NInput
value={this.inputValue}
onUpdateValue={this.handleInputUpdateValue}
onChange={this.handleInputChange}
/>
{this.label}
</div>
<NInput
size="small"
placeholder=""
value={this.inputValue}
onUpdateValue={this.handleInputUpdateValue}
onChange={this.handleInputChange}
/>
)
}
})

View File

@ -1,12 +1,4 @@
import {
h,
defineComponent,
ref,
computed,
PropType,
toRef,
ComputedRef
} from 'vue'
import { h, defineComponent, ref, computed, PropType, toRef } from 'vue'
import {
hsv2rgb,
rgb2hsv,
@ -16,13 +8,15 @@ import {
hsl2hsv,
hsv2hsl,
rgb2hsl,
hsl2rgb,
toRgbaString,
toHsvaString,
toHslaString,
toRgbString,
HSVA,
RGBA,
HSLA
HSLA,
toHslString
} from 'seemly'
import HueSlider from './HueSlider'
import Pallete from './Pallete'
@ -104,7 +98,7 @@ export default defineComponent({
return [...hsv2rgb(h, s, v), a]
case 'hsla':
;[h, s, l, a] = hsla(mergedValue)
return [...hsl2hsv(h, s, l), a]
return [...hsl2rgb(h, s, l), a]
}
})
@ -137,50 +131,80 @@ export default defineComponent({
})
const uncontrolledHueRef = ref<number>(0)
const displayedHueRef: ComputedRef<number> = computed(() => {
if (valueModeRef.value === 'rgba') {
const hash = getRgbString(rgbaRef.value)
const shouldFollowMouseRef = computed(() => {
const { value: valueMode } = valueModeRef
console.log('cachedRgbStringRef.value', cachedRgbStringRef.value)
console.log('cachedHslStringRef.value', cachedHslStringRef.value)
if (valueMode === null) return true
if (valueMode === 'rgba') {
const { value } = rgbaRef
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (hash) {
if (hash === cachedRgbStringRef.value) {
return cachedHueRef.value
}
if (value && cachedRgbStringRef.value === toRgbString(value)) {
return true
}
} else if (valueMode === 'hsla') {
const { value } = hslaRef
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (value && cachedHslStringRef.value === toHslString(value)) {
return true
}
}
if (hsvaRef.value) return hsvaRef.value[0]
return false
})
const displayedHueRef = computed(() => {
if (shouldFollowMouseRef.value) {
return cachedHueRef.value
}
const { value } = hsvaRef
if (value) return value[0]
return uncontrolledHueRef.value
})
function getRgbString (rgba: null): null
function getRgbString (rgba: RGBA): string
function getRgbString (rgba: RGBA | null): null | string
function getRgbString (rgba: RGBA | null): null | string {
if (!rgba) return null
return toRgbString(rgba)
}
const cachedRgbStringRef = ref(
rgbaRef.value ? toRgbString(rgbaRef.value) : null
)
const cachedHueRef = ref(displayedHueRef.value)
// If you move in pallete, which means s, v in hsv is updated
// In hsv mode, everthing is fine. Howerer in hsl & rgb mode, the controlled
// value's change outside the component may cause cursor shifting!
//
// Before mousemove, everything is ok.
// During mousemove, I think the position should follows cursor if the
// controlled value outside is really the expected value.
//
// props.value rgba => hsv(old cursor position) => new hsv => new rgba (cache)
// props.value new rgba => hsv(new cursor postion)
// If new rgba is the same as the cached new rgba, the cursor should follow
// mouse but not the new hsv value
// Also the hue slider will be influenced by rgba value.
// For hsl mode, keep the same way too.
const { value: initRgba } = rgbaRef
const { value: initHsla } = hslaRef
const { value: initHsva } = hsvaRef
const cachedRgbStringRef = ref(initRgba ? toRgbString(initRgba) : null)
const cachedHslStringRef = ref(initHsla ? toHslString(initHsla) : null)
const cachedHueRef = ref(initHsva?.[0] || uncontrolledHueRef.value)
function handleUpdateSv (s: number, v: number): void {
const { value: hsvaArr } = hsvaRef
const hue = displayedHueRef.value
const alpha = hsvaArr ? hsvaArr[3] : 1
let nextRgba: RGBA
let nextHsla: HSLA
let nextRgbaString: string
let nextHslaString: string
switch (displayedModeRef.value) {
case 'hsva':
doUpdateValue(toHsvaString([hue, s, v, alpha]))
break
case 'hsla':
doUpdateValue(toHslaString([...hsv2hsl(hue, s, v), alpha]))
nextHsla = [...hsv2hsl(hue, s, v), alpha]
nextHslaString = toHslaString(nextHsla)
cachedHslStringRef.value = toHslString(nextHsla)
cachedHueRef.value = displayedHueRef.value
doUpdateValue(nextHslaString)
break
case 'rgba':
nextRgba = [...hsv2rgb(hue, s, v), alpha]
nextRgbaString = toRgbaString([...hsv2rgb(hue, s, v), alpha])
cachedRgbStringRef.value = toRgbString(nextRgba)
cachedHueRef.value = hue
cachedHueRef.value = displayedHueRef.value
doUpdateValue(nextRgbaString)
break
}
@ -201,7 +225,7 @@ export default defineComponent({
doUpdateValue(toRgbaString([...hsv2rgb(hue, s, v), a]))
break
case 'hsla':
doUpdateValue(toRgbaString([...hsv2hsl(hue, s, v), a]))
doUpdateValue(toHslaString([...hsv2hsl(hue, s, v), a]))
break
}
}
@ -220,6 +244,8 @@ export default defineComponent({
return {
mergedValue: mergedValueRef, // debug
hsva: hsvaRef,
rgba: rgbaRef,
shouldFollowMouse: shouldFollowMouseRef,
displayedHue: displayedHueRef,
displayedMode: displayedModeRef,
mergedValueArr: mergedValueArrRef,
@ -231,20 +257,32 @@ export default defineComponent({
},
render () {
return (
<div>
<div
class="n-color-picker-panel"
style={{
width: '180px'
}}
>
<div>value: {this.mergedValue}</div>
<Pallete
hsva={this.hsva}
rgba={this.rgba}
shouldFollowMouse={this.shouldFollowMouse}
displayedHue={this.displayedHue}
onUpdateSV={this.handleUpdateSv}
/>
<HueSlider hue={this.displayedHue} onUpdateHue={this.handleUpdateHue} />
<ColorInput
mode={this.displayedMode}
onUpdateMode={this.handleUpdateDisplayedMode}
value={this.mergedValueArr}
onUpdateValue={this.handleInputUpdateValue}
/>
<div class="n-color-picker-control">
<HueSlider
hue={this.displayedHue}
onUpdateHue={this.handleUpdateHue}
/>
<ColorInput
mode={this.displayedMode}
onUpdateMode={this.handleUpdateDisplayedMode}
value={this.mergedValueArr}
onUpdateValue={this.handleInputUpdateValue}
/>
</div>
</div>
)
}

View File

@ -33,7 +33,7 @@ export default defineComponent({
if (!railEl) return
const { width, left } = railEl.getBoundingClientRect()
const newHue = Math.floor(((e.clientX - left) / width) * 360)
props.onUpdateHue(newHue > 360 ? 360 : newHue < 0 ? 0 : newHue)
props.onUpdateHue(newHue >= 360 ? 359 : newHue < 0 ? 0 : newHue)
}
function handleMouseUp (): void {
off('mousemove', document, handleMouseMove)
@ -46,10 +46,17 @@ export default defineComponent({
},
render () {
return (
<div class="n-hue-slider">
<div
class="n-hue-slider"
style={{
marginBottom: '8px'
}}
>
<div
ref="railRef"
style={{
boxShadow: 'inset 0 0 2px 0 rgba(0, 0, 0, .24)',
boxSizing: 'border-box',
backgroundImage: GRADIENT,
height: HANDLE_SIZE,
borderRadius: RADIUS,
@ -72,10 +79,10 @@ export default defineComponent({
style={{
userSelect: 'none',
position: 'absolute',
top: 0,
left: `calc((${this.hue}%) / 18 * 5 - ${RADIUS})`,
boxShadow: 'rgb(0 0 0 / 20%) 0px 0px 0px 1px',
left: `calc((${this.hue}%) / 359 * 100 - ${RADIUS})`,
boxSizing: 'border-box',
border: '2px solid black',
border: '2px solid white',
backgroundColor: `hsl(${this.hue}, 100%, 50%)`,
borderRadius: RADIUS,
width: HANDLE_SIZE,

View File

@ -13,11 +13,16 @@ export default defineComponent({
type: (Array as unknown) as PropType<HSVA | null>,
default: null
},
rgba: {
type: (Array as unknown) as PropType<HSVA | null>,
default: null
},
// 0 - 360
displayedHue: {
type: Number,
required: true
},
shouldFollowMouse: Boolean,
onUpdateSV: {
type: Function as PropType<(s: number, v: number) => void>,
required: true
@ -25,6 +30,12 @@ export default defineComponent({
},
setup (props) {
const palleteRef = ref<HTMLElement | null>(null)
const cachedLeftRef = ref('0')
const cachedBottomRef = ref('0')
function derivePosition (newS: number, newV: number): void {
cachedLeftRef.value = `calc(${newS}% - ${RADIUS})`
cachedBottomRef.value = `calc(${newV}% - ${RADIUS})`
}
function handleMouseDown (e: MouseEvent): void {
if (!palleteRef.value) return
on('mousemove', document, handleMouseMove)
@ -37,11 +48,10 @@ export default defineComponent({
const { width, height, left, bottom } = palleteEl.getBoundingClientRect()
const newV = (bottom - e.clientY) / height
const newS = (e.clientX - left) / width
props.onUpdateSV(
100 * (newS > 100 ? 1 : newS < 0 ? 0 : newS),
100 * (newV > 100 ? 1 : newV < 0 ? 0 : newV)
)
const normalizedNewS = 100 * (newS > 1 ? 1 : newS < 0 ? 0 : newS)
const normalizedNewV = 100 * (newV > 1 ? 1 : newV < 0 ? 0 : newV)
derivePosition(normalizedNewS, normalizedNewV)
props.onUpdateSV(normalizedNewS, normalizedNewV)
}
function handleMouseUp (): void {
off('mousemove', document, handleMouseMove)
@ -50,10 +60,27 @@ export default defineComponent({
return {
palleteRef,
handleColor: computed(() => {
// const [r, g, b] = hsv2rgb(props.hue, props.s, props.v)
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
// return `rgb(${r}, ${g}, ${b})`
return 'transparent'
const { rgba } = props
if (!rgba) return ''
return `rgb(${rgba[0]}, ${rgba[1]}, ${rgba[2]})`
}),
position: computed(() => {
if (props.shouldFollowMouse) {
return {
left: cachedLeftRef.value,
bottom: cachedBottomRef.value
}
}
if (!props.hsva) {
return {
left: '0',
bottom: '0'
}
}
return {
left: `calc(${props.hsva[1]}% - ${RADIUS})`,
bottom: `calc(${props.hsva[2]}% - ${RADIUS})`
}
}),
handleMouseDown
}
@ -61,7 +88,7 @@ export default defineComponent({
render () {
return (
<div
style="height: 300px; position: relative;"
style="height: 180px; position: relative; margin-bottom: 8px;"
onMousedown={this.handleMouseDown}
ref="palleteRef"
>
@ -85,7 +112,8 @@ export default defineComponent({
top: 0,
bottom: 0,
backgroundImage:
'linear-gradient(180deg, rgba(0, 0, 0, 0%), rgba(0, 0, 0, 100%))'
'linear-gradient(180deg, rgba(0, 0, 0, 0%), rgba(0, 0, 0, 100%))',
boxShadow: 'inset 0 0 2px 0 rgba(0, 0, 0, .24)'
}}
/>
{this.hsva && (
@ -99,10 +127,9 @@ export default defineComponent({
boxSizing: 'border-box',
border: '2px solid white',
position: 'absolute',
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
left: `calc(${this.hsva[1]}% - ${RADIUS})`,
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
bottom: `calc(${this.hsva[2]}% - ${RADIUS})`
boxShadow: 'rgb(0 0 0 / 20%) 0px 0px 0px 1px',
left: this.position.left,
bottom: this.position.bottom
}}
/>
)}

View File

@ -1,6 +1,9 @@
import { c, cB, cE } from '../../../_utils/cssr'
export default c([
cB('color-picker-panel', `
padding: 8px;
`),
cB('color-input', `
display: flex;
`, [

View File

@ -40,8 +40,10 @@ export interface SelectIgnoredOption {
export type ValueAtom = string | number
export type Value = ValueAtom | string[] | number[] | ValueAtom[]
export type OnUpdateValue = <
T extends ValueAtom &
export type OnUpdateValue = (
value: string &
number &
ValueAtom &
string[] &
number[] &
ValueAtom[] &
@ -49,11 +51,9 @@ export type OnUpdateValue = <
(string[] | null) &
(number[] | null) &
(ValueAtom[] | null)
>(
value: T
) => void
export type OnUpdateValueImpl = <
T extends
export type OnUpdateValueImpl = (
value:
| ValueAtom
| string[]
| number[]
@ -62,8 +62,6 @@ export type OnUpdateValueImpl = <
| (string[] | null)
| (number[] | null)
| (ValueAtom[] | null)
>(
value: T
) => void
export type SelectTreeMate = TreeMate<
SelectBaseOption,