Skip to content

Instantly share code, notes, and snippets.

@nurdism
Last active July 7, 2024 20:17
Show Gist options
  • Save nurdism/78f7cf1c885bad42e7ffe100a1a22ff4 to your computer and use it in GitHub Desktop.
Save nurdism/78f7cf1c885bad42e7ffe100a1a22ff4 to your computer and use it in GitHub Desktop.
Very simple color picker for Vue 3
<script setup lang="ts">
/**
* ColorPicker.vue
* Very simple color picker for vue 3
* Only has 1 dependency tinycolor2 can be replaced with dedicated functions
*
* Preview: https://i.imgur.com/RjFUHsI.png
*/
import tinycolor from 'tinycolor2'
const h = ref(0)
const s = ref(0)
const v = ref(0)
const map = ref<HTMLElement>(null)
const slider = ref<HTMLElement>(null)
const props = defineProps({
modelValue: {
type: String,
required: true,
},
})
const emit = defineEmits(['update:modelValue'])
const pointer = computed(() => (tinycolor(props.modelValue).isDark() ? '#fff' : '#000'))
const background = computed(() => {
return tinycolor({
h: 360 * (1 - h.value),
s: 100,
v: 100,
}).toRgbString()
})
watch(
() => props.modelValue,
(val) => setColor(val),
)
onMounted(() => setColor(props.modelValue))
const dragging = {
map: false,
slider: false,
}
function onMouseMove(e: MouseEvent) {
e.preventDefault()
if (dragging.map) {
const rect = map.value.getBoundingClientRect()
s.value = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1)
v.value = Math.min(Math.max((rect.bottom - e.clientY) / rect.height, 0), 1)
update()
}
if (dragging.slider) {
const rect = slider.value.getBoundingClientRect()
h.value = Math.min(Math.max((rect.bottom - e.clientY) / rect.height, 0), 1)
update()
}
}
function onMapMouseDown(e: MouseEvent) {
e.preventDefault()
dragging.map = true
onMouseMove(e)
}
function onMapMouseUp(e: MouseEvent) {
e.preventDefault()
dragging.map = false
onMouseMove(e)
}
function onSliderClick(e: MouseEvent) {
dragging.slider = true
onMouseMove(e)
dragging.slider = false
}
function onSliderMouseDown(e: MouseEvent) {
e.preventDefault()
dragging.slider = true
onMouseMove(e)
}
function onSliderUpeDown(e: MouseEvent) {
e.preventDefault()
dragging.slider = false
onMouseMove(e)
}
function update() {
const current = tinycolor({ h: 360 * (1 - h.value), s: s.value, v: v.value })
const hex = current.toHexString()
if (props.modelValue.toLowerCase() != hex.toLowerCase()) {
emit('update:modelValue', hex)
}
}
function setColor(input: string) {
const current = tinycolor({ h: 360 * (1 - h.value), s: s.value, v: v.value })
const color = tinycolor(input)
if (input.toLowerCase() != current.toHexString().toLowerCase() && color.isValid()) {
const hsv = color.toHsv()
h.value = hsv.h / 360
s.value = hsv.s
v.value = hsv.v
}
}
</script>
<template>
<div class="color-picker">
<div class="slider">
<div class="slider-area" @click="onSliderClick">
<div class="slider-container" ref="slider" @mousemove="onMouseMove" @mouseup="onSliderUpeDown">
<div class="slider-handle" :style="{ bottom: `${h * 100}%` }" @mousedown="onSliderMouseDown"></div>
</div>
</div>
</div>
<div class="map" @mousedown="onMapMouseDown" @mousemove="onMouseMove" @mouseup="onMapMouseUp" @mouseleave="onMapMouseUp" ref="map">
<div class="map-background" :style="{ backgroundColor: background }">
<div class="map-background-overlay"></div>
</div>
<div class="map-pointer" :style="{ left: `${s * 100}%`, bottom: `${v * 100}%`, borderColor: pointer }"></div>
</div>
</div>
</template>
<style lang="scss" scoped>
.color-picker {
position: relative;
width: 100%;
height: 100%;
.map {
position: absolute;
top: 0;
bottom: 0;
right: 24px;
left: 0;
overflow: hidden;
user-select: none;
.map-background {
top: 0;
left: 0;
position: absolute;
height: 100%;
width: 100%;
.map-background-overlay {
display: block;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%),
linear-gradient(to right, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
}
}
.map-pointer {
position: absolute;
width: 10px;
height: 10px;
margin-left: -5px;
margin-bottom: -5px;
border-radius: 100%;
border: 1px solid;
will-change: auto;
z-index: 100;
}
}
.slider {
position: absolute;
top: 8px;
bottom: 8px;
right: 7px;
z-index: 100;
.slider-area {
height: 100%;
width: 100%;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
.slider-container {
width: 5px;
height: 100%;
position: relative;
border-radius: 3px;
background: linear-gradient(
rgb(255, 0, 0) 0%,
rgb(255, 255, 0) 17%,
rgb(0, 255, 0) 33%,
rgb(0, 255, 255) 50%,
rgb(0, 0, 255) 67%,
rgb(255, 0, 255) 83%,
rgb(255, 0, 0) 100%
);
.slider-handle {
width: 1em;
height: 1em;
border-radius: 100%;
position: absolute;
left: 50%;
transform: translate(-50%, 50%);
background-color: white;
box-shadow: 0.5px 0.5px 2px 1px rgba(0, 0, 0, 0.32);
}
}
}
}
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment