添加取色器组件

This commit is contained in:
pipipi-pikachu 2021-01-02 20:26:10 +08:00
parent 2186fc0d97
commit b71b78f9f1
10 changed files with 780 additions and 35 deletions

11
package-lock.json generated
View File

@ -1935,6 +1935,12 @@
"integrity": "sha1-qcpLcKGLJwzLK8Cqr+/R1Ia36nQ=",
"dev": true
},
"@types/tinycolor2": {
"version": "1.4.2",
"resolved": "https://registry.npm.taobao.org/@types/tinycolor2/download/@types/tinycolor2-1.4.2.tgz",
"integrity": "sha1-chylxdGimItKiG41wv/Fc1tq+98=",
"dev": true
},
"@types/uglify-js": {
"version": "3.11.1",
"resolved": "https://registry.npm.taobao.org/@types/uglify-js/download/@types/uglify-js-3.11.1.tgz?cache=0&sync_timestamp=1605057452755&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fuglify-js%2Fdownload%2F%40types%2Fuglify-js-3.11.1.tgz",
@ -15318,6 +15324,11 @@
"resolved": "https://registry.npm.taobao.org/tiny-emitter/download/tiny-emitter-2.1.0.tgz",
"integrity": "sha1-HRpW7fxRxD6GPLtTgqcjMONVVCM="
},
"tinycolor2": {
"version": "1.4.2",
"resolved": "https://registry.npm.taobao.org/tinycolor2/download/tinycolor2-1.4.2.tgz",
"integrity": "sha1-P2pNEHGtB2dtf6Ry4frECnGdiAM="
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npm.taobao.org/tmp/download/tmp-0.0.33.tgz",

View File

@ -28,6 +28,7 @@
"prosemirror-state": "^1.3.3",
"prosemirror-view": "^1.16.4",
"store2": "^2.12.0",
"tinycolor2": "^1.4.2",
"vue": "^3.0.0",
"vuedraggable": "^4.0.1",
"vuex": "^4.0.0-0"
@ -45,6 +46,7 @@
"@types/prosemirror-schema-basic": "^1.0.1",
"@types/prosemirror-schema-list": "^1.0.1",
"@types/resize-observer-browser": "^0.1.4",
"@types/tinycolor2": "^1.4.2",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "~4.5.0",

View File

@ -0,0 +1,131 @@
<template>
<div class="alpha">
<div class="alpha-checkboard-wrap">
<Checkboard />
</div>
<div class="alpha-gradient" :style="{ background: gradientColor }"></div>
<div
class="alpha-container"
ref="alphaRef"
@mousedown="$event => handleMouseDown($event)"
>
<div class="alpha-pointer" :style="{ left: color.a * 100 + '%' }">
<div class="alpha-picker"></div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onUnmounted, PropType, ref } from 'vue'
import Checkboard from './Checkboard.vue'
import { ColorFormats } from 'tinycolor2'
export default defineComponent({
name: 'alpha',
components: {
Checkboard,
},
props: {
modelValue: {
type: Object as PropType<ColorFormats.RGBA>,
required: true,
},
},
setup(props, { emit }) {
const color = computed(() => props.modelValue)
const gradientColor = computed(() => {
const rgbaStr = [color.value.r, color.value.g, color.value.b].join(',')
return `linear-gradient(to right, rgba(${rgbaStr}, 0) 0%, rgba(${rgbaStr}, 1) 100%)`
})
const alphaRef = ref<HTMLElement | null>(null)
const handleChange = (e: MouseEvent) => {
e.preventDefault()
if(!alphaRef.value) return
const containerWidth = alphaRef.value.clientWidth
const xOffset = alphaRef.value.getBoundingClientRect().left + window.pageXOffset
const left = e.pageX - xOffset
let a
if(left < 0) a = 0
else if(left > containerWidth) a = 1
else a = Math.round(left * 100 / containerWidth) / 100
if(color.value.a !== a) {
emit('update:modelValue', {
r: color.value.r,
g: color.value.g,
b: color.value.b,
a: a,
})
}
}
const unbindEventListeners = () => {
window.removeEventListener('mousemove', handleChange)
window.removeEventListener('mouseup', unbindEventListeners)
}
const handleMouseDown = (e: MouseEvent) => {
handleChange(e)
window.addEventListener('mousemove', handleChange)
window.addEventListener('mouseup', unbindEventListeners)
}
onUnmounted(unbindEventListeners)
return {
alphaRef,
gradientColor,
handleMouseDown,
color,
}
},
})
</script>
<style>
.alpha {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.alpha-checkboard-wrap {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
}
.alpha-gradient {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.alpha-container {
cursor: pointer;
position: relative;
z-index: 2;
height: 100%;
margin: 0 3px;
}
.alpha-pointer {
z-index: 2;
position: absolute;
}
.alpha-picker {
cursor: pointer;
width: 4px;
height: 8px;
box-shadow: 0 0 2px rgba(0, 0, 0, .6);
background: #fff;
margin-top: 1px;
transform: translateX(-2px);
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<div class="checkerboard" :style="bgStyle"></div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
const checkboardCache = {}
const renderCheckboard = (white: string, grey: string, size: number) => {
const canvas = document.createElement('canvas')
canvas.width = canvas.height = size * 2
const ctx = canvas.getContext('2d')
if(!ctx) return null
ctx.fillStyle = white
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = grey
ctx.fillRect(0, 0, size, size)
ctx.translate(size, size)
ctx.fillRect(0, 0, size, size)
return canvas.toDataURL()
}
const getCheckboard = (white: string, grey: string, size: number) => {
const key = white + ',' + grey + ',' + size
if(checkboardCache[key]) return checkboardCache[key]
const checkboard = renderCheckboard(white, grey, size)
checkboardCache[key] = checkboard
return checkboard
}
export default defineComponent({
name: 'checkboard',
props: {
size: {
type: Number,
default: 8,
},
white: {
type: String,
default: '#fff',
},
grey: {
type: String,
default: '#e6e6e6',
},
},
setup(props) {
const bgStyle = computed(() => {
const checkboard = getCheckboard(props.white, props.grey, props.size)
return { backgroundImage: `url(${checkboard})` }
})
return {
bgStyle,
}
},
})
</script>
<style>
.checkerboard {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-size: contain;
}
</style>

View File

@ -0,0 +1,63 @@
<template>
<div class="editable-input">
<input
class="input-content"
:value="val"
@input="$event => handleInput($event)"
>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
import tinycolor, { ColorFormats } from 'tinycolor2'
export default defineComponent({
name: 'editable-input',
props: {
modelValue: {
type: Object as PropType<ColorFormats.RGBA>,
required: true,
},
},
setup(props, { emit }) {
const val = computed(() => {
let _hex = ''
if(props.modelValue.a < 1) _hex = tinycolor(props.modelValue).toHex8String().toUpperCase()
else _hex = tinycolor(props.modelValue).toHexString().toUpperCase()
return _hex.replace('#', '')
})
const handleInput = (e: InputEvent) => {
const value = (e.target as HTMLInputElement).value
if(value.length >= 6) emit('update:modelValue', tinycolor(value).toRgb())
}
return {
val,
handleInput,
}
},
})
</script>
<style>
.editable-input {
width: 100%;
position: relative;
overflow: hidden;
text-align: center;
font-size: 14px;
}
.input-content {
width: 100%;
padding: 3px;
border: 0;
border-bottom: 1px solid #ddd;
outline: none;
text-align: center;
}
.input-label {
text-transform: capitalize;
}
</style>

View File

@ -0,0 +1,127 @@
<template>
<div class="hue">
<div
class="hue-container"
ref="hueRef"
@mousedown="$event => handleMouseDown($event)"
>
<div
class="hue-pointer"
:style="{ left: pointerLeft }"
>
<div class="hue-picker"></div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onUnmounted, PropType, ref, watch } from 'vue'
import tinycolor, { ColorFormats } from 'tinycolor2'
export default defineComponent({
name: 'hue',
props: {
modelValue: {
type: Object as PropType<ColorFormats.RGBA>,
required: true,
},
},
setup(props, { emit }) {
const oldHue = ref(0)
const pullDirection = ref('')
const color = computed(() => tinycolor(props.modelValue).toHsl())
const pointerLeft = computed(() => {
if(color.value.h === 0 && pullDirection.value === 'right') return '100%'
return color.value.h * 100 / 360 + '%'
})
watch(() => props.modelValue, () => {
const hsl = tinycolor(props.modelValue).toHsl()
const h = hsl.h
if(h !== 0 && h - oldHue.value > 0) pullDirection.value = 'right'
if(h !== 0 && h - oldHue.value < 0) pullDirection.value = 'left'
oldHue.value = h
})
const hueRef = ref<HTMLElement | null>(null)
const handleChange = (e: MouseEvent) => {
e.preventDefault()
if(!hueRef.value) return
const containerWidth = hueRef.value.clientWidth
const xOffset = hueRef.value.getBoundingClientRect().left + window.pageXOffset
const left = e.pageX - xOffset
let h, percent
if(left < 0) h = 0
else if(left > containerWidth) h = 360
else {
percent = left * 100 / containerWidth
h = (360 * percent / 100)
}
if(color.value.h !== h) {
const rgba = tinycolor({
h,
l: color.value.l,
s: color.value.s,
a: color.value.a,
}).toRgb()
emit('update:modelValue', rgba)
}
}
const unbindEventListeners = () => {
window.removeEventListener('mousemove', handleChange)
window.removeEventListener('mouseup', unbindEventListeners)
}
const handleMouseDown = (e: MouseEvent) => {
handleChange(e)
window.addEventListener('mousemove', handleChange)
window.addEventListener('mouseup', unbindEventListeners)
}
onUnmounted(unbindEventListeners)
return {
hueRef,
handleMouseDown,
pointerLeft,
}
},
})
</script>
<style>
.hue {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}
.hue-container {
cursor: pointer;
margin: 0 2px;
position: relative;
height: 100%;
}
.hue-pointer {
z-index: 2;
position: absolute;
top: 0;
}
.hue-picker {
cursor: pointer;
margin-top: 1px;
width: 4px;
height: 8px;
box-shadow: 0 0 2px rgba(0, 0, 0, .6);
background: #fff;
transform: translateX(-2px);
}
</style>

View File

@ -0,0 +1,122 @@
<template>
<div
class="saturation"
ref="saturationRef"
:style="{ background: bgColor }"
@mousedown="$event => handleMouseDown($event)"
>
<div class="saturation-white"></div>
<div class="saturation-black"></div>
<div class="saturation-pointer"
:style="{
top: pointerTop,
left: pointerLeft,
}"
>
<div class="saturation-circle"></div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onUnmounted, PropType, ref } from 'vue'
import tinycolor, { ColorFormats } from 'tinycolor2'
import throttle from 'lodash/throttle'
import clamp from 'lodash/clamp'
export default defineComponent({
name: 'saturation',
props: {
modelValue: {
type: Object as PropType<ColorFormats.RGBA>,
required: true,
},
},
setup(props, { emit }) {
const color = computed(() => tinycolor(props.modelValue).toHsv())
const bgColor = computed(() => `hsl(${color.value.h}, 100%, 50%)`)
const pointerTop = computed(() => (-(color.value.v * 100) + 1) + 100 + '%')
const pointerLeft = computed(() => color.value.s * 100 + '%')
const emitChangeEvent = throttle(function(param) {
emit('update:modelValue', param)
}, 20, { leading: true, trailing: false })
const saturationRef = ref<HTMLElement | null>(null)
const handleChange = (e: MouseEvent) => {
e.preventDefault()
if(!saturationRef.value) return
const containerWidth = saturationRef.value.clientWidth
const containerHeight = saturationRef.value.clientHeight
const xOffset = saturationRef.value.getBoundingClientRect().left + window.pageXOffset
const yOffset = saturationRef.value.getBoundingClientRect().top + window.pageYOffset
const left = clamp(e.pageX - xOffset, 0, containerWidth)
const top = clamp(e.pageY - yOffset, 0, containerHeight)
const saturation = left / containerWidth
const bright = clamp(-(top / containerHeight) + 1, 0, 1)
const rgba = tinycolor({
h: color.value.h,
s: saturation,
v: bright,
a: color.value.a,
}).toRgb()
emitChangeEvent(rgba)
}
const unbindEventListeners = () => {
window.removeEventListener('mousemove', handleChange)
window.removeEventListener('mouseup', unbindEventListeners)
}
const handleMouseDown = (e: MouseEvent) => {
handleChange(e)
window.addEventListener('mousemove', handleChange)
window.addEventListener('mouseup', unbindEventListeners)
}
onUnmounted(unbindEventListeners)
return {
saturationRef,
bgColor,
handleMouseDown,
pointerTop,
pointerLeft,
}
},
})
</script>
<style>
.saturation,
.saturation-white,
.saturation-black {
cursor: pointer;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.saturation-white {
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
}
.saturation-black {
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
}
.saturation-pointer {
cursor: pointer;
position: absolute;
}
.saturation-circle {
width: 4px;
height: 4px;
box-shadow: 0 0 0 1.5px #fff, inset 0 0 1px 1px rgba(0, 0, 0, .3), 0 0 1px 2px rgba(0, 0, 0, .4);
border-radius: 50%;
transform: translate(-2px, -2px);
}
</style>

View File

@ -0,0 +1,239 @@
<template>
<div class="color-picker" @contextmenu.prevent>
<div class="picker-saturation-wrap">
<Saturation v-model="color" />
</div>
<div class="picker-controls">
<div class="picker-color-wrap">
<div class="picker-current-color" :style="{ background: currentColor }"></div>
<Checkboard />
</div>
<div class="picker-sliders">
<div class="picker-hue-wrap"><Hue v-model="color" /></div>
<div class="picker-alpha-wrap"><Alpha v-model="color" /></div>
</div>
</div>
<div class="picker-field"><EditableInput v-model="color" /></div>
<div class="picker-presets">
<div
class="picker-presets-color"
v-for="c in themeColors"
:key="c"
:style="{background: c}"
@click="selectPresetColor(c)"
></div>
</div>
<div class="picker-gradient-presets">
<div
class="picker-gradient-col"
v-for="(col, index) in presetColors"
:key="index"
>
<div class="picker-gradient-color"
v-for="c in col"
:key="c"
:style="{background: c}"
@click="selectPresetColor(c)"
></div>
</div>
</div>
<div class="picker-presets">
<div
v-for="c in standardColors"
:key="c"
class="picker-presets-color"
:style="{ background: c }"
@click="selectPresetColor(c)"
></div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import tinycolor, { ColorFormats } from 'tinycolor2'
import Alpha from './Alpha.vue'
import Checkboard from './Checkboard.vue'
import Hue from './Hue.vue'
import Saturation from './Saturation.vue'
import EditableInput from './EditableInput.vue'
const presetColorConfig = [
['#7f7f7f', '#f2f2f2'],
['#0d0d0d', '#808080'],
['#1c1a10', '#ddd8c3'],
['#0e243d', '#c6d9f0'],
['#233f5e', '#dae5f0'],
['#632623', '#f2dbdb'],
['#4d602c', '#eaf1de'],
['#3f3150', '#e6e0ec'],
['#1e5867', '#d9eef3'],
['#99490f', '#fee9da'],
]
const gradient = (startColor: string, endColor: string, step: number) => {
const _startColor = tinycolor(startColor).toRgb()
const _endColor = tinycolor(endColor).toRgb()
const rStep = (_endColor.r - _startColor.r) / step
const gStep = (_endColor.g - _startColor.g) / step
const bStep = (_endColor.b - _startColor.b) / step
const gradientColorArr = []
for(let i = 0; i < step; i++) {
const gradientColor = tinycolor({
r: _startColor.r + rStep * i,
g: _startColor.g + gStep * i,
b: _startColor.b + bStep * i,
}).toRgbString()
gradientColorArr.push(gradientColor)
}
return gradientColorArr
}
const getPresetColors = () => {
const presetColors = []
for(const color of presetColorConfig) {
presetColors.push(gradient(color[1], color[0], 5))
}
return presetColors
}
export default defineComponent({
name: 'color-picker',
components: {
Alpha,
Checkboard,
Hue,
Saturation,
EditableInput,
},
props: {
modelValue: {
type: String,
default: '#e86b99',
},
},
setup(props, { emit }) {
const color = computed({
get() {
return tinycolor(props.modelValue).toRgb()
},
set(rgba: ColorFormats.RGBA) {
const rgbaString = `rgba(${[rgba.r, rgba.g, rgba.b, rgba.a].join(',')})`
emit('update:modelValue', rgbaString)
},
})
const themeColors = ['#000000', '#ffffff', '#eeece1', '#1e497b', '#4e81bb', '#e2534d', '#9aba60', '#8165a0', '#47acc5', '#f9974c']
const standardColors = ['#c21401', '#ff1e02', '#ffc12a', '#ffff3a', '#90cf5b', '#00af57', '#00afee', '#0071be', '#00215f', '#72349d']
const presetColors = getPresetColors()
const currentColor = computed(() => {
return `rgba(${[color.value.r, color.value.g, color.value.b, color.value.a].join(',')})`
})
const selectPresetColor = (colorString: string) => {
emit('update:modelValue', colorString)
}
return {
themeColors,
standardColors,
presetColors,
color,
currentColor,
selectPresetColor,
}
},
})
</script>
<style lang="scss" scoped>
.color-picker {
position: relative;
width: 240px;
background: #fff;
user-select: none;
}
.picker-saturation-wrap {
width: 100%;
padding-bottom: 50%;
position: relative;
overflow: hidden;
}
.picker-controls {
display: flex;
}
.picker-sliders {
padding: 4px 0;
flex: 1;
}
.picker-hue-wrap {
position: relative;
height: 10px;
}
.picker-alpha-wrap {
position: relative;
height: 10px;
margin-top: 4px;
overflow: hidden;
}
.picker-color-wrap {
width: 24px;
height: 24px;
position: relative;
margin-top: 4px;
margin-right: 4px;
.checkerboard {
background-size: auto;
}
}
.picker-current-color {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
}
.picker-field {
margin-bottom: 8px;
}
.picker-presets {
@include grid-layout-wrapper();
}
.picker-presets-color {
@include grid-layout-item(10, 7%);
height: 0;
padding-bottom: 7%;
flex-shrink: 0;
position: relative;
cursor: pointer;
}
.picker-gradient-presets {
@include grid-layout-wrapper();
}
.picker-gradient-col {
@include grid-layout-item(10, 7%);
display: flex;
flex-direction: column;
}
.picker-gradient-color {
width: 100%;
height: 0;
padding-bottom: 100%;
position: relative;
cursor: pointer;
}
</style>

View File

@ -1,33 +0,0 @@
<template>
<APopover trigger="click" overlayClassName="popover">
<template v-slot:content>
<slot name="content"></slot>
</template>
<slot></slot>
</APopover>
</template>
<script>
import { Popover } from 'ant-design-vue'
export default {
name: 'popover',
components: {
APopover: Popover,
},
}
</script>
<style lang="scss">
.popover {
padding-top: 6px;
.ant-popover-arrow {
display: none;
}
.ant-popover-inner {
border: 1px solid #eee;
box-shadow: 3px 3px 3px rgba(#000, 0.15);
}
}
</style>

View File

@ -1,13 +1,24 @@
<template>
<div class="slide-style-panel">
slide-style-panel
<ColorPicker v-model="color" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { defineComponent, ref } from 'vue'
import ColorPicker from '@/components/ColorPicker/index.vue'
export default defineComponent({
name: 'slide-style-panel',
components: {
ColorPicker,
},
setup() {
const color = ref('#888')
return {
color,
}
},
})
</script>