color-picker完整版
前面发过一版,漏了个最重要的代码文件color.js,现在补上。
以下是整个color-picker的完整版
由于UI需求,需要使用直接取色的取色器面板,有点类似于element-plus里的el-color-pick,但是不需要有展开的操作。于是模仿element-plus写了该组件。技术栈vue3.js + javascript
该组件包含三个部分:
- 一个饱和度 - 明度(SV)面板,用于调整颜色的饱和度和明度;
- 用于调整颜色色相(hue 值)的滑块;
-
用于调整颜色透明度(alpha 值)的滑块。
代码实现包含:index.vue、SvPanel.vue、HueSlider.vue、AlphaSlider.vue、utils.js、color.js(新增)
目录
调整颜色色相(hue 值)的滑块代码:HueSlider.vue
调整颜色透明度(alpha 值)的滑块代码:AlphaSlider.vue
入口代码:index.vue
<template>
<div class="color-dropdown__main-wrapper">
<SvPanel ref="svPanel" :color="state.color"></SvPanel>
<HueSlider ref="hueSlider" :color="state.color"></HueSlider>
</div>
<AlphaSlider ref="alphaSlider" :color="state.color"></AlphaSlider>
</template>
<script setup>
import {
reactive,
onBeforeUnmount,
onMounted,
watch,
ref,
computed,
} from "vue";
import SvPanel from "./SvPanel.vue";
import HueSlider from "./HueSlider.vue";
import AlphaSlider from "./AlphaSlider.vue";
import Color from "/public/native/core/color.js";
const state = reactive({
color: new Color(),
});
const emits = defineEmits({
change: null,
});
const alphaSlider = ref(null);
let colorValue = computed(() => {
const hue = state.color.get("hue");
const value = state.color.get("value");
const saturation = state.color.get("saturation");
const alpha = state.color.get("alpha");
return { hue, value, saturation, alpha };
});
let props = defineProps({
modelValue: {
type: String,
required: false,
default: "#ffffff",
},
});
watch(
() => colorValue,
(colorValue) => {
alphaSlider.value.update();
let alpha = colorValue.value.alpha
? colorValue.value.alpha / 101
: 0.99;
emits("change", {
color: state.color.tohex(),
alpha,
});
},
{ deep: true }
);
watch(
() => props.modelValue,
(modelValue) => {
console.log("modelValue-change:", modelValue);
state.color.fromHex(modelValue);
}
);
onMounted(() => {
state.color.fromHex(props.modelValue);
});
onBeforeUnmount(() => {
//console.log('onBeforeUnmount')
});
</script>
<style scoped lang="less">
.color-dropdown__main-wrapper {
margin-bottom: 3px;
display: flex;
&:after {
content: "";
display: table;
clear: both;
}
}
</style>
饱和度 - 明度(SV)面板代码:SvPanel.vue
<template>
<div
class="color-svpanel"
:style="{
backgroundColor: state.background,
}"
>
<div class="color-svpanel__white"></div>
<div class="color-svpanel__black"></div>
<div
class="color-svpanel__cursor"
:style="{
top: state.cursorTop + 'px',
left: state.cursorLeft + 'px',
}"
>
<div></div>
</div>
</div>
</template>
<script setup>
import {
reactive,
onBeforeUnmount,
onMounted,
getCurrentInstance,
computed,
watch,
} from "vue";
import { draggable, getClientXY } from "./utils.js";
let { vnode } = getCurrentInstance();
let props = defineProps({
color: {
type: Object,
required: true,
},
});
const state = reactive({
cursorTop: 0,
cursorLeft: 0,
background: "hsl(0, 100%, 50%)",
});
let colorValue = computed(() => {
const hue = props.color.get("hue");
const value = props.color.get("value");
return { hue, value };
});
watch(
() => colorValue.value,
() => {
update();
}
);
onMounted(() => {
draggable(vnode.el, {
drag: (event) => {
handleDrag(event);
},
end: (event) => {
handleDrag(event);
},
});
update();
});
onBeforeUnmount(() => {
//console.log('onBeforeUnmount')
});
function update() {
const saturation = props.color.get("saturation");
const value = props.color.get("value");
const el = vnode.el;
const { clientWidth, clientHeight } = el;
state.cursorLeft = (saturation * clientWidth) / 100;
state.cursorTop = ((100 - value) * clientHeight) / 100;
state.background = `hsl(${props.color.get("hue")}, 100%, 50%)`;
}
function handleDrag(event) {
const el = vnode.el;
const rect = el.getBoundingClientRect();
const { clientX, clientY } = getClientXY(event);
let left = clientX - rect.left;
let top = clientY - rect.top;
left = Math.max(0, left);
left = Math.min(left, rect.width);
top = Math.max(0, top);
top = Math.min(top, rect.height);
state.cursorLeft = left;
state.cursorTop = top;
props.color.set({
saturation: (left / rect.width) * 100,
value: 100 - (top / rect.height) * 100,
});
}
</script>
<style scoped lang="less">
.color-svpanel {
position: relative;
width: 128px;
height: 82px;
margin-right: 3px;
background-color: #fff;
.color-svpanel__white,
.color-svpanel__black {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.color-svpanel__white {
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
}
.color-svpanel__black {
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
}
.color-svpanel__cursor {
position: absolute;
& > div {
cursor: head;
width: 4px;
height: 4px;
box-shadow: 0 0 0 1.5px #fff, inset 0 0 1px 1px #0000004d,
0 0 1px 2px #0006;
border-radius: 50%;
transform: translate(-2px, -2px);
}
}
}
</style>
调整颜色色相(hue 值)的滑块代码:HueSlider.vue
<template>
<div class="color-hue-slider is-vertical hue-slider">
<div class="color-hue-slider__bar" ref="bar"></div>
<div
class="color-hue-slider__thumb"
ref="thumb"
:style="{
left: 0,
top: state.thumbTop + 'px',
}"
></div>
</div>
</template>
<script setup>
import {
ref,
reactive,
onBeforeUnmount,
onMounted,
getCurrentInstance,
watch,
defineProps,
nextTick,
} from "vue";
import { draggable, getClientXY } from "./utils.js";
let { vnode } = getCurrentInstance();
let props = defineProps({
color: {
type: Object,
required: true,
},
});
const state = reactive({
thumbLeft: 0,
thumbTop: 0,
hue: 0,
});
const thumb = ref(null);
const bar = ref(null);
onMounted(() => {
const dragConfig = {
drag: (event) => {
handleDrag(event);
},
end: (event) => {
handleDrag(event);
},
};
draggable(bar.value, dragConfig);
draggable(thumb.value, dragConfig);
nextTick(() => {
update();
});
});
onBeforeUnmount(() => {
//console.log('onBeforeUnmount')
});
watch(
() => state.hue,
(hue) => {
update();
}
);
function handleDrag(event) {
if (!bar.value || !thumb.value) return;
const el = vnode.el;
const rect = el.getBoundingClientRect();
const { clientY } = getClientXY(event);
let top = clientY - rect.top;
top = Math.min(top, rect.height - thumb.value.offsetHeight / 2);
top = Math.max(thumb.value.offsetHeight / 2, top);
state.hue = Math.round(
((top - thumb.value.offsetHeight / 2) /
(rect.height - thumb.value.offsetHeight)) *
360
);
props.color.set("hue", state.hue);
}
function update() {
state.thumbTop = getThumbTop();
}
function getThumbTop() {
if (!thumb.value) return 0;
const el = vnode.el;
if (!el) return 0;
return Math.round(
(state.hue * (el.offsetHeight - thumb.value.offsetHeight / 2)) / 360
);
}
</script>
<style scoped lang="less">
.color-hue-slider__bar {
position: relative;
background: linear-gradient(
to right,
#f00 0%,
#ff0 17%,
#0f0 33%,
#0ff 50%,
#00f 67%,
#f0f 83%,
#f00 100%
);
height: 100%;
}
.color-hue-slider__thumb {
position: absolute;
cursor: pointer;
box-sizing: border-box;
left: 0;
top: 0;
width: 4px;
height: 100%;
border-radius: 1px;
background: #fff;
border: 1px solid var(--el-border-color-lighter);
box-shadow: 0 0 2px #0009;
z-index: 1;
}
.color-hue-slider {
position: relative;
box-sizing: border-box;
width: 280px;
height: 12px;
background-color: red;
padding: 0 2px;
float: right;
&.is-vertical {
width: 6px;
height: 82px;
padding: 2px 0;
.color-hue-slider__bar {
background: linear-gradient(
to bottom,
#f00 0%,
#ff0 17%,
#0f0 33%,
#0ff 50%,
#00f 67%,
#f0f 83%,
#f00 100%
);
}
.color-hue-slider__thumb {
left: 0;
top: 0;
width: 100%;
height: 4px;
}
}
}
</style>
调整颜色透明度(alpha 值)的滑块代码:AlphaSlider.vue
<template>
<div class="color-alpha-slider">
<div
ref="bar"
class="color-alpha-slider__bar"
@click="handleClick"
:style="{ background: state.background }"
></div>
<div
ref="thumb"
class="color-alpha-slider__thumb"
:style="thumbStyle"
></div>
</div>
</template>
<script setup>
import {
reactive,
onBeforeUnmount,
onMounted,
getCurrentInstance,
ref,
computed,
watch,
} from "vue";
import { draggable, getClientXY } from "./utils.js";
let { vnode } = getCurrentInstance();
const state = reactive({
thumbLeft: 0,
thumbTop: 0,
background: "",
});
let props = defineProps({
color: {
type: Object,
required: true,
},
vertical: {
type: Boolean,
default: false,
},
});
const thumb = ref(null);
const bar = ref(null);
const thumbStyle = computed(() => ({
left: addUnit(state.thumbLeft),
top: addUnit(state.thumbTop),
}));
watch(
() => props.color.get("alpha"),
() => {
update();
}
);
watch(
() => props.color.get("value"),
() => {
update();
}
);
onMounted(() => {
if (!bar.value || !thumb.value) return;
const dragConfig = {
drag: (event) => {
handleDrag(event);
},
end: (event) => {
handleDrag(event);
},
};
draggable(bar.value, dragConfig);
draggable(thumb.value, dragConfig);
update();
});
onBeforeUnmount(() => {
//console.log('onBeforeUnmount')
});
function handleClick(event) {
const target = event.target;
if (target !== thumb.value) {
handleDrag(event);
}
}
function handleDrag(event) {
if (!bar.value || !thumb.value) return;
const el = vnode.el;
const rect = el.getBoundingClientRect();
const { clientX } = getClientXY(event);
let left = clientX - rect.left;
left = Math.max(thumb.value.offsetWidth / 2, left);
left = Math.min(left, rect.width - thumb.value.offsetWidth / 2);
props.color.set(
"alpha",
Math.round(
((left - thumb.value.offsetWidth / 2) /
(rect.width - thumb.value.offsetWidth)) *
100
)
);
}
function update() {
state.thumbLeft = getThumbLeft();
state.thumbTop = getThumbTop();
state.background = getBackground();
}
function getThumbLeft() {
if (!thumb.value) return 0;
if (props.vertical) return 0;
const el = vnode.el;
const alpha = props.color.get("alpha");
if (!el) return 0;
return Math.round(
(alpha * (el.offsetWidth - thumb.value.offsetWidth / 2)) / 100
);
}
function getThumbTop() {
if (!thumb.value) return 0;
const el = vnode.el;
if (!props.vertical) return 0;
const alpha = props.color.get("alpha");
if (!el) return 0;
return Math.round(
(alpha * (el.offsetHeight - thumb.value.offsetHeight / 2)) / 100
);
}
function getBackground() {
if (props.color && props.color.get("value")) {
const { r, g, b } = props.color.toRgba();
return `linear-gradient(to right, rgba(${r}, ${g}, ${b}, 0) 0%, rgba(${r}, ${g}, ${b}, 1) 100%)`;
}
return "";
}
function addUnit(value, defaultUnit = "px") {
if (!value) return "";
if (isNumber(value) || isStringNumber(value)) {
return `${value}${defaultUnit}`;
} else if (isString(value)) {
return value;
}
}
const isStringNumber = (val) => {
if (!isString(val)) {
return false;
}
return !Number.isNaN(Number(val));
};
const isNumber = (val) => typeof val === "number";
const isString = (val) => typeof val === "string";
defineExpose({
update,
bar,
thumb,
});
</script>
<style scoped lang="less">
.color-alpha-slider {
position: relative;
box-sizing: border-box;
width: 128px;
height: 8px;
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%),
linear-gradient(135deg, #ccc 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #ccc 75%),
linear-gradient(135deg, transparent 75%, #ccc 75%);
background-size: 8px 8px;
background-position: 0 0, 4px 0, 4px -4px, 0 4px;
.color-alpha-slider__bar {
position: relative;
background: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
#ffffff 100%
);
height: 100%;
}
.color-alpha-slider__thumb {
position: absolute;
cursor: pointer;
box-sizing: border-box;
left: 0;
top: 0;
width: 4px;
height: 100%;
border-radius: 1px;
background: #fff;
border: 1px solid #ebeef5;
box-shadow: 0 0 2px #0009;
z-index: 1;
}
}
</style>
工具代码:utils.js
let isDragging = false;
export function draggable(element, options) {
const moveFn = function (event) {
options.drag?.(event);
};
const upFn = function (event) {
document.removeEventListener("mousemove", moveFn);
document.removeEventListener("mouseup", upFn);
document.removeEventListener("touchmove", moveFn);
document.removeEventListener("touchend", upFn);
document.onselectstart = null;
document.ondragstart = null;
isDragging = false;
options.end?.(event);
};
const downFn = function (event) {
if (isDragging) return;
event.preventDefault();
document.onselectstart = () => false;
document.ondragstart = () => false;
document.addEventListener("mousemove", moveFn);
document.addEventListener("mouseup", upFn);
document.addEventListener("touchmove", moveFn);
document.addEventListener("touchend", upFn);
isDragging = true;
options.start?.(event);
};
element.addEventListener("mousedown", downFn);
element.addEventListener("touchstart", downFn);
}
export const getClientXY = (event) => {
let clientX;
let clientY;
if (event.type === "touchend") {
clientY = event.changedTouches[0].clientY;
clientX = event.changedTouches[0].clientX;
} else if (event.type.startsWith("touch")) {
clientY = event.touches[0].clientY;
clientX = event.touches[0].clientX;
} else {
clientY = event.clientY;
clientX = event.clientX;
}
return {
clientX,
clientY,
};
};
color.js
export default class Color {
constructor(hue = 0, saturation = 100, value = 100, alpha = 100) {
this._hue = hue;
this._saturation = saturation;
this._value = value;
this._alpha = alpha;
}
set(prop, value) {
if (arguments.length === 1 && typeof prop === "object") {
for (const p in prop) {
this.set(p, prop[p]);
}
return;
}
this[`_${prop}`] = value;
}
get(prop) {
if (prop === "alpha") {
return Math.floor(this[`_${prop}`]);
}
return this[`_${prop}`];
}
toRgba() {
let color = hsv2rgb(this._hue, this._saturation, this._value);
color.a = Math.floor((this._alpha / 100) * 255);
return color;
}
fromHex(string) {
let color = hex2rgba(string);
this._alpha = Math.floor((color.a / 255) * 100);
color = rgb2hsv(color.r, color.g, color.b);
this._hue = color.h;
this._saturation = color.s;
this._value = color.v;
}
tohex() {
let color = hsv2rgb(this._hue, this._saturation, this._value);
color.a = (this._alpha / 100) * 255;
return rgba2hex(color);
}
}
export const rgb2hsv = function (r, g, b) {
r = bound01(r, 255);
g = bound01(g, 255);
b = bound01(b, 255);
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h;
const v = max;
const d = max - min;
const s = max === 0 ? 0 : d / max;
if (max === min) {
h = 0; // achromatic
} else {
switch (max) {
case r: {
h = (g - b) / d + (g < b ? 6 : 0);
break;
}
case g: {
h = (b - r) / d + 2;
break;
}
case b: {
h = (r - g) / d + 4;
break;
}
}
h /= 6;
}
return { h: h * 360, s: s * 100, v: v * 100 };
};
const hsv2rgb = function (h, s, v) {
h = bound01(h, 360) * 6;
s = bound01(s, 100);
v = bound01(v, 100);
const i = Math.floor(h);
const f = h - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
const mod = i % 6;
const r = [v, q, p, p, t, v][mod];
const g = [t, v, v, q, p, p][mod];
const b = [p, p, t, v, v, q][mod];
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255),
};
};
const bound01 = function (value, max) {
if (isOnePointZero(value)) value = "100%";
const processPercent = isPercentage(value);
value = Math.min(max, Math.max(0, Number.parseFloat(`${value}`)));
if (processPercent) {
value = Number.parseInt(`${value * max}`, 10) / 100;
}
if (Math.abs(value - max) < 0.000001) {
return 1;
}
return (value % max) / Number.parseFloat(max);
};
const isOnePointZero = function (n) {
return (
typeof n === "string" && n.includes(".") && Number.parseFloat(n) === 1
);
};
const isPercentage = function (n) {
return typeof n === "string" && n.includes("%");
};
//16进制颜色转rgba
export function hex2rgba(str) {
let reg = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
if (!reg.test(str)) {
return;
}
// eslint-disable-next-line
let newStr = str.toLowerCase().replace(/\#/g, "");
let len = newStr.length;
if (len == 3) {
let t = "";
for (let i = 0; i < len; i++) {
t += newStr.slice(i, i + 1).concat(newStr.slice(i, i + 1));
}
newStr = t;
}
let arr = []; //将字符串分隔,两个两个的分隔
if (newStr.length == 6) {
newStr += "ff";
}
for (let i = 0; i < 8; i = i + 2) {
let s = newStr.slice(i, i + 2);
let num = parseInt("0x" + s);
arr.push(num);
}
return {
r: arr[0],
g: arr[1],
b: arr[2],
a: arr[3],
};
}
//rgba颜色转16进制
export function rgba2hex(value) {
if (typeof value == "string") {
value = rgbaFromString(value);
}
let { r, g, b, a = 255 } = value;
const INT_HEX_MAP = {
10: "A",
11: "B",
12: "C",
13: "D",
14: "E",
15: "F",
};
const hexOne = (value) => {
value = Math.min(Math.round(value), 255);
const high = Math.floor(value / 16);
const low = value % 16;
return `${INT_HEX_MAP[high] || high}${INT_HEX_MAP[low] || low}`;
};
if (Number.isNaN(+r) || Number.isNaN(+g) || Number.isNaN(+b)) return "";
return `#${hexOne(r)}${hexOne(g)}${hexOne(b)}${hexOne(a)}`;
}
//rgba颜色字符串转对象
export function rgbaFromString(str) {
const parts = str
.replace(/rgba|rgb|\(|\)/gm, "")
.split(/\s|,/g)
.filter((val) => val !== "")
.map((val, index) =>
index > 2 ? Number.parseFloat(val) : Number.parseInt(val, 10)
);
if (parts.length === 3) {
parts[3] = 1;
}
let color = {
r: parts[0],
g: parts[1],
b: parts[2],
a: parts[3] * 255,
};
return color;
}
common.js:
使用示例:
<template>
<ColorPicker
v-model="color"
@change="changeColor"
></ColorPicker>
</template>
<script setup>
import { ref } from "vue";
import ColorPicker from "./color-picker/index.vue";
const color=ref("#ffffff")
function changeColor({ color, alpha }) {
console.log(color,alpha)
}
</script>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通