mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
添加取色器组件
This commit is contained in:
parent
2186fc0d97
commit
b71b78f9f1
11
package-lock.json
generated
11
package-lock.json
generated
@ -1935,6 +1935,12 @@
|
|||||||
"integrity": "sha1-qcpLcKGLJwzLK8Cqr+/R1Ia36nQ=",
|
"integrity": "sha1-qcpLcKGLJwzLK8Cqr+/R1Ia36nQ=",
|
||||||
"dev": true
|
"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": {
|
"@types/uglify-js": {
|
||||||
"version": "3.11.1",
|
"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",
|
"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",
|
"resolved": "https://registry.npm.taobao.org/tiny-emitter/download/tiny-emitter-2.1.0.tgz",
|
||||||
"integrity": "sha1-HRpW7fxRxD6GPLtTgqcjMONVVCM="
|
"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": {
|
"tmp": {
|
||||||
"version": "0.0.33",
|
"version": "0.0.33",
|
||||||
"resolved": "https://registry.npm.taobao.org/tmp/download/tmp-0.0.33.tgz",
|
"resolved": "https://registry.npm.taobao.org/tmp/download/tmp-0.0.33.tgz",
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"prosemirror-state": "^1.3.3",
|
"prosemirror-state": "^1.3.3",
|
||||||
"prosemirror-view": "^1.16.4",
|
"prosemirror-view": "^1.16.4",
|
||||||
"store2": "^2.12.0",
|
"store2": "^2.12.0",
|
||||||
|
"tinycolor2": "^1.4.2",
|
||||||
"vue": "^3.0.0",
|
"vue": "^3.0.0",
|
||||||
"vuedraggable": "^4.0.1",
|
"vuedraggable": "^4.0.1",
|
||||||
"vuex": "^4.0.0-0"
|
"vuex": "^4.0.0-0"
|
||||||
@ -45,6 +46,7 @@
|
|||||||
"@types/prosemirror-schema-basic": "^1.0.1",
|
"@types/prosemirror-schema-basic": "^1.0.1",
|
||||||
"@types/prosemirror-schema-list": "^1.0.1",
|
"@types/prosemirror-schema-list": "^1.0.1",
|
||||||
"@types/resize-observer-browser": "^0.1.4",
|
"@types/resize-observer-browser": "^0.1.4",
|
||||||
|
"@types/tinycolor2": "^1.4.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||||
"@typescript-eslint/parser": "^2.33.0",
|
"@typescript-eslint/parser": "^2.33.0",
|
||||||
"@vue/cli-plugin-babel": "~4.5.0",
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
131
src/components/ColorPicker/Alpha.vue
Normal file
131
src/components/ColorPicker/Alpha.vue
Normal 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>
|
72
src/components/ColorPicker/Checkboard.vue
Normal file
72
src/components/ColorPicker/Checkboard.vue
Normal 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>
|
63
src/components/ColorPicker/EditableInput.vue
Normal file
63
src/components/ColorPicker/EditableInput.vue
Normal 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>
|
127
src/components/ColorPicker/Hue.vue
Normal file
127
src/components/ColorPicker/Hue.vue
Normal 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>
|
122
src/components/ColorPicker/Saturation.vue
Normal file
122
src/components/ColorPicker/Saturation.vue
Normal 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>
|
239
src/components/ColorPicker/index.vue
Normal file
239
src/components/ColorPicker/index.vue
Normal 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>
|
@ -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>
|
|
@ -1,13 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="slide-style-panel">
|
<div class="slide-style-panel">
|
||||||
slide-style-panel
|
<ColorPicker v-model="color" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent, ref } from 'vue'
|
||||||
|
import ColorPicker from '@/components/ColorPicker/index.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'slide-style-panel',
|
name: 'slide-style-panel',
|
||||||
|
components: {
|
||||||
|
ColorPicker,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const color = ref('#888')
|
||||||
|
|
||||||
|
return {
|
||||||
|
color,
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
Loading…
x
Reference in New Issue
Block a user