@thinkpixellab-public/px-vue 3.0.101 → 3.0.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/icons/ellipsis.svg +1 -0
- package/assets/icons/eyedropper.svg +1 -0
- package/components/PxBalancedText.vue +3 -0
- package/components/PxBaseColor.vue +186 -0
- package/components/PxColorPalette.vue +118 -0
- package/components/PxColorPaletteButton.vue +140 -0
- package/components/PxColorPanel.vue +267 -0
- package/components/PxColorPicker.vue +142 -0
- package/package.json +2 -1
- package/utils/color.js +50 -0
- package/utils/drag.js +15 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24"><g transform="translate(4 11)" fill="currentColor" fill-rule="evenodd"><circle cx="1.5" cy="1.5" r="1.5"/><circle cx="8" cy="1.5" r="1.5"/><circle cx="14.5" cy="1.5" r="1.5"/></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24"><g fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke="currentColor" stroke-width="2"><path d="M16.716 2.498a3.067 3.067 0 0 1 2.962-.793c1.059.283 2.654 1.878 2.937 2.937a3.067 3.067 0 0 1-.793 2.962l-4.152 4.152-.538-.537-9.72 9.72L1.6 22.72l1.78-5.811 9.721-9.721-.537-.538 4.152-4.152ZM11.315 5.402l7.526 7.526"/></g></svg>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import tinycolor from 'tinycolor2';
|
|
3
|
+
import {
|
|
4
|
+
hsvaEnsureObject,
|
|
5
|
+
hsvaIsEqual,
|
|
6
|
+
hsvaToHsla,
|
|
7
|
+
hslaToStr,
|
|
8
|
+
hsvaContrastColor,
|
|
9
|
+
} from '../utils/color.js';
|
|
10
|
+
|
|
11
|
+
const TINY_FORMATS = ['rgb', 'prgb', 'hex6', 'hex3', 'hex8', 'name', 'hsl', 'hsv'];
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
name: 'px-color-picker',
|
|
15
|
+
|
|
16
|
+
props: {
|
|
17
|
+
/**
|
|
18
|
+
* The color value.
|
|
19
|
+
*/
|
|
20
|
+
color: { default: '#FF0000' },
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The format of the color value (used for emitting events, v-model, and the default format
|
|
24
|
+
* for getColor). Allowed values are the same as those available from the tinycolor library
|
|
25
|
+
* (rgb, prgb, hex6, hex3, hex8, name, hsl, hsv) as well as 'auto' (which will attempt to
|
|
26
|
+
* use the same format as the last value set with the color prop) and 'tiny' (which will
|
|
27
|
+
* return a tinycolor object) and 'hsva' which will return hsva values as an object. The
|
|
28
|
+
* format has the advantage of not requiring a tinycolor object to be created when the color
|
|
29
|
+
* changes.
|
|
30
|
+
*
|
|
31
|
+
* @values rgb, prgb, hex6, hex3, hex8, name, hsl, hsv, auto, tiny
|
|
32
|
+
*/
|
|
33
|
+
format: { type: String, default: 'hsva' },
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether alpha colors are enabled.
|
|
37
|
+
*/
|
|
38
|
+
alphaEnabled: { type: Boolean, default: false },
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
data() {
|
|
42
|
+
return { autoFormat: this.format, h: 0, s: 0, v: 0, a: 1 };
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
computed: {
|
|
46
|
+
hsva() {
|
|
47
|
+
return { h: this.h, s: this.s, v: this.v, a: this.alphaEnabled ? this.a : 1 };
|
|
48
|
+
},
|
|
49
|
+
hsla() {
|
|
50
|
+
return hsvaToHsla(this.hsva);
|
|
51
|
+
},
|
|
52
|
+
hslStr() {
|
|
53
|
+
return hslaToStr(this.hsla, false);
|
|
54
|
+
},
|
|
55
|
+
hslaStr() {
|
|
56
|
+
return hslaToStr(this.hsla, true);
|
|
57
|
+
},
|
|
58
|
+
hslaTransparentStr() {
|
|
59
|
+
return hslaToStr({ ...this.hsla, a: 0 }, true);
|
|
60
|
+
},
|
|
61
|
+
hueStr() {
|
|
62
|
+
return `hsl(${this.h}, 100%, 50%)`;
|
|
63
|
+
},
|
|
64
|
+
contrastStr() {
|
|
65
|
+
return hsvaContrastColor(this.hsva);
|
|
66
|
+
},
|
|
67
|
+
eyedropperSupported() {
|
|
68
|
+
return window?.EyeDropper;
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
watch: {
|
|
73
|
+
color(nv) {
|
|
74
|
+
// update colorValue when color changes
|
|
75
|
+
this.setColor(nv);
|
|
76
|
+
},
|
|
77
|
+
hsla() {
|
|
78
|
+
// emit change when this changes as a proxy for changes to hsva as individual props
|
|
79
|
+
this.emitChange();
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
mounted() {
|
|
84
|
+
if (this.color) {
|
|
85
|
+
this.setColor(this.color);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this.format) {
|
|
89
|
+
if (!TINY_FORMATS.includes(this.format)) {
|
|
90
|
+
console.warn(
|
|
91
|
+
`[PxBaseColor] Invalid format: "${this.format}". See comments in PxBaseColor.vue`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
methods: {
|
|
97
|
+
setColor(strOrHsva) {
|
|
98
|
+
if (typeof strOrHsva === 'string') {
|
|
99
|
+
this.setColorStr(strOrHsva);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (typeof strOrHsva === 'object') {
|
|
103
|
+
this.setColorHsva(strOrHsva);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
setColorStr(str) {
|
|
108
|
+
// this is for efficiency but also to prevent color creep as we convert between formats
|
|
109
|
+
if (this.lastEmit == str) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const tiny = tinycolor(str);
|
|
114
|
+
this.autoFormat = tiny.getFormat();
|
|
115
|
+
|
|
116
|
+
const hsva = tiny.toHsv();
|
|
117
|
+
this.setColorHsva(hsva);
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
setColorHsva(hsva) {
|
|
121
|
+
if (this.isCurrentHsva(hsva)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.h = hsva.h;
|
|
126
|
+
this.s = hsva.s;
|
|
127
|
+
this.v = hsva.v;
|
|
128
|
+
this.a = hsva.a;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
emitChange() {
|
|
132
|
+
const clr = this.getColor(this.format);
|
|
133
|
+
|
|
134
|
+
this.lastEmit = clr;
|
|
135
|
+
|
|
136
|
+
this.$emit('update:color', clr);
|
|
137
|
+
this.$emit('change', clr);
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
getColor(format = null) {
|
|
141
|
+
format = format || this.format;
|
|
142
|
+
|
|
143
|
+
if (format == 'auto') {
|
|
144
|
+
format = this.autoFormat;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let clr = { h: this.h, s: this.s, v: this.v, a: this.a };
|
|
148
|
+
|
|
149
|
+
if (format == 'hsva') {
|
|
150
|
+
return clr;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (format && [...TINY_FORMATS, 'tiny'].includes(format)) {
|
|
154
|
+
const tiny = tinycolor(clr);
|
|
155
|
+
|
|
156
|
+
if (format == 'tiny') {
|
|
157
|
+
clr = tiny;
|
|
158
|
+
} else {
|
|
159
|
+
clr = tiny.toString(format);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return clr;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
colorStrToHsva(str) {
|
|
167
|
+
const tiny = tinycolor(str);
|
|
168
|
+
return tiny.toHsv();
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
isCurrentHsva(hOrObj, s, v, a) {
|
|
172
|
+
const hsvaCompare = hsvaEnsureObject(hOrObj, s, v, a);
|
|
173
|
+
return hsvaIsEqual(this.hsva, hsvaCompare);
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
setColorFromEyedropper() {
|
|
177
|
+
if (this.eyedropperSupported) {
|
|
178
|
+
const eyeDropper = new EyeDropper();
|
|
179
|
+
eyeDropper.open().then(result => {
|
|
180
|
+
this.setColorStr(result.sRGBHex);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
</script>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Describe this component...
|
|
3
|
+
-->
|
|
4
|
+
<template>
|
|
5
|
+
<div :class="bem()">
|
|
6
|
+
<PxColorPaletteButton
|
|
7
|
+
v-for="(color, index) in calcPalette"
|
|
8
|
+
:key="index"
|
|
9
|
+
:hsva="color.hsva"
|
|
10
|
+
:selected="!(pickerActive && isCurrentHsva(pickerHsva)) && isCurrentHsva(color.hsva)"
|
|
11
|
+
@click.native="onPaletteClick(color)"
|
|
12
|
+
></PxColorPaletteButton>
|
|
13
|
+
<PxColorPicker
|
|
14
|
+
v-if="showPicker"
|
|
15
|
+
v-bind="calcPickerAttrs"
|
|
16
|
+
:color="pickerHsva"
|
|
17
|
+
:selected="pickerActive && isCurrentHsva(pickerHsva)"
|
|
18
|
+
:pending="pickerPending"
|
|
19
|
+
@click="onPickerClick"
|
|
20
|
+
@change="onPickerChange"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
import PxBaseColor from './PxBaseColor.vue';
|
|
27
|
+
import tinycolor from 'tinycolor2';
|
|
28
|
+
import PxColorPaletteButton from './PxColorPaletteButton.vue';
|
|
29
|
+
import PxColorPicker from './PxColorPicker.vue';
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
name: 'px-color-palette',
|
|
33
|
+
components: { PxColorPaletteButton, PxColorPicker },
|
|
34
|
+
mixins: [PxBaseColor],
|
|
35
|
+
props: {
|
|
36
|
+
/**
|
|
37
|
+
* Array of colors to display in the palette
|
|
38
|
+
*/
|
|
39
|
+
palette: { type: Array, default: null },
|
|
40
|
+
|
|
41
|
+
// none | picker | eyedropper | eyedropper-only
|
|
42
|
+
pickerMode: { type: String, default: 'none' },
|
|
43
|
+
pickerAttrs: { type: Object, default: () => {} },
|
|
44
|
+
pickerInitColor: { type: String, default: 'blue' },
|
|
45
|
+
pickerInitSelected: { type: Boolean, default: false },
|
|
46
|
+
},
|
|
47
|
+
data() {
|
|
48
|
+
return {
|
|
49
|
+
pickerActive: this.pickerInitSelected,
|
|
50
|
+
pickerPending: true,
|
|
51
|
+
pickerHsva: this.colorStrToHsva(this.pickerInitColor),
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
computed: {
|
|
55
|
+
calcPalette() {
|
|
56
|
+
return this.palette.map(color => {
|
|
57
|
+
const tiny = tinycolor(color);
|
|
58
|
+
return {
|
|
59
|
+
hsva: tiny.toHsv(),
|
|
60
|
+
css: tiny.toString(this.alphaEnabled ? 'hex6' : 'rgb'),
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
calcPickerAttrs() {
|
|
65
|
+
const eyedropperMode = {
|
|
66
|
+
none: 'none',
|
|
67
|
+
picker: 'none',
|
|
68
|
+
eyedropper: 'popup',
|
|
69
|
+
'eyedropper-only': 'only',
|
|
70
|
+
}[this.pickerMode];
|
|
71
|
+
|
|
72
|
+
const iconFallback = {
|
|
73
|
+
none: null,
|
|
74
|
+
picker: 'ellipsis',
|
|
75
|
+
eyedropper: 'ellipsis',
|
|
76
|
+
'eyedropper-only': 'eyedropper',
|
|
77
|
+
}[this.pickerMode];
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
iconFallback,
|
|
81
|
+
eyedropperMode,
|
|
82
|
+
alphaEnabled: this.alphaEnabled,
|
|
83
|
+
...this.pickerAttrs,
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
showPicker() {
|
|
87
|
+
return this.pickerMode != 'none';
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
// watch: {},
|
|
91
|
+
mounted() {},
|
|
92
|
+
methods: {
|
|
93
|
+
onPaletteClick(color) {
|
|
94
|
+
this.pickerActive = false;
|
|
95
|
+
this.setColorHsva(color.hsva);
|
|
96
|
+
},
|
|
97
|
+
onPickerClick() {
|
|
98
|
+
this.pickerActive = true;
|
|
99
|
+
},
|
|
100
|
+
onPickerChange(e) {
|
|
101
|
+
if (this.pickerActive) {
|
|
102
|
+
this.pickerPending = false;
|
|
103
|
+
this.pickerHsva = e;
|
|
104
|
+
this.setColorHsva(e);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<style lang="scss">
|
|
112
|
+
@use '../styles/px.scss' as *;
|
|
113
|
+
|
|
114
|
+
.px-color-palette {
|
|
115
|
+
display: flex;
|
|
116
|
+
gap: 0.5em;
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Describe this component...
|
|
3
|
+
-->
|
|
4
|
+
<template>
|
|
5
|
+
<button :class="bem({ selected, pending })" :style="{ color: hslaStr }">
|
|
6
|
+
<span />
|
|
7
|
+
<slot />
|
|
8
|
+
</button>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script>
|
|
12
|
+
// import { mapState } from 'vuex';
|
|
13
|
+
// import Component from '~/components/Component.vue';
|
|
14
|
+
// import Mixin from '~/components/Mixin.vue';
|
|
15
|
+
import { hsvaToHsla, hslaToStr } from '../utils/color.js';
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
name: 'px-color-palette-button',
|
|
19
|
+
props: {
|
|
20
|
+
hsva: {
|
|
21
|
+
type: Object,
|
|
22
|
+
default: () => {
|
|
23
|
+
return { h: 0, s: 0, v: 0, a: 0 };
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
selected: { type: Boolean, default: false },
|
|
27
|
+
pending: { type: Boolean, default: false },
|
|
28
|
+
},
|
|
29
|
+
// data() { return { b: 0 }; },
|
|
30
|
+
computed: {
|
|
31
|
+
hslaStr() {
|
|
32
|
+
return hslaToStr(hsvaToHsla(this.hsva), true);
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
// watch: {},
|
|
36
|
+
// mounted() {},
|
|
37
|
+
// methods: {},
|
|
38
|
+
};
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<style lang="scss">
|
|
42
|
+
@use '../styles/px.scss' as *;
|
|
43
|
+
|
|
44
|
+
@function color-wheel-gradient($count, $blend: true, $sat: 100%, $light: 50%) {
|
|
45
|
+
$grad: 'conic-gradient(';
|
|
46
|
+
@for $i from 1 through $count {
|
|
47
|
+
$color: hsl(360 - divide(360, $count) * $i, $sat, $light);
|
|
48
|
+
$start: percentage(divide($i - 1, $count));
|
|
49
|
+
$stop: percentage(divide($i, $count));
|
|
50
|
+
$end: if($i == $count, ')', ',');
|
|
51
|
+
|
|
52
|
+
@if $blend {
|
|
53
|
+
$grad: '#{$grad} #{$color} #{$start}#{$end}';
|
|
54
|
+
} @else {
|
|
55
|
+
$grad: '#{$grad} #{$color} #{$start}, #{$color} #{$stop}#{$end}';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@return unquote($grad);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.px-color-palette-button {
|
|
63
|
+
@include control-reset();
|
|
64
|
+
|
|
65
|
+
position: relative;
|
|
66
|
+
aspect-ratio: 1;
|
|
67
|
+
width: 2em;
|
|
68
|
+
border-radius: 50%;
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
display: inline-flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
|
|
74
|
+
@include transition(transform, $dur: 0.2s);
|
|
75
|
+
|
|
76
|
+
@include checkered-background(
|
|
77
|
+
$width: 0.25em,
|
|
78
|
+
$color: rgba(black, 0.1),
|
|
79
|
+
$color-alt: transparent
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
> span {
|
|
83
|
+
@include abs();
|
|
84
|
+
border-radius: inherit;
|
|
85
|
+
background-color: currentColor;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&:before,
|
|
89
|
+
&:after {
|
|
90
|
+
border-radius: inherit;
|
|
91
|
+
@include transition(all, $dur: 0.3s);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@include before() {
|
|
95
|
+
@include abs(-2px);
|
|
96
|
+
border: 2px solid black;
|
|
97
|
+
opacity: 0;
|
|
98
|
+
}
|
|
99
|
+
@include after() {
|
|
100
|
+
@include abs();
|
|
101
|
+
border: 1.5px solid white;
|
|
102
|
+
opacity: 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&:hover {
|
|
106
|
+
transform: scale(1.1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
&:active {
|
|
110
|
+
transform: scale(1) !important;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
&--selected {
|
|
114
|
+
&:before,
|
|
115
|
+
&:after {
|
|
116
|
+
opacity: 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
&--pending > span {
|
|
121
|
+
@include abs(-0.5px);
|
|
122
|
+
background-color: transparent;
|
|
123
|
+
background-image: color-wheel-gradient(8, false, $light: 50%);
|
|
124
|
+
@include after() {
|
|
125
|
+
@include abs(4px);
|
|
126
|
+
background-color: rgba(white, 0.8);
|
|
127
|
+
border: 0px solid white;
|
|
128
|
+
border-radius: 50%;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
&--pending {
|
|
133
|
+
color: black !important;
|
|
134
|
+
// &:before {
|
|
135
|
+
// border: 1px solid rgba(black, 0.5);
|
|
136
|
+
// opacity: 1;
|
|
137
|
+
// }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
</style>
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:class="bem()"
|
|
4
|
+
:style="{
|
|
5
|
+
'--px-color-panel-hsl': hslStr,
|
|
6
|
+
'--px-color-panel-hsla': hslaStr,
|
|
7
|
+
'--px-color-panel-hsla-transparent': hslaTransparentStr,
|
|
8
|
+
'--px-color-panel-hsl-hue': hueStr,
|
|
9
|
+
'--px-color-panel-contrast': contrastStr,
|
|
10
|
+
}"
|
|
11
|
+
>
|
|
12
|
+
<div ref="satval" :class="[bem('satval'), satValPanelClass]">
|
|
13
|
+
<div
|
|
14
|
+
:class="bem('sel', { satval: true })"
|
|
15
|
+
:style="{
|
|
16
|
+
left: cssPcnt(s),
|
|
17
|
+
top: cssPcnt(1 - v),
|
|
18
|
+
}"
|
|
19
|
+
></div>
|
|
20
|
+
</div>
|
|
21
|
+
<div :class="bem('sliders')">
|
|
22
|
+
<div :class="[bem('hue'), hueSliderClass]" ref="hue">
|
|
23
|
+
<div
|
|
24
|
+
:class="bem('sel', { hue: true })"
|
|
25
|
+
:style="{
|
|
26
|
+
left: cssPcnt(h / 360),
|
|
27
|
+
}"
|
|
28
|
+
></div>
|
|
29
|
+
</div>
|
|
30
|
+
<div v-if="alphaEnabled" :class="[bem('alpha'), alphaSliderClass]" ref="alpha">
|
|
31
|
+
<div
|
|
32
|
+
:class="bem('sel', { alpha: true })"
|
|
33
|
+
:style="{
|
|
34
|
+
left: cssPcnt(a),
|
|
35
|
+
}"
|
|
36
|
+
></div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div :class="bem('swatch')"></div>
|
|
40
|
+
<PxIconButton
|
|
41
|
+
v-if="eyedropperEnabled && eyedropperSupported"
|
|
42
|
+
:class="[bem('eyedropper'), eyedropperClass]"
|
|
43
|
+
@click="setColorFromEyedropper"
|
|
44
|
+
:src="EyeDropperSvg"
|
|
45
|
+
:size="0.85"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script>
|
|
51
|
+
import Drag from '../utils/drag.js';
|
|
52
|
+
import PxBaseColor from './PxBaseColor.vue';
|
|
53
|
+
import PxIconButton from './PxIconButton.vue';
|
|
54
|
+
import EyeDropperSvg from '../assets/icons/eyedropper.svg';
|
|
55
|
+
|
|
56
|
+
export default {
|
|
57
|
+
name: 'px-color-panel',
|
|
58
|
+
mixins: [PxBaseColor],
|
|
59
|
+
components: { PxIconButton },
|
|
60
|
+
props: {
|
|
61
|
+
hueSliderClass: { type: String, default: '' },
|
|
62
|
+
alphaSliderClass: { type: String, default: '' },
|
|
63
|
+
satValPanelClass: { type: String, default: '' },
|
|
64
|
+
eyedropperClass: { type: String, default: '' },
|
|
65
|
+
eyedropperEnabled: { type: Boolean, default: true },
|
|
66
|
+
eyedropperIcon: { type: String, default: EyeDropperSvg },
|
|
67
|
+
swatchVisible: { type: Boolean, default: true },
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
data() {
|
|
71
|
+
return {
|
|
72
|
+
EyeDropperSvg,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// watch: {},
|
|
77
|
+
mounted() {
|
|
78
|
+
if (this.$refs.satval) {
|
|
79
|
+
this.satvalDrag = new Drag(this.$refs.satval, {
|
|
80
|
+
startThreshold: 0,
|
|
81
|
+
lock: null,
|
|
82
|
+
onStart: this.onSatvalDrag,
|
|
83
|
+
onDrag: this.onSatvalDrag,
|
|
84
|
+
excludeSelector: this.dragExcludeSelector,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this.$refs.hue) {
|
|
89
|
+
this.hueDrag = new Drag(this.$refs.hue, {
|
|
90
|
+
startThreshold: 0,
|
|
91
|
+
lock: null,
|
|
92
|
+
onStart: this.onHueDrag,
|
|
93
|
+
onDrag: this.onHueDrag,
|
|
94
|
+
excludeSelector: this.dragExcludeSelector,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (this.alphaEnabled && this.$refs.alpha) {
|
|
99
|
+
this.alphaDrag = new Drag(this.$refs.alpha, {
|
|
100
|
+
startThreshold: 0,
|
|
101
|
+
lock: null,
|
|
102
|
+
onStart: this.onAlphaDrag,
|
|
103
|
+
onDrag: this.onAlphaDrag,
|
|
104
|
+
excludeSelector: this.dragExcludeSelector,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
beforeDestroy() {
|
|
109
|
+
if (this.satvalDrag) {
|
|
110
|
+
this.satvalDrag.destroy();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (this.hueDrag) {
|
|
114
|
+
this.hueDrag.destroy();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (this.alphaDrag) {
|
|
118
|
+
this.alphaDrag.destroy();
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
methods: {
|
|
122
|
+
onSatvalDrag(e) {
|
|
123
|
+
this.s = Math.max(0, Math.min(1, e.elementXPos));
|
|
124
|
+
this.v = Math.max(0, Math.min(1, 1 - e.elementYPos));
|
|
125
|
+
},
|
|
126
|
+
onHueDrag(e) {
|
|
127
|
+
this.h = 360 * Math.max(0, Math.min(1, e.elementXPos));
|
|
128
|
+
},
|
|
129
|
+
onAlphaDrag(e) {
|
|
130
|
+
this.a = Math.max(0, Math.min(1, e.elementXPos));
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
</script>
|
|
135
|
+
|
|
136
|
+
<style lang="scss">
|
|
137
|
+
@use '../styles/px.scss' as *;
|
|
138
|
+
|
|
139
|
+
@at-root {
|
|
140
|
+
:root {
|
|
141
|
+
--px-color-panel-aspect: 1;
|
|
142
|
+
--px-color-panel-gap: 0.5em;
|
|
143
|
+
--px-color-panel-radius: 0.5em;
|
|
144
|
+
--px-color-panel-slider-height: 0.5em;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.px-color-panel {
|
|
149
|
+
width: 20em;
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
aspect-ratio: var(--px-color-panel-aspect);
|
|
153
|
+
gap: var(--px-color-panel-gap) 0;
|
|
154
|
+
|
|
155
|
+
// prettier-ignore
|
|
156
|
+
@include grid-art(
|
|
157
|
+
(
|
|
158
|
+
'auto | #(0, 1fr) |',
|
|
159
|
+
'satval | satval | #(0,1fr)',
|
|
160
|
+
'swatch | sliders | auto',
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
&__satval {
|
|
165
|
+
grid-area: satval;
|
|
166
|
+
flex: 1;
|
|
167
|
+
position: relative;
|
|
168
|
+
display: block;
|
|
169
|
+
width: 100%;
|
|
170
|
+
border-radius: var(--px-color-panel-radius);
|
|
171
|
+
|
|
172
|
+
background-image: linear-gradient(rgba(0, 0, 0, 0), #000),
|
|
173
|
+
linear-gradient(90deg, #fff, var(--px-color-panel-hsl-hue));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
&__sliders {
|
|
177
|
+
grid-area: sliders;
|
|
178
|
+
display: flex;
|
|
179
|
+
flex-direction: column;
|
|
180
|
+
justify-content: center;
|
|
181
|
+
gap: var(--px-color-panel-gap);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
&__hue,
|
|
185
|
+
&__alpha {
|
|
186
|
+
position: relative;
|
|
187
|
+
display: block;
|
|
188
|
+
width: 100%;
|
|
189
|
+
height: 0.66em;
|
|
190
|
+
border-radius: var(--px-color-panel-radius);
|
|
191
|
+
flex: none;
|
|
192
|
+
align-self: center;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
&__hue {
|
|
196
|
+
grid-area: hue;
|
|
197
|
+
background-image: linear-gradient(90deg, red, yellow, lime, aqua, blue, fuchsia, red);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
&__alpha {
|
|
201
|
+
grid-area: alpha;
|
|
202
|
+
|
|
203
|
+
@include checkered-background(
|
|
204
|
+
$width: 0.25em,
|
|
205
|
+
$color: rgba(black, 0.1),
|
|
206
|
+
$color-alt: transparent
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
@include after() {
|
|
210
|
+
@include abs();
|
|
211
|
+
border-radius: inherit;
|
|
212
|
+
background-image: linear-gradient(
|
|
213
|
+
to right,
|
|
214
|
+
var(--px-color-panel-hsla-transparent) 0%,
|
|
215
|
+
var(--px-color-panel-hsl) 100%
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
&__sel {
|
|
221
|
+
position: absolute;
|
|
222
|
+
top: 100%;
|
|
223
|
+
left: 100%;
|
|
224
|
+
transform: translate(-50%, -50%);
|
|
225
|
+
width: 1px;
|
|
226
|
+
height: 1px;
|
|
227
|
+
width: 0.66em;
|
|
228
|
+
height: 0.66em;
|
|
229
|
+
background-color: var(--px-color-panel-hsl);
|
|
230
|
+
border: 1px solid white;
|
|
231
|
+
border-radius: 50%;
|
|
232
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
|
233
|
+
z-index: 1;
|
|
234
|
+
|
|
235
|
+
&:hover {
|
|
236
|
+
z-index: 2;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
&--hue {
|
|
240
|
+
top: 50%;
|
|
241
|
+
background-color: var(--px-color-panel-hsl-hue);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
&--alpha {
|
|
245
|
+
top: 50%;
|
|
246
|
+
background-color: var(--px-color-panel-hsla);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
&__swatch {
|
|
251
|
+
grid-area: swatch;
|
|
252
|
+
aspect-ratio: 1;
|
|
253
|
+
border-radius: 50%;
|
|
254
|
+
background-color: var(--px-color-panel-hsla);
|
|
255
|
+
margin-inline-end: var(--px-color-panel-gap);
|
|
256
|
+
border: 1px solid rgba(black, 0.1);
|
|
257
|
+
min-height: 1.66em;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
&__eyedropper {
|
|
261
|
+
grid-area: swatch;
|
|
262
|
+
margin-inline-end: var(--px-color-panel-gap);
|
|
263
|
+
color: var(--px-color-panel-contrast);
|
|
264
|
+
aspect-ratio: 1;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
</style>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="bem()">
|
|
3
|
+
<PxColorPaletteButton
|
|
4
|
+
@click.native="click"
|
|
5
|
+
:hsva="hsva"
|
|
6
|
+
:selected="selected"
|
|
7
|
+
:pending="pending"
|
|
8
|
+
>
|
|
9
|
+
<PxIcon
|
|
10
|
+
:class="bem('icon')"
|
|
11
|
+
v-bind="calcIconAttrs"
|
|
12
|
+
:style="{ color: calcIconColor }"
|
|
13
|
+
:size="1"
|
|
14
|
+
/>
|
|
15
|
+
</PxColorPaletteButton>
|
|
16
|
+
<PxFloat
|
|
17
|
+
:popupClass="bem('popup')"
|
|
18
|
+
:visible.sync="popupVisible"
|
|
19
|
+
variant="none"
|
|
20
|
+
placement="bottom-center"
|
|
21
|
+
>
|
|
22
|
+
<PxColorPanel
|
|
23
|
+
:class="bem('panel')"
|
|
24
|
+
:color="hsva"
|
|
25
|
+
:alphaEnabled="alphaEnabled"
|
|
26
|
+
:eyedropperEnabled="eyedropperMode === 'popup'"
|
|
27
|
+
format="hsva"
|
|
28
|
+
@change="panelColorChange"
|
|
29
|
+
></PxColorPanel>
|
|
30
|
+
</PxFloat>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script>
|
|
35
|
+
import PxFloat from './PxFloat.vue';
|
|
36
|
+
import PxColorPanel from './PxColorPanel.vue';
|
|
37
|
+
import PxColorPaletteButton from './PxColorPaletteButton.vue';
|
|
38
|
+
import PxBaseColor from './PxBaseColor.vue';
|
|
39
|
+
import PxIcon from './PxIcon.vue';
|
|
40
|
+
|
|
41
|
+
const PxColorPicker = {
|
|
42
|
+
name: 'px-color-picker',
|
|
43
|
+
components: { PxFloat, PxColorPanel, PxColorPaletteButton, PxIcon },
|
|
44
|
+
mixins: [PxBaseColor],
|
|
45
|
+
props: {
|
|
46
|
+
/**
|
|
47
|
+
* An icon to display inside the button. If a string is provided, it will be interpreted as
|
|
48
|
+
* the src of the icon. If an object is provided, it will be passed directly to the icon
|
|
49
|
+
* component as props (with a filter applied to ensure no fallback attributes).
|
|
50
|
+
*/
|
|
51
|
+
icon: { type: [String, Object], default: null },
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Convenience prop for setting the icon if the icon prop hasn't been set. Allowed values
|
|
55
|
+
* are 'eyedropper' and 'ellipsis'.
|
|
56
|
+
*/
|
|
57
|
+
iconFallback: { type: String, default: null },
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Describes how to integrate the eyedropper into the color picker. Allowed values are:
|
|
61
|
+
* 'none' - don't show the eyedropper
|
|
62
|
+
* 'popup' - show the eyedropper in the popup
|
|
63
|
+
* 'only' - only show the eyedropper (instead of the standard dialog)
|
|
64
|
+
*/
|
|
65
|
+
eyedropperMode: { type: String, default: 'none' },
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Makes the color picker appear selected (passed to the PxColorPaletteButton component)
|
|
69
|
+
*/
|
|
70
|
+
selected: { type: Boolean, default: false },
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Gives the color picker a pending appearance (passed to the PxColorPaletteButton component)
|
|
74
|
+
*/
|
|
75
|
+
pending: { type: Boolean, default: false },
|
|
76
|
+
},
|
|
77
|
+
data() {
|
|
78
|
+
return { popupVisible: false };
|
|
79
|
+
},
|
|
80
|
+
computed: {
|
|
81
|
+
calcIconAttrs() {
|
|
82
|
+
if (this.icon) {
|
|
83
|
+
return this.icon;
|
|
84
|
+
} else if (this.iconFallback) {
|
|
85
|
+
switch (this.iconFallback) {
|
|
86
|
+
case 'eyedropper':
|
|
87
|
+
return { src: require('../assets/icons/eyedropper.svg'), size: 0.9 };
|
|
88
|
+
case 'ellipsis':
|
|
89
|
+
return { src: require('../assets/icons/ellipsis.svg') };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
calcIconColor() {
|
|
94
|
+
if (this.pending) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return this.contrastStr;
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
// watch: {},
|
|
101
|
+
// mounted() {},
|
|
102
|
+
methods: {
|
|
103
|
+
panelColorChange(hsva) {
|
|
104
|
+
this.setColorHsva(hsva);
|
|
105
|
+
},
|
|
106
|
+
click() {
|
|
107
|
+
this.$emit('click');
|
|
108
|
+
|
|
109
|
+
if (this.eyedropperMode === 'only') {
|
|
110
|
+
this.setColorFromEyedropper();
|
|
111
|
+
} else {
|
|
112
|
+
this.popupVisible = true;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export default PxColorPicker;
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
<style lang="scss">
|
|
122
|
+
@use '../styles/px.scss' as *;
|
|
123
|
+
|
|
124
|
+
@at-root {
|
|
125
|
+
:root {
|
|
126
|
+
--px-color-picker-popup-bg: #{clr(page-bg)};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
.px-color-picker {
|
|
130
|
+
&__panel {
|
|
131
|
+
width: 10em;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
&__popup {
|
|
135
|
+
padding: 0.66em;
|
|
136
|
+
border-radius: 0.75em;
|
|
137
|
+
|
|
138
|
+
background-color: var(--px-color-picker-popup-bg);
|
|
139
|
+
@include depth-shadow(20, 0.15);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thinkpixellab-public/px-vue",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.103",
|
|
4
4
|
"description": "General purpose Vue components and helpers that can be used across projects.",
|
|
5
5
|
"author": "Pixel Lab",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"qrcode": "^1.5.1",
|
|
26
26
|
"tiny-date-picker": "^3.2.8",
|
|
27
27
|
"tiny-swiper": "^2.2.0",
|
|
28
|
+
"tinycolor2": "^1.6.0",
|
|
28
29
|
"vue-inline-svg": "^2.1.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
package/utils/color.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
function hsvaEnsureObject(hOrObj, s, v, a) {
|
|
2
|
+
if (typeof hOrObj === 'object') {
|
|
3
|
+
return hOrObj;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return { h: hOrObj, s, v, a };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function hsvaIsEqual(hsva1, hsva2) {
|
|
10
|
+
return hsva1.h === hsva2.h && hsva1.s === hsva2.s && hsva1.v === hsva2.v && hsva1.a === hsva2.a;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hsvaToHsla(hOrObj, s, v, a) {
|
|
14
|
+
const hsva = hsvaEnsureObject(hOrObj, s, v, a);
|
|
15
|
+
|
|
16
|
+
let l = ((2 - hsva.s) * hsva.v) / 2;
|
|
17
|
+
s = hsva.s;
|
|
18
|
+
|
|
19
|
+
if (l != 0) {
|
|
20
|
+
if (l == 1) {
|
|
21
|
+
s = 0;
|
|
22
|
+
} else if (l < 0.5) {
|
|
23
|
+
s = (s * hsva.v) / (l * 2);
|
|
24
|
+
} else {
|
|
25
|
+
s = (s * hsva.v) / (2 - l * 2);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { h: hsva.h, s: s, l: l, a: hsva.a };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hslaToStr(hsla, alphaEnabled = true) {
|
|
33
|
+
if (alphaEnabled) {
|
|
34
|
+
return `hsla(${hsla.h}, ${hsla.s * 100}%, ${hsla.l * 100}%, ${hsla.a})`;
|
|
35
|
+
} else {
|
|
36
|
+
return `hsl(${hsla.h}, ${hsla.s * 100}%, ${hsla.l * 100}%)`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hsvaContrastColor(hsva, light = '#FFF', dark = '#000') {
|
|
41
|
+
const THRESHOLD = 0.5;
|
|
42
|
+
const hsla = hsvaToHsla(hsva);
|
|
43
|
+
if (hsla.a < THRESHOLD) {
|
|
44
|
+
return dark;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return hsla.l > THRESHOLD ? dark : light;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { hsvaEnsureObject, hsvaIsEqual, hsvaToHsla, hslaToStr, hsvaContrastColor };
|
package/utils/drag.js
CHANGED
|
@@ -267,13 +267,26 @@ function Drag(element, options) {
|
|
|
267
267
|
coords.y = nativeEvent.clientY;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
const x = this.thresholdMet && this.startEv && this.lock == 'y' ? this.startEv.x : coords.x;
|
|
271
|
+
const y = this.thresholdMet && this.startEv && this.lock == 'x' ? this.startEv.y : coords.y;
|
|
272
|
+
|
|
270
273
|
const ev = {
|
|
271
|
-
x
|
|
272
|
-
y
|
|
274
|
+
x,
|
|
275
|
+
y,
|
|
273
276
|
native: nativeEvent,
|
|
274
277
|
lock: this.lock,
|
|
275
278
|
};
|
|
276
279
|
|
|
280
|
+
if (this.thresholdMet && this.element) {
|
|
281
|
+
const rect = element.getBoundingClientRect();
|
|
282
|
+
ev.elementX = x - rect.left;
|
|
283
|
+
ev.elementY = y - rect.top;
|
|
284
|
+
ev.elementWidth = rect.width;
|
|
285
|
+
ev.elementHeight = rect.height;
|
|
286
|
+
ev.elementXPos = ev.elementX / rect.width;
|
|
287
|
+
ev.elementYPos = ev.elementY / rect.height;
|
|
288
|
+
}
|
|
289
|
+
|
|
277
290
|
return ev;
|
|
278
291
|
};
|
|
279
292
|
|