color-picker完整版

前面发过一版,漏了个最重要的代码文件color.js,现在补上。

以下是整个color-picker的完整版

由于UI需求,需要使用直接取色的取色器面板,有点类似于element-plus里的el-color-pick,但是不需要有展开的操作。于是模仿element-plus写了该组件。技术栈vue3.js + javascript

该组件包含三个部分:

  1. 一个饱和度 - 明度(SV)面板,用于调整颜色的饱和度和明度;
  2. 用于调整颜色色相(hue 值)的滑块;
  3. 用于调整颜色透明度(alpha 值)的滑块。

代码实现包含:index.vue、SvPanel.vue、HueSlider.vue、AlphaSlider.vue、utils.js、color.js(新增)

目录

入口代码:index.vue

饱和度 - 明度(SV)面板代码:SvPanel.vue

调整颜色色相(hue 值)的滑块代码:HueSlider.vue

调整颜色透明度(alpha 值)的滑块代码:AlphaSlider.vue

工具代码:utils.js

使用示例:


入口代码: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>

成果图片:

posted @   YAY_tyy  阅读(7)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示