This commit is contained in:
xuziqiang
2024-05-15 17:29:42 +08:00
commit d0155dbe3c
7296 changed files with 1832517 additions and 0 deletions

View File

@@ -0,0 +1,288 @@
<template>
<div :class="['vc-sketch', disableAlpha ? 'vc-sketch__disable-alpha' : '']">
<div class="vc-sketch-saturation-wrap">
<saturation v-model:value="colors" @change="childChange" />
</div>
<div class="vc-sketch-controls">
<div class="vc-sketch-sliders">
<div class="vc-sketch-hue-wrap">
<hue v-model:value="colors" @change="childChange" />
</div>
</div>
<div class="vc-sketch-color-wrap">
<div
:aria-label="`Current color is ${activeColor}`"
class="vc-sketch-active-color"
:style="{ background: activeColor }"
></div>
<check-board />
</div>
</div>
<div class="vc-sketch-field" v-if="!disableFields">
<!-- rgba -->
<div class="vc-sketch-field--double">
<edit-input label="hex" :value="hex" @change="inputChange" />
</div>
<div class="vc-sketch-field--single">
<edit-input label="r" :value="colors.rgba.r" @change="inputChange" />
</div>
<div class="vc-sketch-field--single">
<edit-input label="g" :value="colors.rgba.g" @change="inputChange" />
</div>
<div class="vc-sketch-field--single">
<edit-input label="b" :value="colors.rgba.b" @change="inputChange" />
</div>
</div>
<div
class="vc-sketch-presets"
role="group"
aria-label="A color preset, pick one to set as current color"
>
<template v-for="c in presetColors" :key="c">
<div
v-if="!isTransparent(c)"
class="vc-sketch-presets-color"
:aria-label="'Color:' + c"
:style="{ background: c }"
@click="handlePreset(c)"
>
</div>
<div
v-else
:aria-label="'Color:' + c"
class="vc-sketch-presets-color"
@click="handlePreset(c)"
>
<check-board />
</div>
</template>
</div>
</div>
</template>
<script>
import colorMixin from './mixin/color';
import EditInput from './common/EditableInput.vue';
import saturation from './common/Saturation.vue';
import hue from './common/Hue.vue';
import CheckBoard from './common/Checkboard.vue';
const presetColors = [
'#D0021B',
'#F5A623',
'#F8E71C',
'#8B572A',
'#7ED321',
'#417505',
'#BD10E0',
'#4A90E2',
'#50E3C2',
'#B8E986',
'#000000',
'#4A4A4A',
];
export default {
name: 'NsColorPicker',
components: {
saturation,
hue,
EditInput,
CheckBoard,
},
mixins: [colorMixin],
props: {
presetColors: {
type: Array,
default() {
return presetColors;
},
},
disableAlpha: {
type: Boolean,
default: false,
},
disableFields: {
type: Boolean,
default: false,
},
},
computed: {
hex() {
let hex;
if (this.colors.a < 1) {
hex = this.colors.hex8;
} else {
hex = this.colors.hex;
}
return hex.replace('#', '');
},
activeColor() {
var rgba = this.colors.rgba;
return 'rgba(' + [rgba.r, rgba.g, rgba.b, rgba.a].join(',') + ')';
},
},
methods: {
handlePreset(c) {
this.colorChange({
hex: c,
source: 'hex',
});
},
childChange(data) {
this.colorChange(data);
},
inputChange(data) {
if (!data) {
return;
}
if (data.hex) {
this.isValidHex(data.hex) &&
this.colorChange({
hex: data.hex,
source: 'hex',
});
} else if (data.r || data.g || data.b || data.a) {
this.colorChange({
r: data.r || this.colors.rgba.r,
g: data.g || this.colors.rgba.g,
b: data.b || this.colors.rgba.b,
a: data.a || this.colors.rgba.a,
source: 'rgba',
});
}
},
},
};
</script>
<style>
.vc-sketch {
position: relative;
width: 200px;
padding: 10px 10px 0;
box-sizing: initial;
background: #fff;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15);
}
.vc-sketch-saturation-wrap {
width: 100%;
padding-bottom: 75%;
position: relative;
overflow: hidden;
}
.vc-sketch-controls {
display: flex;
}
.vc-sketch-sliders {
padding: 4px 0;
flex: 1;
}
.vc-sketch-sliders .vc-hue,
.vc-sketch-sliders .vc-alpha-gradient {
border-radius: 2px;
}
.vc-sketch-hue-wrap {
position: relative;
height: 24px;
}
.vc-sketch-alpha-wrap {
position: relative;
height: 10px;
margin-top: 4px;
overflow: hidden;
}
.vc-sketch-color-wrap {
width: 24px;
height: 24px;
position: relative;
margin-top: 4px;
margin-left: 4px;
border-radius: 3px;
}
.vc-sketch-active-color {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 2px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15), inset 0 0 4px rgba(0, 0, 0, 0.25);
z-index: 2;
}
.vc-sketch-color-wrap .vc-checkerboard {
background-size: auto;
}
.vc-sketch-field {
display: flex;
padding-top: 4px;
}
.vc-sketch-field .vc-input__input {
width: 90%;
padding: 2px 0;
text-align: center;
border: none;
box-shadow: inset 0 0 0 1px #ccc;
font-size: 10px;
}
.vc-sketch-field .vc-input__label {
display: block;
text-align: center;
font-size: 11px;
color: #222;
padding-top: 3px;
padding-bottom: 4px;
text-transform: capitalize;
}
.vc-sketch-field--single {
flex: 1;
padding-left: 6px;
}
.vc-sketch-field--double {
flex: 2;
}
.vc-sketch-presets {
margin-right: -10px;
margin-left: -10px;
padding-left: 10px;
padding-top: 10px;
border-top: 1px solid #eee;
}
.vc-sketch-presets-color {
border-radius: 3px;
overflow: hidden;
position: relative;
display: inline-block;
margin: 0 7px 10px 0;
vertical-align: top;
cursor: pointer;
width: 24px;
height: 24px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
}
.vc-sketch-presets-color .vc-checkerboard {
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
border-radius: 3px;
}
.vc-sketch__disable-alpha .vc-sketch-color-wrap {
height: 10px;
}
</style>

