Last active
July 7, 2024 20:17
-
-
Save nurdism/78f7cf1c885bad42e7ffe100a1a22ff4 to your computer and use it in GitHub Desktop.
Very simple color picker for Vue 3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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