color-picker封装(开箱即用)

目录

入口代码:index.vue

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

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

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

工具代码:utils.js

使用示例:


由于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

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

使用示例:

<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  阅读(4)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示