View File

@@ -0,0 +1,138 @@
<template>
<div class="vc-alpha">
<div class="vc-alpha-checkboard-wrap">
<checkboard></checkboard>
</div>
<div class="vc-alpha-gradient" :style="{ background: gradientColor }"></div>
<div
class="vc-alpha-container"
ref="container"
@mousedown="handleMouseDown"
@touchmove="handleChange"
@touchstart="handleChange"
>
<div class="vc-alpha-pointer" :style="{ left: colors.a * 100 + '%' }">
<div class="vc-alpha-picker"></div>
</div>
</div>
</div>
</template>
<script>
import checkboard from './Checkboard.vue';
export default {
name: 'Alpha',
props: {
value: Object,
onChange: Function,
},
components: {
checkboard,
},
computed: {
colors() {
return this.value;
},
gradientColor() {
var rgba = this.colors.rgba;
var rgbStr = [rgba.r, rgba.g, rgba.b].join(',');
return (
'linear-gradient(to right, rgba(' + rgbStr + ', 0) 0%, rgba(' + rgbStr + ', 1) 100%)'
);
},
},
methods: {
handleChange(e, skip) {
!skip && e.preventDefault();
var container = this.$refs.container;
if (!container) {
// for some edge cases, container may not exist. see #220
return;
}
var containerWidth = container.clientWidth;
var xOffset = container.getBoundingClientRect().left + window.pageXOffset;
var pageX = e.pageX || (e.touches ? e.touches[0].pageX : 0);
var left = pageX - xOffset;
var a;
if (left < 0) {
a = 0;
} else if (left > containerWidth) {
a = 1;
} else {
a = Math.round((left * 100) / containerWidth) / 100;
}
if (this.colors.a !== a) {
this.$emit('change', {
h: this.colors.hsl.h,
s: this.colors.hsl.s,
l: this.colors.hsl.l,
a: a,
source: 'rgba',
});
}
},
handleMouseDown(e) {
this.handleChange(e, true);
window.addEventListener('mousemove', this.handleChange);
window.addEventListener('mouseup', this.handleMouseUp);
},
handleMouseUp() {
this.unbindEventListeners();
},
unbindEventListeners() {
window.removeEventListener('mousemove', this.handleChange);
window.removeEventListener('mouseup', this.handleMouseUp);
},
},
};
</script>
<style>
.vc-alpha {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
.vc-alpha-checkboard-wrap {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
overflow: hidden;
}
.vc-alpha-gradient {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
.vc-alpha-container {
cursor: pointer;
position: relative;
z-index: 2;
height: 100%;
margin: 0 3px;
}
.vc-alpha-pointer {
z-index: 2;
position: absolute;
}
.vc-alpha-picker {
cursor: pointer;
width: 4px;
border-radius: 1px;
height: 8px;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
background: #fff;
margin-top: 1px;
transform: translateX(-2px);
}
</style>

View File

@@ -0,0 +1,92 @@
<template>
<div class="vc-checkerboard" :style="bgStyle"></div>
</template>
<script>
let _checkboardCache = {};
export default {
name: 'Checkboard',
props: {
size: {
type: [Number, String],
default: 8,
},
white: {
type: String,
default: '#fff',
},
grey: {
type: String,
default: '#e6e6e6',
},
},
computed: {
bgStyle() {
return {
'background-image': 'url(' + getCheckboard(this.white, this.grey, this.size) + ')',
};
},
},
};
/**
* get base 64 data by canvas
*
* @param {String} c1 hex color
* @param {String} c2 hex color
* @param {Number} size
*/
function renderCheckboard(c1, c2, size) {
// Dont Render On Server
if (typeof document === 'undefined') {
return null;
}
var canvas = document.createElement('canvas');
canvas.width = canvas.height = size * 2;
var ctx = canvas.getContext('2d');
// If no context can be found, return early.
if (!ctx) {
return null;
}
ctx.fillStyle = c1;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = c2;
ctx.fillRect(0, 0, size, size);
ctx.translate(size, size);
ctx.fillRect(0, 0, size, size);
return canvas.toDataURL();
}
/**
* get checkboard base data and cache
*
* @param {String} c1 hex color
* @param {String} c2 hex color
* @param {Number} size
*/
function getCheckboard(c1, c2, size) {
var key = c1 + ',' + c2 + ',' + size;
if (_checkboardCache[key]) {
return _checkboardCache[key];
} else {
var checkboard = renderCheckboard(c1, c2, size);
_checkboardCache[key] = checkboard;
return checkboard;
}
}
</script>
<style>
.vc-checkerboard {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
background-size: contain;
}
</style>

View File

@@ -0,0 +1,114 @@
<template>
<div class="vc-editable-input">
<input
:aria-labelledby="labelId"
class="vc-input__input"
v-model="val"
@keydown="handleKeyDown"
@input="update"
ref="input"
/>
<span :for="label" class="vc-input__label" :id="labelId">{{ labelSpanText }}</span>
<span class="vc-input__desc">{{ desc }}</span>
</div>
</template>
<script>
export default {
name: 'editableInput',
props: {
label: String,
labelText: String,
desc: String,
value: [String, Number],
max: Number,
min: Number,
arrowOffset: {
type: Number,
default: 1,
},
},
computed: {
val: {
get() {
return this.value;
},
set(v) {
// TODO: min
if (!(this.max === undefined) && +v > this.max) {
this.$refs.input.value = this.max;
} else {
return v;
}
},
},
labelId() {
return `input__label__${this.label}__${Math.random().toString().slice(2, 5)}`;
},
labelSpanText() {
return this.labelText || this.label;
},
},
methods: {
update(e) {
this.handleChange(e.target.value);
},
handleChange(newVal) {
let data = {};
data[this.label] = newVal;
if (data.hex === undefined && data['#'] === undefined) {
this.$emit('change', data);
} else if (newVal.length > 5) {
this.$emit('change', data);
}
},
// **** unused
// handleBlur (e) {
// console.log(e)
// },
handleKeyDown(e) {
let val = this.val;
let number = Number(val);
if (number) {
let amount = this.arrowOffset || 1;
// Up
if (e.keyCode === 38) {
val = number + amount;
this.handleChange(val);
e.preventDefault();
}
// Down
if (e.keyCode === 40) {
val = number - amount;
this.handleChange(val);
e.preventDefault();
}
}
},
// **** unused
// handleDrag (e) {
// console.log(e)
// },
// handleMouseDown (e) {
// console.log(e)
// }
},
};
</script>
<style>
.vc-editable-input {
position: relative;
}
.vc-input__input {
padding: 0;
border: 0;
outline: none;
}
.vc-input__label {
text-transform: capitalize;
}
</style>

View File

@@ -0,0 +1,205 @@
<template>
<div :class="['vc-hue', directionClass]">
<div
class="vc-hue-container"
role="slider"
:aria-valuenow="colors.hsl.h"
aria-valuemin="0"
aria-valuemax="360"
ref="container"
@mousedown="handleMouseDown"
@touchmove="handleChange"
@touchstart="handleChange"
>
<div
class="vc-hue-pointer"
:style="{ top: pointerTop, left: pointerLeft }"
role="presentation"
>
<div class="vc-hue-picker"></div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Hue',
props: {
value: Object,
direction: {
type: String,
// [horizontal | vertical]
default: 'horizontal',
},
},
data() {
return {
oldHue: 0,
pullDirection: '',
};
},
computed: {
colors() {
const h = this.value.hsl.h;
if (h !== 0 && h - this.oldHue > 0) this.pullDirection = 'right';
if (h !== 0 && h - this.oldHue < 0) this.pullDirection = 'left';
this.oldHue = h;
return this.value;
},
directionClass() {
return {
'vc-hue--horizontal': this.direction === 'horizontal',
'vc-hue--vertical': this.direction === 'vertical',
};
},
pointerTop() {
if (this.direction === 'vertical') {
if (this.colors.hsl.h === 0 && this.pullDirection === 'right') return 0;
return -((this.colors.hsl.h * 100) / 360) + 100 + '%';
} else {
return 0;
}
},
pointerLeft() {
if (this.direction === 'vertical') {
return 0;
} else {
if (this.colors.hsl.h === 0 && this.pullDirection === 'right') return '100%';
return (this.colors.hsl.h * 100) / 360 + '%';
}
},
},
methods: {
handleChange(e, skip) {
!skip && e.preventDefault();
var container = this.$refs.container;
if (!container) {
// for some edge cases, container may not exist. see #220
return;
}
var containerWidth = container.clientWidth;
var containerHeight = container.clientHeight;
var xOffset = container.getBoundingClientRect().left + window.pageXOffset;
var yOffset = container.getBoundingClientRect().top + window.pageYOffset;
var pageX = e.pageX || (e.touches ? e.touches[0].pageX : 0);
var pageY = e.pageY || (e.touches ? e.touches[0].pageY : 0);
var left = pageX - xOffset;
var top = pageY - yOffset;
var h;
var percent;
if (this.direction === 'vertical') {
if (top < 0) {
h = 360;
} else if (top > containerHeight) {
h = 0;
} else {
percent = -((top * 100) / containerHeight) + 100;
h = (360 * percent) / 100;
}
if (this.colors.hsl.h !== h) {
this.$emit('change', {
h: h,
s: this.colors.hsl.s,
l: this.colors.hsl.l,
a: this.colors.hsl.a,
source: 'hsl',
});
}
} else {
if (left < 0) {
h = 0;
} else if (left > containerWidth) {
h = 360;
} else {
percent = (left * 100) / containerWidth;
h = (360 * percent) / 100;
}
if (this.colors.hsl.h !== h) {
this.$emit('change', {
h: h,
s: this.colors.hsl.s,
l: this.colors.hsl.l,
a: this.colors.hsl.a,
source: 'hsl',
});
}
}
},
handleMouseDown(e) {
this.handleChange(e, true);
window.addEventListener('mousemove', this.handleChange);
window.addEventListener('mouseup', this.handleMouseUp);
},
handleMouseUp(e) {
this.unbindEventListeners();
},
unbindEventListeners() {
window.removeEventListener('mousemove', this.handleChange);
window.removeEventListener('mouseup', this.handleMouseUp);
},
},
};
</script>
<style>
.vc-hue {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
border-radius: 2px;
}
.vc-hue--horizontal {
background: linear-gradient(
to right,
#f00 0%,
#ff0 17%,
#0f0 33%,
#0ff 50%,
#00f 67%,
#f0f 83%,
#f00 100%
);
}
.vc-hue--vertical {
background: linear-gradient(
to top,
#f00 0%,
#ff0 17%,
#0f0 33%,
#0ff 50%,
#00f 67%,
#f0f 83%,
#f00 100%
);
}
.vc-hue-container {
cursor: pointer;
margin: 0 2px;
position: relative;
height: 100%;
}
.vc-hue-pointer {
z-index: 2;
position: absolute;
}
.vc-hue-picker {
cursor: pointer;
margin-top: 1px;
width: 4px;
border-radius: 1px;
height: 20px;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
background: #fff;
transform: translateX(-2px);
}
</style>

View File

@@ -0,0 +1,132 @@
<template>
<div
class="vc-saturation"
:style="{ background: bgColor }"
ref="container"
@mousedown="handleMouseDown"
@touchmove="handleChange"
@touchstart="handleChange"
>
<div class="vc-saturation--white"></div>
<div class="vc-saturation--black"></div>
<div class="vc-saturation-pointer" :style="{ top: pointerTop, left: pointerLeft }">
<div class="vc-saturation-circle"></div>
</div>
</div>
</template>
<script>
import clamp from 'clamp';
import { throttle } from 'lodash-es';
export default {
name: 'Saturation',
props: {
value: Object,
},
computed: {
colors() {
return this.value;
},
bgColor() {
return `hsl(${this.colors.hsv.h}, 100%, 50%)`;
},
pointerTop() {
return -(this.colors.hsv.v * 100) + 1 + 100 + '%';
},
pointerLeft() {
return this.colors.hsv.s * 100 + '%';
},
},
methods: {
throttle: throttle(
(fn, data) => {
fn(data);
},
20,
{
leading: true,
trailing: false,
}
),
handleChange(e, skip) {
!skip && e.preventDefault();
var container = this.$refs.container;
if (!container) {
// for some edge cases, container may not exist. see #220
return;
}
var containerWidth = container.clientWidth;
var containerHeight = container.clientHeight;
var xOffset = container.getBoundingClientRect().left + window.pageXOffset;
var yOffset = container.getBoundingClientRect().top + window.pageYOffset;
var pageX = e.pageX || (e.touches ? e.touches[0].pageX : 0);
var pageY = e.pageY || (e.touches ? e.touches[0].pageY : 0);
var left = clamp(pageX - xOffset, 0, containerWidth);
var top = clamp(pageY - yOffset, 0, containerHeight);
var saturation = left / containerWidth;
var bright = clamp(-(top / containerHeight) + 1, 0, 1);
this.throttle(this.onChange, {
h: this.colors.hsv.h,
s: saturation,
v: bright,
a: this.colors.hsv.a,
source: 'hsva',
});
},
onChange(param) {
this.$emit('change', param);
},
handleMouseDown(e) {
// this.handleChange(e, true)
window.addEventListener('mousemove', this.handleChange);
window.addEventListener('mouseup', this.handleChange);
window.addEventListener('mouseup', this.handleMouseUp);
},
handleMouseUp(e) {
this.unbindEventListeners();
},
unbindEventListeners() {
window.removeEventListener('mousemove', this.handleChange);
window.removeEventListener('mouseup', this.handleChange);
window.removeEventListener('mouseup', this.handleMouseUp);
},
},
created() {},
};
</script>
<style>
.vc-saturation,
.vc-saturation--white,
.vc-saturation--black {
cursor: pointer;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.vc-saturation--white {
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
}
.vc-saturation--black {
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
}
.vc-saturation-pointer {
cursor: pointer;
position: absolute;
}
.vc-saturation-circle {
cursor: head;
width: 4px;
height: 4px;
box-shadow: 0 0 0 1.5px #fff, inset 0 0 1px 1px rgba(0, 0, 0, 0.3),
0 0 1px 2px rgba(0, 0, 0, 0.4);
border-radius: 50%;
transform: translate(-2px, -2px);
}
</style>

View File

@@ -0,0 +1,4 @@
import ColorPicker from './color-picker.vue';
import { withInstall } from '/nerv-lib/util';
export const NsColorPicker = withInstall(ColorPicker);

View File

@@ -0,0 +1,117 @@
import tinycolor from 'tinycolor2';
function _colorChange(data, oldHue) {
const alpha = data && data.a;
let color;
// hsl is better than hex between conversions
if (data && data.hsl) {
color = tinycolor(data.hsl);
} else if (data && data.hex && data.hex.length > 0) {
color = tinycolor(data.hex);
} else if (data && data.hsv) {
color = tinycolor(data.hsv);
} else if (data && data.rgba) {
color = tinycolor(data.rgba);
} else if (data && data.rgb) {
color = tinycolor(data.rgb);
} else {
color = tinycolor(data);
}
if (color && (color._a === undefined || color._a === null)) {
color.setAlpha(alpha || 1);
}
const hsl = color.toHsl();
const hsv = color.toHsv();
if (hsl.s === 0) {
hsv.h = hsl.h = data.h || (data.hsl && data.hsl.h) || oldHue || 0;
}
/* --- comment this block to fix #109, may cause #25 again --- */
// when the hsv.v is less than 0.0164 (base on test)
// because of possible loss of precision
// the result of hue and saturation would be miscalculated
// if (hsv.v < 0.0164) {
// hsv.h = data.h || (data.hsv && data.hsv.h) || 0
// hsv.s = data.s || (data.hsv && data.hsv.s) || 0
// }
// if (hsl.l < 0.01) {
// hsl.h = data.h || (data.hsl && data.hsl.h) || 0
// hsl.s = data.s || (data.hsl && data.hsl.s) || 0
// }
/* ------ */
return {
hsl: hsl,
hex: color.toHexString().toUpperCase(),
hex8: color.toHex8String().toUpperCase(),
rgba: color.toRgb(),
hsv: hsv,
oldHue: data.h || oldHue || hsl.h,
source: data.source,
a: data.a || color.getAlpha(),
};
}
export default {
props: ['value'],
data() {
return {
val: _colorChange(this.value),
};
},
computed: {
colors: {
get() {
return this.val;
},
set(newVal) {
this.val = newVal;
this.$emit('update:value', newVal);
},
},
},
watch: {
value(newVal) {
this.val = _colorChange(newVal);
},
},
methods: {
colorChange(data, oldHue) {
this.oldHue = this.colors.hsl.h;
this.colors = _colorChange(data, oldHue || this.oldHue);
},
isValidHex(hex) {
return tinycolor(hex).isValid();
},
simpleCheckForValidColor(data) {
const keysToCheck = ['r', 'g', 'b', 'a', 'h', 's', 'l', 'v'];
let checked = 0;
let passed = 0;
for (let i = 0; i < keysToCheck.length; i++) {
const letter = keysToCheck[i];
if (data[letter]) {
checked++;
if (!isNaN(data[letter])) {
passed++;
}
}
}
if (checked === passed) {
return data;
}
},
paletteUpperCase(palette) {
return palette.map((c) => c.toUpperCase());
},
isTransparent(color) {
return tinycolor(color).getAlpha() === 0;
},
},
};