canvas-color-disc 1.2.0 → 1.2.1
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/README.cn.md +176 -0
- package/README.md +124 -29
- package/dist/canvas-color-disc.es.js +77 -45
- package/dist/canvas-color-disc.umd.js +1 -1
- package/dist/style.css +1 -1
- package/dist/vue3/canvas-color-disc.es.js +267 -0
- package/dist/vue3/canvas-color-disc.umd.js +1 -0
- package/dist/vue3/style.css +1 -0
- package/package.json +18 -4
- package/src/components/CanvasColorDisc.vue +58 -34
- package/src/index.js +3 -0
- package/src/style.css +20 -0
package/README.cn.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
[English](./README.md) | [简体中文](./README.cn.md)
|
|
2
|
+
|
|
3
|
+
# canvas-color-disc
|
|
4
|
+
|
|
5
|
+
> 基于 Vue + Canvas 的圆形色盘取色组件,支持 PC 与移动端(Pointer Events),同时支持 Vue 2 与 Vue 3
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Vue 版本支持
|
|
10
|
+
|
|
11
|
+
| Vue 版本 | 引入路径 | v-model 的 prop / 事件 | 样式 |
|
|
12
|
+
| --- | --- | --- | --- |
|
|
13
|
+
| Vue 2(`^2.5.0`) | `canvas-color-disc`(或 `canvas-color-disc/vue2`) | `value` / `input` | `canvas-color-disc/dist/style.css` |
|
|
14
|
+
| Vue 3(`^3.0.0`) | `canvas-color-disc/vue3` | `modelValue` / `update:modelValue` | `canvas-color-disc/vue3/style.css` |
|
|
15
|
+
|
|
16
|
+
两份产物来自同一份源码(`src/components/CanvasColorDisc.vue`),只是 SFC 编译目标不同;默认入口 `.` 保持为 Vue 2 构建以兼容旧版本。
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install canvas-color-disc
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
yarn add canvas-color-disc
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> Vue 为 peer 依赖(`^2.5.0 || ^3.0.0`),需自行安装;不依赖任何第三方运行时库。
|
|
29
|
+
|
|
30
|
+
## 引入
|
|
31
|
+
|
|
32
|
+
包内导出的是组件本身,同时挂载了 `install` 方法,因此全局注册与局部注册都支持。
|
|
33
|
+
|
|
34
|
+
### Vue 2
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import Vue from 'vue'
|
|
38
|
+
import CanvasColorDisc from 'canvas-color-disc'
|
|
39
|
+
import 'canvas-color-disc/dist/style.css'
|
|
40
|
+
|
|
41
|
+
Vue.use(CanvasColorDisc)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Vue 3
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import { createApp } from 'vue'
|
|
48
|
+
import CanvasColorDisc from 'canvas-color-disc/vue3'
|
|
49
|
+
import 'canvas-color-disc/vue3/style.css'
|
|
50
|
+
import App from './App.vue'
|
|
51
|
+
|
|
52
|
+
createApp(App).use(CanvasColorDisc).mount('#app')
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 局部注册
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
// Vue 2
|
|
59
|
+
import CanvasColorDisc from 'canvas-color-disc'
|
|
60
|
+
import 'canvas-color-disc/dist/style.css'
|
|
61
|
+
|
|
62
|
+
// Vue 3
|
|
63
|
+
import CanvasColorDisc from 'canvas-color-disc/vue3'
|
|
64
|
+
import 'canvas-color-disc/vue3/style.css'
|
|
65
|
+
|
|
66
|
+
export default {
|
|
67
|
+
components: { CanvasColorDisc }
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
注册后组件名为 `CanvasColorDisc`,模板中可使用 `<canvas-color-disc />`。
|
|
72
|
+
|
|
73
|
+
### 浏览器直接引入(UMD)
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<link rel="stylesheet" href="https://unpkg.com/canvas-color-disc/dist/style.css">
|
|
77
|
+
<script src="https://unpkg.com/vue@2.7.16/dist/vue.js"></script>
|
|
78
|
+
<script src="https://unpkg.com/canvas-color-disc"></script>
|
|
79
|
+
|
|
80
|
+
<script>
|
|
81
|
+
// 未调用 Vue.use 时,也可直接作为组件选项使用
|
|
82
|
+
new Vue({
|
|
83
|
+
el: '#app',
|
|
84
|
+
components: { CanvasColorDisc: window.CanvasColorDisc }
|
|
85
|
+
})
|
|
86
|
+
</script>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Vue 3 请使用 `dist/vue3/canvas-color-disc.umd.js` 搭配 `dist/vue3/style.css`,全局变量同样是 `window.CanvasColorDisc`,且支持 `app.use(...)`。
|
|
90
|
+
|
|
91
|
+
## 使用
|
|
92
|
+
|
|
93
|
+
Vue 2 与 Vue 3 的模板写法完全一致:
|
|
94
|
+
|
|
95
|
+
```vue
|
|
96
|
+
<template>
|
|
97
|
+
<div id="app">
|
|
98
|
+
<p>RGB: {{ rgb }}</p>
|
|
99
|
+
<canvas-color-disc
|
|
100
|
+
v-model="rgb"
|
|
101
|
+
:width="300"
|
|
102
|
+
:height="300"
|
|
103
|
+
@change="onChange"
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
</template>
|
|
107
|
+
|
|
108
|
+
<script>
|
|
109
|
+
export default {
|
|
110
|
+
data () {
|
|
111
|
+
return {
|
|
112
|
+
rgb: { r: 255, g: 255, b: 255 }
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
methods: {
|
|
116
|
+
onChange (rgb) {
|
|
117
|
+
console.log('change:', rgb)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
</script>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`v-model` 绑定的是 RGB 对象,父组件修改该对象时色盘选择器会同步移动。
|
|
125
|
+
|
|
126
|
+
## API
|
|
127
|
+
|
|
128
|
+
### Props
|
|
129
|
+
|
|
130
|
+
| 参数 | 说明 | 类型 | 默认值 | 适用版本 |
|
|
131
|
+
| --- | --- | --- | --- | --- |
|
|
132
|
+
| value | 当前颜色,配合 `v-model` 使用 | Object `{ r, g, b }` | `{ r: 255, g: 255, b: 255 }` | Vue 2 |
|
|
133
|
+
| modelValue | 当前颜色,配合 `v-model` 使用 | Object `{ r, g, b }` | `undefined`(回退到 `value`) | Vue 3 |
|
|
134
|
+
| width | 画布宽度(CSS 像素) | Number | 300 | 通用 |
|
|
135
|
+
| height | 画布高度(CSS 像素) | Number | 300 | 通用 |
|
|
136
|
+
|
|
137
|
+
### Events
|
|
138
|
+
|
|
139
|
+
| 事件名 | 说明 | 回调参数 | 适用版本 |
|
|
140
|
+
| --- | --- | --- | --- |
|
|
141
|
+
| input | 颜色变化时实时触发(按帧节流,配合 `v-model`) | `{ r, g, b }` | Vue 2 |
|
|
142
|
+
| update:modelValue | 同 `input`,配合 `v-model` 使用 | `{ r, g, b }` | Vue 3 |
|
|
143
|
+
| change | 一次选择(按下后抬起)结束时触发 | `{ r, g, b }` | 通用 |
|
|
144
|
+
|
|
145
|
+
`input` 与 `update:modelValue` 会同时派发,未被监听的一方会被忽略。
|
|
146
|
+
|
|
147
|
+
### 说明
|
|
148
|
+
|
|
149
|
+
- 色盘为 HSV 模型:色相取角度、饱和度取离圆心的半径比例,明度固定为最高,圆心为白色
|
|
150
|
+
- `width`/`height` 为画布逻辑尺寸(CSS 像素),色盘半径自动按 `min(width, height) / 2 - 18` 适配,圆心居中;两者可以不相等
|
|
151
|
+
- 画布按 `devicePixelRatio` 放大绘制,高清屏下不会模糊
|
|
152
|
+
- 取色坐标会自动换算回画布逻辑坐标:即使画布被外部 CSS 缩放(如全局 `canvas { max-width: 100% }`、父容器压缩、transform 缩放),取色位置仍与视觉位置一致
|
|
153
|
+
- 点击或拖动超出色盘范围时,选择器会钉在圆周上(取圆周对应位置的颜色)
|
|
154
|
+
- 基于 Pointer Events,鼠标与触摸的操作方式一致;拖动通过 `setPointerCapture` 捕获,指针移出画布也不中断
|
|
155
|
+
- 样式为普通 CSS,通过 `.canvas-color-disc` 后代选择器限定作用域,每个应用引入一次对应版本的 `style.css` 即可
|
|
156
|
+
|
|
157
|
+
## 本地开发
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# 安装依赖
|
|
161
|
+
npm install
|
|
162
|
+
|
|
163
|
+
# 启动 Vue 2 demo(localhost:5173)
|
|
164
|
+
npm run dev
|
|
165
|
+
|
|
166
|
+
# 同时构建 Vue 2(dist/)与 Vue 3(dist/vue3/)产物
|
|
167
|
+
npm run build
|
|
168
|
+
|
|
169
|
+
# 单独构建某一版本
|
|
170
|
+
npm run build:vue2
|
|
171
|
+
npm run build:vue3
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## 许可证
|
|
175
|
+
|
|
176
|
+
[MIT](./LICENSE)
|
package/README.md
CHANGED
|
@@ -1,30 +1,96 @@
|
|
|
1
|
-
|
|
1
|
+
[English](./README.md) | [简体中文](./README.cn.md)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# canvas-color-disc
|
|
4
4
|
|
|
5
|
+
> A circular color picker built with Vue + Canvas, works on both desktop and mobile (Pointer Events). Supports Vue 2 and Vue 3.
|
|
5
6
|
|
|
6
7
|

|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
## Vue version support
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
| Vue version | Import path | v-model prop / event | Style |
|
|
12
|
+
| --- | --- | --- | --- |
|
|
13
|
+
| Vue 2 (`^2.5.0`) | `canvas-color-disc` (or `canvas-color-disc/vue2`) | `value` / `input` | `canvas-color-disc/dist/style.css` |
|
|
14
|
+
| Vue 3 (`^3.0.0`) | `canvas-color-disc/vue3` | `modelValue` / `update:modelValue` | `canvas-color-disc/vue3/style.css` |
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
Both builds come from the same source (`src/components/CanvasColorDisc.vue`); only the SFC compile target differs. The default entry `.` stays on the Vue 2 build for backward compatibility.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install canvas-color-disc
|
|
15
22
|
```
|
|
16
23
|
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
npm run dev
|
|
24
|
+
```bash
|
|
25
|
+
yarn add canvas-color-disc
|
|
20
26
|
```
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
> Vue is a peer dependency (`^2.5.0 || ^3.0.0`) and must be installed by yourself. There is no other runtime dependency.
|
|
29
|
+
|
|
30
|
+
## Import
|
|
31
|
+
|
|
32
|
+
The package exports the component itself with an `install` method attached, so both global and local registration work.
|
|
33
|
+
|
|
34
|
+
### Vue 2
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import Vue from 'vue'
|
|
38
|
+
import CanvasColorDisc from 'canvas-color-disc'
|
|
39
|
+
import 'canvas-color-disc/dist/style.css'
|
|
40
|
+
|
|
41
|
+
Vue.use(CanvasColorDisc)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Vue 3
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import { createApp } from 'vue'
|
|
48
|
+
import CanvasColorDisc from 'canvas-color-disc/vue3'
|
|
49
|
+
import 'canvas-color-disc/vue3/style.css'
|
|
50
|
+
import App from './App.vue'
|
|
51
|
+
|
|
52
|
+
createApp(App).use(CanvasColorDisc).mount('#app')
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Local registration
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
// Vue 2
|
|
59
|
+
import CanvasColorDisc from 'canvas-color-disc'
|
|
60
|
+
import 'canvas-color-disc/dist/style.css'
|
|
61
|
+
|
|
62
|
+
// Vue 3
|
|
63
|
+
import CanvasColorDisc from 'canvas-color-disc/vue3'
|
|
64
|
+
import 'canvas-color-disc/vue3/style.css'
|
|
65
|
+
|
|
66
|
+
export default {
|
|
67
|
+
components: { CanvasColorDisc }
|
|
68
|
+
}
|
|
25
69
|
```
|
|
26
70
|
|
|
27
|
-
|
|
71
|
+
The component is registered as `CanvasColorDisc`, use it as `<canvas-color-disc />` in templates.
|
|
72
|
+
|
|
73
|
+
### Use via CDN (UMD)
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<link rel="stylesheet" href="https://unpkg.com/canvas-color-disc/dist/style.css">
|
|
77
|
+
<script src="https://unpkg.com/vue@2.7.16/dist/vue.js"></script>
|
|
78
|
+
<script src="https://unpkg.com/canvas-color-disc"></script>
|
|
79
|
+
|
|
80
|
+
<script>
|
|
81
|
+
// Without Vue.use, it can also be used directly as a component option
|
|
82
|
+
new Vue({
|
|
83
|
+
el: '#app',
|
|
84
|
+
components: { CanvasColorDisc: window.CanvasColorDisc }
|
|
85
|
+
})
|
|
86
|
+
</script>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
For Vue 3 use `dist/vue3/canvas-color-disc.umd.js` together with `dist/vue3/style.css`; the global is still `window.CanvasColorDisc` and it accepts an app instance via `app.use(...)`.
|
|
90
|
+
|
|
91
|
+
## Usage
|
|
92
|
+
|
|
93
|
+
The same template works for Vue 2 and Vue 3:
|
|
28
94
|
|
|
29
95
|
```vue
|
|
30
96
|
<template>
|
|
@@ -55,27 +121,56 @@ export default {
|
|
|
55
121
|
</script>
|
|
56
122
|
```
|
|
57
123
|
|
|
124
|
+
`v-model` binds an RGB object. Updating that object from the parent moves the selector accordingly.
|
|
125
|
+
|
|
58
126
|
## API
|
|
59
127
|
|
|
60
128
|
### Props
|
|
61
129
|
|
|
62
|
-
|
|
|
63
|
-
| --- | --- | --- | --- |
|
|
64
|
-
| value |
|
|
65
|
-
|
|
|
66
|
-
|
|
|
130
|
+
| Prop | Description | Type | Default | Vue version |
|
|
131
|
+
| --- | --- | --- | --- | --- |
|
|
132
|
+
| value | Current color, used by `v-model` | Object `{ r, g, b }` | `{ r: 255, g: 255, b: 255 }` | Vue 2 |
|
|
133
|
+
| modelValue | Current color, used by `v-model` | Object `{ r, g, b }` | `undefined` (falls back to `value`) | Vue 3 |
|
|
134
|
+
| width | Canvas width (CSS pixels) | Number | 300 | both |
|
|
135
|
+
| height | Canvas height (CSS pixels) | Number | 300 | both |
|
|
67
136
|
|
|
68
137
|
### Events
|
|
69
138
|
|
|
70
|
-
|
|
|
71
|
-
| --- | --- | --- |
|
|
72
|
-
| input |
|
|
73
|
-
|
|
|
139
|
+
| Event | Description | Payload | Vue version |
|
|
140
|
+
| --- | --- | --- | --- |
|
|
141
|
+
| input | Fired in real time while the color changes (throttled per frame, used by `v-model`) | `{ r, g, b }` | Vue 2 |
|
|
142
|
+
| update:modelValue | Same as `input`, used by `v-model` | `{ r, g, b }` | Vue 3 |
|
|
143
|
+
| change | Fired when a pick gesture (press then release) ends | `{ r, g, b }` | both |
|
|
144
|
+
|
|
145
|
+
Both `input` and `update:modelValue` are always emitted, so the unused one is simply ignored.
|
|
146
|
+
|
|
147
|
+
### Notes
|
|
148
|
+
|
|
149
|
+
- The disc uses the HSV model: hue comes from the angle, saturation from the distance to the center ratio, value is fixed to maximum, and the center is white
|
|
150
|
+
- `width` / `height` are logical canvas sizes in CSS pixels. The disc radius is `min(width, height) / 2 - 18` and the origin is centered; they do not have to be equal
|
|
151
|
+
- The canvas is rendered with `devicePixelRatio` scaling, so it stays sharp on high-DPI screens
|
|
152
|
+
- Pointer coordinates are converted back to logical canvas coordinates: even if the canvas is scaled by external CSS (e.g. a global `canvas { max-width: 100% }`, a squeezed parent, or a `transform` scale), the picked position still matches the visual position
|
|
153
|
+
- Clicking or dragging outside the disc pins the selector to the circumference and takes the color at that point
|
|
154
|
+
- Built on Pointer Events, so mouse and touch behave the same; dragging uses `setPointerCapture`, so it continues even when the pointer leaves the canvas
|
|
155
|
+
- Styles are shipped as plain CSS scoped by the `.canvas-color-disc` ancestor selector; import the matching `style.css` once per app
|
|
156
|
+
|
|
157
|
+
## Local development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# install dependencies
|
|
161
|
+
npm install
|
|
162
|
+
|
|
163
|
+
# serve the Vue 2 demo at localhost:5173
|
|
164
|
+
npm run dev
|
|
165
|
+
|
|
166
|
+
# build both Vue 2 (dist/) and Vue 3 (dist/vue3/) bundles
|
|
167
|
+
npm run build
|
|
168
|
+
|
|
169
|
+
# build a single target
|
|
170
|
+
npm run build:vue2
|
|
171
|
+
npm run build:vue3
|
|
172
|
+
```
|
|
74
173
|
|
|
75
|
-
|
|
174
|
+
## License
|
|
76
175
|
|
|
77
|
-
|
|
78
|
-
- `width`/`height` 为画布逻辑尺寸(CSS 像素),色盘半径自动按 `min(width, height) / 2 - 18` 适配,圆心居中
|
|
79
|
-
- 取色坐标会自动换算回画布逻辑坐标:即使画布被外部 CSS 缩放(如全局 `canvas { max-width: 100% }`、父容器压缩、transform 缩放),取色位置仍与视觉位置一致
|
|
80
|
-
- 点击或拖动超出色盘范围时,选择器会钉在圆周上(取圆周对应位置的颜色)
|
|
81
|
-
- 基于 Pointer Events,鼠标与触摸的操作方式一致
|
|
176
|
+
[MIT](./LICENSE)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
var T = Object.defineProperty;
|
|
2
|
-
var
|
|
3
|
-
var U = Object.prototype.hasOwnProperty,
|
|
4
|
-
var
|
|
2
|
+
var P = Object.getOwnPropertySymbols;
|
|
3
|
+
var U = Object.prototype.hasOwnProperty, V = Object.prototype.propertyIsEnumerable;
|
|
4
|
+
var _ = (t, e, s) => e in t ? T(t, e, { enumerable: !0, configurable: !0, writable: !0, value: s }) : t[e] = s, R = (t, e) => {
|
|
5
5
|
for (var s in e || (e = {}))
|
|
6
|
-
U.call(e, s) &&
|
|
7
|
-
if (
|
|
8
|
-
for (var s of
|
|
9
|
-
|
|
6
|
+
U.call(e, s) && _(t, s, e[s]);
|
|
7
|
+
if (P)
|
|
8
|
+
for (var s of P(e))
|
|
9
|
+
V.call(e, s) && _(t, s, e[s]);
|
|
10
10
|
return t;
|
|
11
11
|
};
|
|
12
12
|
function D(t, e, s) {
|
|
@@ -37,7 +37,7 @@ function D(t, e, s) {
|
|
|
37
37
|
b: Math.round(l * 255)
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
-
function
|
|
40
|
+
function k(t, e, s) {
|
|
41
41
|
const o = t / 255, n = e / 255, i = s / 255, a = Math.max(o, n, i), c = Math.min(o, n, i), r = a - c;
|
|
42
42
|
let h = 0;
|
|
43
43
|
return r !== 0 && (a === o ? h = 60 * ((n - i) / r % 6) : a === n ? h = 60 * ((i - o) / r + 2) : h = 60 * ((o - n) / r + 4), h < 0 && (h += 360)), {
|
|
@@ -46,7 +46,7 @@ function z(t, e, s) {
|
|
|
46
46
|
v: a * 100
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
function
|
|
49
|
+
function z(t, e, s, o, n) {
|
|
50
50
|
const i = t - s, a = e - o, c = Math.sqrt(i * i + a * a);
|
|
51
51
|
let r = -Math.atan2(a, i) * 180 / Math.PI;
|
|
52
52
|
return r < 0 && (r += 360), {
|
|
@@ -55,41 +55,41 @@ function $(t, e, s, o, n) {
|
|
|
55
55
|
dist: c
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
-
function
|
|
58
|
+
function $(t, e, s, o, n) {
|
|
59
59
|
const i = -t * Math.PI / 180, a = e / 100 * n;
|
|
60
60
|
return {
|
|
61
61
|
x: s + Math.cos(i) * a,
|
|
62
62
|
y: o + Math.sin(i) * a
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
-
function
|
|
65
|
+
function A(t, e, s, o, n) {
|
|
66
66
|
const i = t.canvas, a = i.width, c = i.height, r = e * n, h = s * n, g = o * n, u = g * g, d = new Array(360);
|
|
67
67
|
for (let f = 0; f < 360; f++)
|
|
68
68
|
d[f] = D(f, 100, 100);
|
|
69
69
|
const l = t.createImageData(a, c), v = l.data;
|
|
70
70
|
for (let f = 0; f < c; f++) {
|
|
71
71
|
const m = f - h, O = m * m;
|
|
72
|
-
for (let
|
|
73
|
-
const
|
|
72
|
+
for (let p = 0; p < a; p++) {
|
|
73
|
+
const C = p - r, x = C * C + O;
|
|
74
74
|
if (x > u)
|
|
75
75
|
continue;
|
|
76
|
-
const
|
|
77
|
-
let
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
v[
|
|
76
|
+
const b = (f * a + p) * 4, w = Math.sqrt(x) / g;
|
|
77
|
+
let y = -Math.atan2(m, C) * 180 / Math.PI;
|
|
78
|
+
y < 0 && (y += 360);
|
|
79
|
+
const I = d[Math.round(y) % 360];
|
|
80
|
+
v[b] = 255 + (I.r - 255) * w, v[b + 1] = 255 + (I.g - 255) * w, v[b + 2] = 255 + (I.b - 255) * w, v[b + 3] = 255;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
t.putImageData(l, 0, 0);
|
|
84
84
|
}
|
|
85
|
-
function
|
|
85
|
+
function X(t, e, s, o, n, i, a, c) {
|
|
86
86
|
var r = typeof t == "function" ? t.options : t;
|
|
87
|
-
return e && (r.render = e, r.staticRenderFns = s, r._compiled = !0),
|
|
87
|
+
return e && (r.render = e, r.staticRenderFns = s, r._compiled = !0), {
|
|
88
88
|
exports: t,
|
|
89
89
|
options: r
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
|
-
const
|
|
92
|
+
const Y = 15, q = 12, E = 18, N = 8;
|
|
93
93
|
function S(t) {
|
|
94
94
|
const e = t || {};
|
|
95
95
|
return {
|
|
@@ -98,17 +98,23 @@ function S(t) {
|
|
|
98
98
|
b: typeof e.b == "number" ? e.b : 255
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
|
-
function
|
|
101
|
+
function F(t, e) {
|
|
102
102
|
return !!t && !!e && t.r === e.r && t.g === e.g && t.b === e.b;
|
|
103
103
|
}
|
|
104
104
|
const j = {
|
|
105
105
|
name: "CanvasColorDisc",
|
|
106
106
|
props: {
|
|
107
|
-
// 当前颜色 {r, g, b}
|
|
107
|
+
// 当前颜色 {r, g, b},Vue 2 下配合 v-model 使用
|
|
108
108
|
value: {
|
|
109
109
|
type: Object,
|
|
110
110
|
default: () => ({ r: 255, g: 255, b: 255 })
|
|
111
111
|
},
|
|
112
|
+
// 当前颜色 {r, g, b},Vue 3 下配合 v-model 使用
|
|
113
|
+
// 默认值为 undefined 用于区分「Vue 2 的 value 模式」与「Vue 3 的 modelValue 模式」
|
|
114
|
+
modelValue: {
|
|
115
|
+
type: Object,
|
|
116
|
+
default: void 0
|
|
117
|
+
},
|
|
112
118
|
// 画布宽高(CSS 像素)
|
|
113
119
|
width: {
|
|
114
120
|
type: Number,
|
|
@@ -121,7 +127,7 @@ const j = {
|
|
|
121
127
|
},
|
|
122
128
|
data() {
|
|
123
129
|
return {
|
|
124
|
-
rgb: S(this.
|
|
130
|
+
rgb: S(this.activeValue()),
|
|
125
131
|
centerX: 0,
|
|
126
132
|
centerY: 0,
|
|
127
133
|
radius: 0,
|
|
@@ -132,7 +138,13 @@ const j = {
|
|
|
132
138
|
value: {
|
|
133
139
|
deep: !0,
|
|
134
140
|
handler(t) {
|
|
135
|
-
|
|
141
|
+
this.modelValue === void 0 && this.applyPropValue(t);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
modelValue: {
|
|
145
|
+
deep: !0,
|
|
146
|
+
handler(t) {
|
|
147
|
+
this.applyPropValue(t);
|
|
136
148
|
}
|
|
137
149
|
},
|
|
138
150
|
width() {
|
|
@@ -147,10 +159,26 @@ const j = {
|
|
|
147
159
|
this.canvasRect = null;
|
|
148
160
|
}), this.canvasResizeObserver.observe(this.$refs.selectorCanvas)), this.rebuildCanvas();
|
|
149
161
|
},
|
|
162
|
+
// Vue 2 生命周期
|
|
150
163
|
beforeDestroy() {
|
|
151
|
-
this.
|
|
164
|
+
this.destroy();
|
|
165
|
+
},
|
|
166
|
+
// Vue 3 生命周期(Vue 2 下该选项会被忽略)
|
|
167
|
+
beforeUnmount() {
|
|
168
|
+
this.destroy();
|
|
152
169
|
},
|
|
153
170
|
methods: {
|
|
171
|
+
destroy() {
|
|
172
|
+
this.rafId && (cancelAnimationFrame(this.rafId), this.rafId = 0), this.canvasResizeObserver && (this.canvasResizeObserver.disconnect(), this.canvasResizeObserver = null);
|
|
173
|
+
},
|
|
174
|
+
// Vue 2 用 value,Vue 3 用 modelValue
|
|
175
|
+
activeValue() {
|
|
176
|
+
return this.modelValue === void 0 ? this.value : this.modelValue;
|
|
177
|
+
},
|
|
178
|
+
// 外部修改绑定值时同步内部颜色
|
|
179
|
+
applyPropValue(t) {
|
|
180
|
+
F(t, this.rgb) || (this.rgb = S(t), this.drawSelector());
|
|
181
|
+
},
|
|
154
182
|
// 初始化画布尺寸并重绘色盘与选择器
|
|
155
183
|
rebuildCanvas() {
|
|
156
184
|
this.$nextTick(() => {
|
|
@@ -159,17 +187,19 @@ const j = {
|
|
|
159
187
|
},
|
|
160
188
|
setupCanvas() {
|
|
161
189
|
const t = window.devicePixelRatio || 1, e = this.$refs.discCanvas, s = this.$refs.selectorCanvas;
|
|
190
|
+
if (!e || !s)
|
|
191
|
+
return;
|
|
162
192
|
[e, s].forEach((n) => {
|
|
163
193
|
n.style.width = this.width + "px", n.style.height = this.height + "px", n.width = Math.round(this.width * t), n.height = Math.round(this.height * t);
|
|
164
|
-
}), this.centerX = this.width / 2, this.centerY = this.height / 2, this.radius = Math.max(Math.min(this.width, this.height) / 2 -
|
|
194
|
+
}), this.centerX = this.width / 2, this.centerY = this.height / 2, this.radius = Math.max(Math.min(this.width, this.height) / 2 - E, 1), this.discCtx = e.getContext("2d"), this.selectorCtx = s.getContext("2d"), this.discCtx.setTransform(t, 0, 0, t, 0, 0), this.selectorCtx.setTransform(t, 0, 0, t, 0, 0), A(this.discCtx, this.centerX, this.centerY, this.radius, t), this.canvasRect = null;
|
|
165
195
|
},
|
|
166
196
|
// 绘制选择器圆环
|
|
167
197
|
drawSelector() {
|
|
168
198
|
const t = this.selectorCtx;
|
|
169
199
|
if (!t)
|
|
170
200
|
return;
|
|
171
|
-
const e =
|
|
172
|
-
t.clearRect(0, 0, this.width, this.height), t.save(), t.shadowOffsetX = 1, t.shadowOffsetY = 3, t.shadowColor = "rgba(100, 100, 100, 0.5)", t.shadowBlur = 5, t.beginPath(), t.arc(s.x, s.y,
|
|
201
|
+
const e = k(this.rgb.r, this.rgb.g, this.rgb.b), s = $(e.h, e.s, this.centerX, this.centerY, this.radius);
|
|
202
|
+
t.clearRect(0, 0, this.width, this.height), t.save(), t.shadowOffsetX = 1, t.shadowOffsetY = 3, t.shadowColor = "rgba(100, 100, 100, 0.5)", t.shadowBlur = 5, t.beginPath(), t.arc(s.x, s.y, Y, 0, Math.PI * 2), t.fillStyle = "#eee", t.fill(), t.restore(), t.save(), t.beginPath(), t.arc(s.x, s.y, q, 0, Math.PI * 2), t.fillStyle = "rgb(" + this.rgb.r + ", " + this.rgb.g + ", " + this.rgb.b + ")", t.fill(), t.restore();
|
|
173
203
|
},
|
|
174
204
|
onPointerDown(t) {
|
|
175
205
|
t.preventDefault();
|
|
@@ -185,7 +215,7 @@ const j = {
|
|
|
185
215
|
this.dragging && this.pickColor(t.clientX, t.clientY);
|
|
186
216
|
},
|
|
187
217
|
onPointerUp() {
|
|
188
|
-
this.dragging && (this.dragging = !1, this.flushUpdate(), this.$emit("change",
|
|
218
|
+
this.dragging && (this.dragging = !1, this.flushUpdate(), this.$emit("change", R({}, this.rgb)));
|
|
189
219
|
},
|
|
190
220
|
// 根据触点坐标更新当前颜色
|
|
191
221
|
pickColor(t, e) {
|
|
@@ -193,36 +223,38 @@ const j = {
|
|
|
193
223
|
const s = this.canvasRect;
|
|
194
224
|
if (!s || s.width === 0 || s.height === 0)
|
|
195
225
|
return;
|
|
196
|
-
const o = (t - s.left) * this.width / s.width, n = (e - s.top) * this.height / s.height, i =
|
|
197
|
-
this.rgb = i.dist <
|
|
226
|
+
const o = (t - s.left) * this.width / s.width, n = (e - s.top) * this.height / s.height, i = z(o, n, this.centerX, this.centerY, this.radius);
|
|
227
|
+
this.rgb = i.dist < N ? { r: 255, g: 255, b: 255 } : D(i.h, i.s, 100), this.scheduleUpdate();
|
|
198
228
|
},
|
|
199
229
|
// 用 requestAnimationFrame 合并高频的 pointermove 更新
|
|
200
230
|
scheduleUpdate() {
|
|
201
231
|
this.rafId || (this.rafId = requestAnimationFrame(() => {
|
|
202
|
-
this.rafId = 0, this.drawSelector(), this
|
|
232
|
+
this.rafId = 0, this.drawSelector(), this.emitInput();
|
|
203
233
|
}));
|
|
204
234
|
},
|
|
205
235
|
// 立即提交尚未执行的更新(交互结束时调用)
|
|
206
236
|
flushUpdate() {
|
|
207
|
-
this.rafId && (cancelAnimationFrame(this.rafId), this.rafId = 0, this.drawSelector(), this
|
|
237
|
+
this.rafId && (cancelAnimationFrame(this.rafId), this.rafId = 0, this.drawSelector(), this.emitInput());
|
|
238
|
+
},
|
|
239
|
+
// 同时派发 Vue 2 与 Vue 3 的 v-model 事件,未监听的一方会被忽略
|
|
240
|
+
emitInput() {
|
|
241
|
+
const t = R({}, this.rgb);
|
|
242
|
+
this.$emit("input", t), this.$emit("update:modelValue", t);
|
|
208
243
|
}
|
|
209
244
|
}
|
|
210
245
|
};
|
|
211
|
-
var
|
|
246
|
+
var B = function() {
|
|
212
247
|
var e = this, s = e._self._c;
|
|
213
248
|
return s("div", { staticClass: "canvas-color-disc" }, [s("canvas", { ref: "discCanvas", staticClass: "disc-canvas", attrs: { width: e.width, height: e.height } }), s("canvas", { ref: "selectorCanvas", staticClass: "disc-canvas disc-selector", attrs: { width: e.width, height: e.height }, on: { pointerdown: e.onPointerDown, pointermove: e.onPointerMove, pointerup: e.onPointerUp, pointercancel: e.onPointerUp } })]);
|
|
214
|
-
},
|
|
249
|
+
}, H = [], L = /* @__PURE__ */ X(
|
|
215
250
|
j,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
!1,
|
|
219
|
-
null,
|
|
220
|
-
"e9569c49"
|
|
251
|
+
B,
|
|
252
|
+
H
|
|
221
253
|
);
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
t.component(
|
|
254
|
+
const M = L.exports;
|
|
255
|
+
M.install = (t) => {
|
|
256
|
+
t.component(M.name, M);
|
|
225
257
|
};
|
|
226
258
|
export {
|
|
227
|
-
|
|
259
|
+
M as default
|
|
228
260
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(c,h){typeof exports=="object"&&typeof module!="undefined"?module.exports=h():typeof define=="function"&&define.amd?define(h):(c=typeof globalThis!="undefined"?globalThis:c||self,c.CanvasColorDisc=h())})(this,function(){"use strict";var
|
|
1
|
+
(function(c,h){typeof exports=="object"&&typeof module!="undefined"?module.exports=h():typeof define=="function"&&define.amd?define(h):(c=typeof globalThis!="undefined"?globalThis:c||self,c.CanvasColorDisc=h())})(this,function(){"use strict";var B=Object.defineProperty;var T=Object.getOwnPropertySymbols;var H=Object.prototype.hasOwnProperty,L=Object.prototype.propertyIsEnumerable;var O=(c,h,d)=>h in c?B(c,h,{enumerable:!0,configurable:!0,writable:!0,value:d}):c[h]=d,_=(c,h)=>{for(var d in h||(h={}))H.call(h,d)&&O(c,d,h[d]);if(T)for(var d of T(h))L.call(h,d)&&O(c,d,h[d]);return c};function c(t,e,s){const o=(t%360+360)%360,n=e/100,i=s/100,a=Math.floor(o/60),u=o/60-a,r=i*(1-n),l=i*(1-n*u),b=i*(1-n*(1-u));let v=0,f=0,g=0;switch(a){case 0:v=i,f=b,g=r;break;case 1:v=l,f=i,g=r;break;case 2:v=r,f=i,g=b;break;case 3:v=r,f=l,g=i;break;case 4:v=b,f=r,g=i;break;default:v=i,f=r,g=l}return{r:Math.round(v*255),g:Math.round(f*255),b:Math.round(g*255)}}function h(t,e,s){const o=t/255,n=e/255,i=s/255,a=Math.max(o,n,i),u=Math.min(o,n,i),r=a-u;let l=0;return r!==0&&(a===o?l=60*((n-i)/r%6):a===n?l=60*((i-o)/r+2):l=60*((o-n)/r+4),l<0&&(l+=360)),{h:l,s:a===0?0:r/a*100,v:a*100}}function d(t,e,s,o,n){const i=t-s,a=e-o,u=Math.sqrt(i*i+a*a);let r=-Math.atan2(a,i)*180/Math.PI;return r<0&&(r+=360),{h:r,s:n===0?0:Math.min(u/n,1)*100,dist:u}}function U(t,e,s,o,n){const i=-t*Math.PI/180,a=e/100*n;return{x:s+Math.cos(i)*a,y:o+Math.sin(i)*a}}function V(t,e,s,o,n){const i=t.canvas,a=i.width,u=i.height,r=e*n,l=s*n,b=o*n,v=b*b,f=new Array(360);for(let p=0;p<360;p++)f[p]=c(p,100,100);const g=t.createImageData(a,u),C=g.data;for(let p=0;p<u;p++){const I=p-l,j=I*I;for(let w=0;w<a;w++){const R=w-r,D=R*R+j;if(D>v)continue;const y=(p*a+w)*4,M=Math.sqrt(D)/b;let x=-Math.atan2(I,R)*180/Math.PI;x<0&&(x+=360);const P=f[Math.round(x)%360];C[y]=255+(P.r-255)*M,C[y+1]=255+(P.g-255)*M,C[y+2]=255+(P.b-255)*M,C[y+3]=255}}t.putImageData(g,0,0)}function k(t,e,s,o,n,i,a,u){var r=typeof t=="function"?t.options:t;return e&&(r.render=e,r.staticRenderFns=s,r._compiled=!0),{exports:t,options:r}}const z=15,$=12,A=18,X=8;function S(t){const e=t||{};return{r:typeof e.r=="number"?e.r:255,g:typeof e.g=="number"?e.g:255,b:typeof e.b=="number"?e.b:255}}function Y(t,e){return!!t&&!!e&&t.r===e.r&&t.g===e.g&&t.b===e.b}const q={name:"CanvasColorDisc",props:{value:{type:Object,default:()=>({r:255,g:255,b:255})},modelValue:{type:Object,default:void 0},width:{type:Number,default:300},height:{type:Number,default:300}},data(){return{rgb:S(this.activeValue()),centerX:0,centerY:0,radius:0,dragging:!1}},watch:{value:{deep:!0,handler(t){this.modelValue===void 0&&this.applyPropValue(t)}},modelValue:{deep:!0,handler(t){this.applyPropValue(t)}},width(){this.rebuildCanvas()},height(){this.rebuildCanvas()}},mounted(){this.canvasRect=null,this.rafId=0,typeof ResizeObserver!="undefined"&&(this.canvasResizeObserver=new ResizeObserver(()=>{this.canvasRect=null}),this.canvasResizeObserver.observe(this.$refs.selectorCanvas)),this.rebuildCanvas()},beforeDestroy(){this.destroy()},beforeUnmount(){this.destroy()},methods:{destroy(){this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=0),this.canvasResizeObserver&&(this.canvasResizeObserver.disconnect(),this.canvasResizeObserver=null)},activeValue(){return this.modelValue===void 0?this.value:this.modelValue},applyPropValue(t){Y(t,this.rgb)||(this.rgb=S(t),this.drawSelector())},rebuildCanvas(){this.$nextTick(()=>{this.setupCanvas(),this.drawSelector()})},setupCanvas(){const t=window.devicePixelRatio||1,e=this.$refs.discCanvas,s=this.$refs.selectorCanvas;if(!e||!s)return;[e,s].forEach(n=>{n.style.width=this.width+"px",n.style.height=this.height+"px",n.width=Math.round(this.width*t),n.height=Math.round(this.height*t)}),this.centerX=this.width/2,this.centerY=this.height/2,this.radius=Math.max(Math.min(this.width,this.height)/2-A,1),this.discCtx=e.getContext("2d"),this.selectorCtx=s.getContext("2d"),this.discCtx.setTransform(t,0,0,t,0,0),this.selectorCtx.setTransform(t,0,0,t,0,0),V(this.discCtx,this.centerX,this.centerY,this.radius,t),this.canvasRect=null},drawSelector(){const t=this.selectorCtx;if(!t)return;const e=h(this.rgb.r,this.rgb.g,this.rgb.b),s=U(e.h,e.s,this.centerX,this.centerY,this.radius);t.clearRect(0,0,this.width,this.height),t.save(),t.shadowOffsetX=1,t.shadowOffsetY=3,t.shadowColor="rgba(100, 100, 100, 0.5)",t.shadowBlur=5,t.beginPath(),t.arc(s.x,s.y,z,0,Math.PI*2),t.fillStyle="#eee",t.fill(),t.restore(),t.save(),t.beginPath(),t.arc(s.x,s.y,$,0,Math.PI*2),t.fillStyle="rgb("+this.rgb.r+", "+this.rgb.g+", "+this.rgb.b+")",t.fill(),t.restore()},onPointerDown(t){t.preventDefault();const e=this.$refs.selectorCanvas;this.canvasRect=e.getBoundingClientRect(),this.dragging=!0;try{e.setPointerCapture(t.pointerId)}catch(s){}this.pickColor(t.clientX,t.clientY)},onPointerMove(t){this.dragging&&this.pickColor(t.clientX,t.clientY)},onPointerUp(){this.dragging&&(this.dragging=!1,this.flushUpdate(),this.$emit("change",_({},this.rgb)))},pickColor(t,e){this.canvasRect||(this.canvasRect=this.$refs.selectorCanvas.getBoundingClientRect());const s=this.canvasRect;if(!s||s.width===0||s.height===0)return;const o=(t-s.left)*this.width/s.width,n=(e-s.top)*this.height/s.height,i=d(o,n,this.centerX,this.centerY,this.radius);this.rgb=i.dist<X?{r:255,g:255,b:255}:c(i.h,i.s,100),this.scheduleUpdate()},scheduleUpdate(){this.rafId||(this.rafId=requestAnimationFrame(()=>{this.rafId=0,this.drawSelector(),this.emitInput()}))},flushUpdate(){this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=0,this.drawSelector(),this.emitInput())},emitInput(){const t=_({},this.rgb);this.$emit("input",t),this.$emit("update:modelValue",t)}}};var E=function(){var e=this,s=e._self._c;return s("div",{staticClass:"canvas-color-disc"},[s("canvas",{ref:"discCanvas",staticClass:"disc-canvas",attrs:{width:e.width,height:e.height}}),s("canvas",{ref:"selectorCanvas",staticClass:"disc-canvas disc-selector",attrs:{width:e.width,height:e.height},on:{pointerdown:e.onPointerDown,pointermove:e.onPointerMove,pointerup:e.onPointerUp,pointercancel:e.onPointerUp}})])},N=[],F=k(q,E,N);const m=F.exports;return m.install=t=>{t.component(m.name,m)},m});
|
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.canvas-color-disc
|
|
1
|
+
.canvas-color-disc{position:relative;display:inline-block}.canvas-color-disc .disc-canvas{display:block}.canvas-color-disc .disc-selector{position:absolute;top:0;left:0;cursor:crosshair;touch-action:none;-webkit-user-select:none;user-select:none}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
var V = Object.defineProperty;
|
|
2
|
+
var D = Object.getOwnPropertySymbols;
|
|
3
|
+
var E = Object.prototype.hasOwnProperty, T = Object.prototype.propertyIsEnumerable;
|
|
4
|
+
var S = (t, e, s) => e in t ? V(t, e, { enumerable: !0, configurable: !0, writable: !0, value: s }) : t[e] = s, R = (t, e) => {
|
|
5
|
+
for (var s in e || (e = {}))
|
|
6
|
+
E.call(e, s) && S(t, s, e[s]);
|
|
7
|
+
if (D)
|
|
8
|
+
for (var s of D(e))
|
|
9
|
+
T.call(e, s) && S(t, s, e[s]);
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { openBlock as $, createElementBlock as z, createElementVNode as x } from "vue";
|
|
13
|
+
function U(t, e, s) {
|
|
14
|
+
const o = (t % 360 + 360) % 360, n = e / 100, i = s / 100, r = Math.floor(o / 60), c = o / 60 - r, a = i * (1 - n), h = i * (1 - n * c), g = i * (1 - n * (1 - c));
|
|
15
|
+
let u = 0, d = 0, l = 0;
|
|
16
|
+
switch (r) {
|
|
17
|
+
case 0:
|
|
18
|
+
u = i, d = g, l = a;
|
|
19
|
+
break;
|
|
20
|
+
case 1:
|
|
21
|
+
u = h, d = i, l = a;
|
|
22
|
+
break;
|
|
23
|
+
case 2:
|
|
24
|
+
u = a, d = i, l = g;
|
|
25
|
+
break;
|
|
26
|
+
case 3:
|
|
27
|
+
u = a, d = h, l = i;
|
|
28
|
+
break;
|
|
29
|
+
case 4:
|
|
30
|
+
u = g, d = a, l = i;
|
|
31
|
+
break;
|
|
32
|
+
default:
|
|
33
|
+
u = i, d = a, l = h;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
r: Math.round(u * 255),
|
|
37
|
+
g: Math.round(d * 255),
|
|
38
|
+
b: Math.round(l * 255)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function A(t, e, s) {
|
|
42
|
+
const o = t / 255, n = e / 255, i = s / 255, r = Math.max(o, n, i), c = Math.min(o, n, i), a = r - c;
|
|
43
|
+
let h = 0;
|
|
44
|
+
return a !== 0 && (r === o ? h = 60 * ((n - i) / a % 6) : r === n ? h = 60 * ((i - o) / a + 2) : h = 60 * ((o - n) / a + 4), h < 0 && (h += 360)), {
|
|
45
|
+
h,
|
|
46
|
+
s: r === 0 ? 0 : a / r * 100,
|
|
47
|
+
v: r * 100
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function X(t, e, s, o, n) {
|
|
51
|
+
const i = t - s, r = e - o, c = Math.sqrt(i * i + r * r);
|
|
52
|
+
let a = -Math.atan2(r, i) * 180 / Math.PI;
|
|
53
|
+
return a < 0 && (a += 360), {
|
|
54
|
+
h: a,
|
|
55
|
+
s: n === 0 ? 0 : Math.min(c / n, 1) * 100,
|
|
56
|
+
dist: c
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function Y(t, e, s, o, n) {
|
|
60
|
+
const i = -t * Math.PI / 180, r = e / 100 * n;
|
|
61
|
+
return {
|
|
62
|
+
x: s + Math.cos(i) * r,
|
|
63
|
+
y: o + Math.sin(i) * r
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function q(t, e, s, o, n) {
|
|
67
|
+
const i = t.canvas, r = i.width, c = i.height, a = e * n, h = s * n, g = o * n, u = g * g, d = new Array(360);
|
|
68
|
+
for (let f = 0; f < 360; f++)
|
|
69
|
+
d[f] = U(f, 100, 100);
|
|
70
|
+
const l = t.createImageData(r, c), p = l.data;
|
|
71
|
+
for (let f = 0; f < c; f++) {
|
|
72
|
+
const w = f - h, k = w * w;
|
|
73
|
+
for (let b = 0; b < r; b++) {
|
|
74
|
+
const C = b - a, M = C * C + k;
|
|
75
|
+
if (M > u)
|
|
76
|
+
continue;
|
|
77
|
+
const m = (f * r + b) * 4, y = Math.sqrt(M) / g;
|
|
78
|
+
let P = -Math.atan2(w, C) * 180 / Math.PI;
|
|
79
|
+
P < 0 && (P += 360);
|
|
80
|
+
const I = d[Math.round(P) % 360];
|
|
81
|
+
p[m] = 255 + (I.r - 255) * y, p[m + 1] = 255 + (I.g - 255) * y, p[m + 2] = 255 + (I.b - 255) * y, p[m + 3] = 255;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
t.putImageData(l, 0, 0);
|
|
85
|
+
}
|
|
86
|
+
const N = 15, B = 12, _ = 18, F = 8;
|
|
87
|
+
function O(t) {
|
|
88
|
+
const e = t || {};
|
|
89
|
+
return {
|
|
90
|
+
r: typeof e.r == "number" ? e.r : 255,
|
|
91
|
+
g: typeof e.g == "number" ? e.g : 255,
|
|
92
|
+
b: typeof e.b == "number" ? e.b : 255
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function j(t, e) {
|
|
96
|
+
return !!t && !!e && t.r === e.r && t.g === e.g && t.b === e.b;
|
|
97
|
+
}
|
|
98
|
+
const v = {
|
|
99
|
+
name: "CanvasColorDisc",
|
|
100
|
+
props: {
|
|
101
|
+
// 当前颜色 {r, g, b},Vue 2 下配合 v-model 使用
|
|
102
|
+
value: {
|
|
103
|
+
type: Object,
|
|
104
|
+
default: () => ({ r: 255, g: 255, b: 255 })
|
|
105
|
+
},
|
|
106
|
+
// 当前颜色 {r, g, b},Vue 3 下配合 v-model 使用
|
|
107
|
+
// 默认值为 undefined 用于区分「Vue 2 的 value 模式」与「Vue 3 的 modelValue 模式」
|
|
108
|
+
modelValue: {
|
|
109
|
+
type: Object,
|
|
110
|
+
default: void 0
|
|
111
|
+
},
|
|
112
|
+
// 画布宽高(CSS 像素)
|
|
113
|
+
width: {
|
|
114
|
+
type: Number,
|
|
115
|
+
default: 300
|
|
116
|
+
},
|
|
117
|
+
height: {
|
|
118
|
+
type: Number,
|
|
119
|
+
default: 300
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
data() {
|
|
123
|
+
return {
|
|
124
|
+
rgb: O(this.activeValue()),
|
|
125
|
+
centerX: 0,
|
|
126
|
+
centerY: 0,
|
|
127
|
+
radius: 0,
|
|
128
|
+
dragging: !1
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
watch: {
|
|
132
|
+
value: {
|
|
133
|
+
deep: !0,
|
|
134
|
+
handler(t) {
|
|
135
|
+
this.modelValue === void 0 && this.applyPropValue(t);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
modelValue: {
|
|
139
|
+
deep: !0,
|
|
140
|
+
handler(t) {
|
|
141
|
+
this.applyPropValue(t);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
width() {
|
|
145
|
+
this.rebuildCanvas();
|
|
146
|
+
},
|
|
147
|
+
height() {
|
|
148
|
+
this.rebuildCanvas();
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
mounted() {
|
|
152
|
+
this.canvasRect = null, this.rafId = 0, typeof ResizeObserver != "undefined" && (this.canvasResizeObserver = new ResizeObserver(() => {
|
|
153
|
+
this.canvasRect = null;
|
|
154
|
+
}), this.canvasResizeObserver.observe(this.$refs.selectorCanvas)), this.rebuildCanvas();
|
|
155
|
+
},
|
|
156
|
+
// Vue 2 生命周期
|
|
157
|
+
beforeDestroy() {
|
|
158
|
+
this.destroy();
|
|
159
|
+
},
|
|
160
|
+
// Vue 3 生命周期(Vue 2 下该选项会被忽略)
|
|
161
|
+
beforeUnmount() {
|
|
162
|
+
this.destroy();
|
|
163
|
+
},
|
|
164
|
+
methods: {
|
|
165
|
+
destroy() {
|
|
166
|
+
this.rafId && (cancelAnimationFrame(this.rafId), this.rafId = 0), this.canvasResizeObserver && (this.canvasResizeObserver.disconnect(), this.canvasResizeObserver = null);
|
|
167
|
+
},
|
|
168
|
+
// Vue 2 用 value,Vue 3 用 modelValue
|
|
169
|
+
activeValue() {
|
|
170
|
+
return this.modelValue === void 0 ? this.value : this.modelValue;
|
|
171
|
+
},
|
|
172
|
+
// 外部修改绑定值时同步内部颜色
|
|
173
|
+
applyPropValue(t) {
|
|
174
|
+
j(t, this.rgb) || (this.rgb = O(t), this.drawSelector());
|
|
175
|
+
},
|
|
176
|
+
// 初始化画布尺寸并重绘色盘与选择器
|
|
177
|
+
rebuildCanvas() {
|
|
178
|
+
this.$nextTick(() => {
|
|
179
|
+
this.setupCanvas(), this.drawSelector();
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
setupCanvas() {
|
|
183
|
+
const t = window.devicePixelRatio || 1, e = this.$refs.discCanvas, s = this.$refs.selectorCanvas;
|
|
184
|
+
if (!e || !s)
|
|
185
|
+
return;
|
|
186
|
+
[e, s].forEach((n) => {
|
|
187
|
+
n.style.width = this.width + "px", n.style.height = this.height + "px", n.width = Math.round(this.width * t), n.height = Math.round(this.height * t);
|
|
188
|
+
}), this.centerX = this.width / 2, this.centerY = this.height / 2, this.radius = Math.max(Math.min(this.width, this.height) / 2 - _, 1), this.discCtx = e.getContext("2d"), this.selectorCtx = s.getContext("2d"), this.discCtx.setTransform(t, 0, 0, t, 0, 0), this.selectorCtx.setTransform(t, 0, 0, t, 0, 0), q(this.discCtx, this.centerX, this.centerY, this.radius, t), this.canvasRect = null;
|
|
189
|
+
},
|
|
190
|
+
// 绘制选择器圆环
|
|
191
|
+
drawSelector() {
|
|
192
|
+
const t = this.selectorCtx;
|
|
193
|
+
if (!t)
|
|
194
|
+
return;
|
|
195
|
+
const e = A(this.rgb.r, this.rgb.g, this.rgb.b), s = Y(e.h, e.s, this.centerX, this.centerY, this.radius);
|
|
196
|
+
t.clearRect(0, 0, this.width, this.height), t.save(), t.shadowOffsetX = 1, t.shadowOffsetY = 3, t.shadowColor = "rgba(100, 100, 100, 0.5)", t.shadowBlur = 5, t.beginPath(), t.arc(s.x, s.y, N, 0, Math.PI * 2), t.fillStyle = "#eee", t.fill(), t.restore(), t.save(), t.beginPath(), t.arc(s.x, s.y, B, 0, Math.PI * 2), t.fillStyle = "rgb(" + this.rgb.r + ", " + this.rgb.g + ", " + this.rgb.b + ")", t.fill(), t.restore();
|
|
197
|
+
},
|
|
198
|
+
onPointerDown(t) {
|
|
199
|
+
t.preventDefault();
|
|
200
|
+
const e = this.$refs.selectorCanvas;
|
|
201
|
+
this.canvasRect = e.getBoundingClientRect(), this.dragging = !0;
|
|
202
|
+
try {
|
|
203
|
+
e.setPointerCapture(t.pointerId);
|
|
204
|
+
} catch (s) {
|
|
205
|
+
}
|
|
206
|
+
this.pickColor(t.clientX, t.clientY);
|
|
207
|
+
},
|
|
208
|
+
onPointerMove(t) {
|
|
209
|
+
this.dragging && this.pickColor(t.clientX, t.clientY);
|
|
210
|
+
},
|
|
211
|
+
onPointerUp() {
|
|
212
|
+
this.dragging && (this.dragging = !1, this.flushUpdate(), this.$emit("change", R({}, this.rgb)));
|
|
213
|
+
},
|
|
214
|
+
// 根据触点坐标更新当前颜色
|
|
215
|
+
pickColor(t, e) {
|
|
216
|
+
this.canvasRect || (this.canvasRect = this.$refs.selectorCanvas.getBoundingClientRect());
|
|
217
|
+
const s = this.canvasRect;
|
|
218
|
+
if (!s || s.width === 0 || s.height === 0)
|
|
219
|
+
return;
|
|
220
|
+
const o = (t - s.left) * this.width / s.width, n = (e - s.top) * this.height / s.height, i = X(o, n, this.centerX, this.centerY, this.radius);
|
|
221
|
+
this.rgb = i.dist < F ? { r: 255, g: 255, b: 255 } : U(i.h, i.s, 100), this.scheduleUpdate();
|
|
222
|
+
},
|
|
223
|
+
// 用 requestAnimationFrame 合并高频的 pointermove 更新
|
|
224
|
+
scheduleUpdate() {
|
|
225
|
+
this.rafId || (this.rafId = requestAnimationFrame(() => {
|
|
226
|
+
this.rafId = 0, this.drawSelector(), this.emitInput();
|
|
227
|
+
}));
|
|
228
|
+
},
|
|
229
|
+
// 立即提交尚未执行的更新(交互结束时调用)
|
|
230
|
+
flushUpdate() {
|
|
231
|
+
this.rafId && (cancelAnimationFrame(this.rafId), this.rafId = 0, this.drawSelector(), this.emitInput());
|
|
232
|
+
},
|
|
233
|
+
// 同时派发 Vue 2 与 Vue 3 的 v-model 事件,未监听的一方会被忽略
|
|
234
|
+
emitInput() {
|
|
235
|
+
const t = R({}, this.rgb);
|
|
236
|
+
this.$emit("input", t), this.$emit("update:modelValue", t);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}, H = { class: "canvas-color-disc" }, L = ["width", "height"], G = ["width", "height"];
|
|
240
|
+
function W(t, e) {
|
|
241
|
+
return $(), z("div", H, [
|
|
242
|
+
x("canvas", {
|
|
243
|
+
ref: "discCanvas",
|
|
244
|
+
class: "disc-canvas",
|
|
245
|
+
width: t.width,
|
|
246
|
+
height: t.height
|
|
247
|
+
}, null, 8, L),
|
|
248
|
+
x("canvas", {
|
|
249
|
+
ref: "selectorCanvas",
|
|
250
|
+
class: "disc-canvas disc-selector",
|
|
251
|
+
width: t.width,
|
|
252
|
+
height: t.height,
|
|
253
|
+
onPointerdown: e[0] || (e[0] = (...s) => t.onPointerDown && t.onPointerDown(...s)),
|
|
254
|
+
onPointermove: e[1] || (e[1] = (...s) => t.onPointerMove && t.onPointerMove(...s)),
|
|
255
|
+
onPointerup: e[2] || (e[2] = (...s) => t.onPointerUp && t.onPointerUp(...s)),
|
|
256
|
+
onPointercancel: e[3] || (e[3] = (...s) => t.onPointerUp && t.onPointerUp(...s))
|
|
257
|
+
}, null, 40, G)
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
v.render = W;
|
|
261
|
+
v.__file = "D:/workspace/canvas-color-disc/src/components/CanvasColorDisc.vue";
|
|
262
|
+
v.install = (t) => {
|
|
263
|
+
t.component(v.name, v);
|
|
264
|
+
};
|
|
265
|
+
export {
|
|
266
|
+
v as default
|
|
267
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(a,h){typeof exports=="object"&&typeof module!="undefined"?module.exports=h(require("vue")):typeof define=="function"&&define.amd?define(["vue"],h):(a=typeof globalThis!="undefined"?globalThis:a||self,a.CanvasColorDisc=h(a.Vue))})(this,function(a){"use strict";var F=Object.defineProperty;var O=Object.getOwnPropertySymbols;var H=Object.prototype.hasOwnProperty,L=Object.prototype.propertyIsEnumerable;var T=(a,h,d)=>h in a?F(a,h,{enumerable:!0,configurable:!0,writable:!0,value:d}):a[h]=d,S=(a,h)=>{for(var d in h||(h={}))H.call(h,d)&&T(a,d,h[d]);if(O)for(var d of O(h))L.call(h,d)&&T(a,d,h[d]);return a};function h(t,e,s){const c=(t%360+360)%360,n=e/100,i=s/100,r=Math.floor(c/60),u=c/60-r,o=i*(1-n),l=i*(1-n*u),b=i*(1-n*(1-u));let v=0,f=0,g=0;switch(r){case 0:v=i,f=b,g=o;break;case 1:v=l,f=i,g=o;break;case 2:v=o,f=i,g=b;break;case 3:v=o,f=l,g=i;break;case 4:v=b,f=o,g=i;break;default:v=i,f=o,g=l}return{r:Math.round(v*255),g:Math.round(f*255),b:Math.round(g*255)}}function d(t,e,s){const c=t/255,n=e/255,i=s/255,r=Math.max(c,n,i),u=Math.min(c,n,i),o=r-u;let l=0;return o!==0&&(r===c?l=60*((n-i)/o%6):r===n?l=60*((i-c)/o+2):l=60*((c-n)/o+4),l<0&&(l+=360)),{h:l,s:r===0?0:o/r*100,v:r*100}}function U(t,e,s,c,n){const i=t-s,r=e-c,u=Math.sqrt(i*i+r*r);let o=-Math.atan2(r,i)*180/Math.PI;return o<0&&(o+=360),{h:o,s:n===0?0:Math.min(u/n,1)*100,dist:u}}function k(t,e,s,c,n){const i=-t*Math.PI/180,r=e/100*n;return{x:s+Math.cos(i)*r,y:c+Math.sin(i)*r}}function E(t,e,s,c,n){const i=t.canvas,r=i.width,u=i.height,o=e*n,l=s*n,b=c*n,v=b*b,f=new Array(360);for(let p=0;p<360;p++)f[p]=h(p,100,100);const g=t.createImageData(r,u),C=g.data;for(let p=0;p<u;p++){const P=p-l,j=P*P;for(let w=0;w<r;w++){const I=w-o,V=I*I+j;if(V>v)continue;const y=(p*r+w)*4,R=Math.sqrt(V)/b;let M=-Math.atan2(P,I)*180/Math.PI;M<0&&(M+=360);const D=f[Math.round(M)%360];C[y]=255+(D.r-255)*R,C[y+1]=255+(D.g-255)*R,C[y+2]=255+(D.b-255)*R,C[y+3]=255}}t.putImageData(g,0,0)}const $=15,q=12,z=18,A=8;function x(t){const e=t||{};return{r:typeof e.r=="number"?e.r:255,g:typeof e.g=="number"?e.g:255,b:typeof e.b=="number"?e.b:255}}function N(t,e){return!!t&&!!e&&t.r===e.r&&t.g===e.g&&t.b===e.b}const m={name:"CanvasColorDisc",props:{value:{type:Object,default:()=>({r:255,g:255,b:255})},modelValue:{type:Object,default:void 0},width:{type:Number,default:300},height:{type:Number,default:300}},data(){return{rgb:x(this.activeValue()),centerX:0,centerY:0,radius:0,dragging:!1}},watch:{value:{deep:!0,handler(t){this.modelValue===void 0&&this.applyPropValue(t)}},modelValue:{deep:!0,handler(t){this.applyPropValue(t)}},width(){this.rebuildCanvas()},height(){this.rebuildCanvas()}},mounted(){this.canvasRect=null,this.rafId=0,typeof ResizeObserver!="undefined"&&(this.canvasResizeObserver=new ResizeObserver(()=>{this.canvasRect=null}),this.canvasResizeObserver.observe(this.$refs.selectorCanvas)),this.rebuildCanvas()},beforeDestroy(){this.destroy()},beforeUnmount(){this.destroy()},methods:{destroy(){this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=0),this.canvasResizeObserver&&(this.canvasResizeObserver.disconnect(),this.canvasResizeObserver=null)},activeValue(){return this.modelValue===void 0?this.value:this.modelValue},applyPropValue(t){N(t,this.rgb)||(this.rgb=x(t),this.drawSelector())},rebuildCanvas(){this.$nextTick(()=>{this.setupCanvas(),this.drawSelector()})},setupCanvas(){const t=window.devicePixelRatio||1,e=this.$refs.discCanvas,s=this.$refs.selectorCanvas;if(!e||!s)return;[e,s].forEach(n=>{n.style.width=this.width+"px",n.style.height=this.height+"px",n.width=Math.round(this.width*t),n.height=Math.round(this.height*t)}),this.centerX=this.width/2,this.centerY=this.height/2,this.radius=Math.max(Math.min(this.width,this.height)/2-z,1),this.discCtx=e.getContext("2d"),this.selectorCtx=s.getContext("2d"),this.discCtx.setTransform(t,0,0,t,0,0),this.selectorCtx.setTransform(t,0,0,t,0,0),E(this.discCtx,this.centerX,this.centerY,this.radius,t),this.canvasRect=null},drawSelector(){const t=this.selectorCtx;if(!t)return;const e=d(this.rgb.r,this.rgb.g,this.rgb.b),s=k(e.h,e.s,this.centerX,this.centerY,this.radius);t.clearRect(0,0,this.width,this.height),t.save(),t.shadowOffsetX=1,t.shadowOffsetY=3,t.shadowColor="rgba(100, 100, 100, 0.5)",t.shadowBlur=5,t.beginPath(),t.arc(s.x,s.y,$,0,Math.PI*2),t.fillStyle="#eee",t.fill(),t.restore(),t.save(),t.beginPath(),t.arc(s.x,s.y,q,0,Math.PI*2),t.fillStyle="rgb("+this.rgb.r+", "+this.rgb.g+", "+this.rgb.b+")",t.fill(),t.restore()},onPointerDown(t){t.preventDefault();const e=this.$refs.selectorCanvas;this.canvasRect=e.getBoundingClientRect(),this.dragging=!0;try{e.setPointerCapture(t.pointerId)}catch(s){}this.pickColor(t.clientX,t.clientY)},onPointerMove(t){this.dragging&&this.pickColor(t.clientX,t.clientY)},onPointerUp(){this.dragging&&(this.dragging=!1,this.flushUpdate(),this.$emit("change",S({},this.rgb)))},pickColor(t,e){this.canvasRect||(this.canvasRect=this.$refs.selectorCanvas.getBoundingClientRect());const s=this.canvasRect;if(!s||s.width===0||s.height===0)return;const c=(t-s.left)*this.width/s.width,n=(e-s.top)*this.height/s.height,i=U(c,n,this.centerX,this.centerY,this.radius);this.rgb=i.dist<A?{r:255,g:255,b:255}:h(i.h,i.s,100),this.scheduleUpdate()},scheduleUpdate(){this.rafId||(this.rafId=requestAnimationFrame(()=>{this.rafId=0,this.drawSelector(),this.emitInput()}))},flushUpdate(){this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=0,this.drawSelector(),this.emitInput())},emitInput(){const t=S({},this.rgb);this.$emit("input",t),this.$emit("update:modelValue",t)}}},X={class:"canvas-color-disc"},Y=["width","height"],B=["width","height"];function _(t,e){return a.openBlock(),a.createElementBlock("div",X,[a.createElementVNode("canvas",{ref:"discCanvas",class:"disc-canvas",width:t.width,height:t.height},null,8,Y),a.createElementVNode("canvas",{ref:"selectorCanvas",class:"disc-canvas disc-selector",width:t.width,height:t.height,onPointerdown:e[0]||(e[0]=(...s)=>t.onPointerDown&&t.onPointerDown(...s)),onPointermove:e[1]||(e[1]=(...s)=>t.onPointerMove&&t.onPointerMove(...s)),onPointerup:e[2]||(e[2]=(...s)=>t.onPointerUp&&t.onPointerUp(...s)),onPointercancel:e[3]||(e[3]=(...s)=>t.onPointerUp&&t.onPointerUp(...s))},null,40,B)])}return m.render=_,m.__file="D:/workspace/canvas-color-disc/src/components/CanvasColorDisc.vue",m.install=t=>{t.component(m.name,m)},m});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.canvas-color-disc{position:relative;display:inline-block}.canvas-color-disc .disc-canvas{display:block}.canvas-color-disc .disc-selector{position:absolute;top:0;left:0;cursor:crosshair;touch-action:none;-webkit-user-select:none;user-select:none}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvas-color-disc",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Color disc realized by canvas",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "dist/canvas-color-disc.umd.js",
|
|
@@ -10,7 +10,18 @@
|
|
|
10
10
|
"import": "./dist/canvas-color-disc.es.js",
|
|
11
11
|
"require": "./dist/canvas-color-disc.umd.js"
|
|
12
12
|
},
|
|
13
|
-
"./
|
|
13
|
+
"./vue2": {
|
|
14
|
+
"import": "./dist/canvas-color-disc.es.js",
|
|
15
|
+
"require": "./dist/canvas-color-disc.umd.js"
|
|
16
|
+
},
|
|
17
|
+
"./vue3": {
|
|
18
|
+
"import": "./dist/vue3/canvas-color-disc.es.js",
|
|
19
|
+
"require": "./dist/vue3/canvas-color-disc.umd.js"
|
|
20
|
+
},
|
|
21
|
+
"./dist/style.css": "./dist/style.css",
|
|
22
|
+
"./vue2/style.css": "./dist/style.css",
|
|
23
|
+
"./dist/vue3/style.css": "./dist/vue3/style.css",
|
|
24
|
+
"./vue3/style.css": "./dist/vue3/style.css"
|
|
14
25
|
},
|
|
15
26
|
"style": "dist/style.css",
|
|
16
27
|
"files": [
|
|
@@ -23,7 +34,9 @@
|
|
|
23
34
|
"scripts": {
|
|
24
35
|
"dev": "vite",
|
|
25
36
|
"start": "vite",
|
|
26
|
-
"build": "
|
|
37
|
+
"build": "npm run build:vue2 && npm run build:vue3",
|
|
38
|
+
"build:vue2": "vite build",
|
|
39
|
+
"build:vue3": "vite build --config vite.config.vue3.js",
|
|
27
40
|
"preview": "vite preview",
|
|
28
41
|
"lint": "eslint --ext .js,.vue src demo",
|
|
29
42
|
"lintfix": "eslint --fix --ext .js,.vue src demo"
|
|
@@ -40,10 +53,11 @@
|
|
|
40
53
|
"author": "chengzan",
|
|
41
54
|
"license": "MIT",
|
|
42
55
|
"peerDependencies": {
|
|
43
|
-
"vue": "^2.5.0"
|
|
56
|
+
"vue": "^2.5.0 || ^3.0.0"
|
|
44
57
|
},
|
|
45
58
|
"devDependencies": {
|
|
46
59
|
"@vitejs/plugin-vue2": "^2.3.1",
|
|
60
|
+
"@vue/compiler-sfc": "^3.5.13",
|
|
47
61
|
"babel-eslint": "^8.2.1",
|
|
48
62
|
"eslint": "^5.16.0",
|
|
49
63
|
"eslint-plugin-vue": "^5.2.3",
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
</template>
|
|
21
21
|
|
|
22
22
|
<script>
|
|
23
|
+
// 样式独立于 SFC,便于 Vue 2 / Vue 3 两次构建共用(构建产物分别为 dist/style.css、dist/vue3/style.css)
|
|
24
|
+
import '../style.css'
|
|
23
25
|
import {
|
|
24
26
|
discToPoint,
|
|
25
27
|
drawColorDisc,
|
|
@@ -52,11 +54,17 @@ function isSameRgb(a, b) {
|
|
|
52
54
|
export default {
|
|
53
55
|
name: 'CanvasColorDisc',
|
|
54
56
|
props: {
|
|
55
|
-
// 当前颜色 {r, g, b}
|
|
57
|
+
// 当前颜色 {r, g, b},Vue 2 下配合 v-model 使用
|
|
56
58
|
value: {
|
|
57
59
|
type: Object,
|
|
58
60
|
default: () => ({ r: 255, g: 255, b: 255 })
|
|
59
61
|
},
|
|
62
|
+
// 当前颜色 {r, g, b},Vue 3 下配合 v-model 使用
|
|
63
|
+
// 默认值为 undefined 用于区分「Vue 2 的 value 模式」与「Vue 3 的 modelValue 模式」
|
|
64
|
+
modelValue: {
|
|
65
|
+
type: Object,
|
|
66
|
+
default: undefined
|
|
67
|
+
},
|
|
60
68
|
// 画布宽高(CSS 像素)
|
|
61
69
|
width: {
|
|
62
70
|
type: Number,
|
|
@@ -69,7 +77,7 @@ export default {
|
|
|
69
77
|
},
|
|
70
78
|
data() {
|
|
71
79
|
return {
|
|
72
|
-
rgb: normalizeRgb(this.
|
|
80
|
+
rgb: normalizeRgb(this.activeValue()),
|
|
73
81
|
centerX: 0,
|
|
74
82
|
centerY: 0,
|
|
75
83
|
radius: 0,
|
|
@@ -80,12 +88,18 @@ export default {
|
|
|
80
88
|
value: {
|
|
81
89
|
deep: true,
|
|
82
90
|
handler(val) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this.
|
|
91
|
+
// Vue 3 模式下 value 恒为默认值,忽略
|
|
92
|
+
if (this.modelValue === undefined) {
|
|
93
|
+
this.applyPropValue(val)
|
|
86
94
|
}
|
|
87
95
|
}
|
|
88
96
|
},
|
|
97
|
+
modelValue: {
|
|
98
|
+
deep: true,
|
|
99
|
+
handler(val) {
|
|
100
|
+
this.applyPropValue(val)
|
|
101
|
+
}
|
|
102
|
+
},
|
|
89
103
|
width() {
|
|
90
104
|
this.rebuildCanvas()
|
|
91
105
|
},
|
|
@@ -105,17 +119,36 @@ export default {
|
|
|
105
119
|
}
|
|
106
120
|
this.rebuildCanvas()
|
|
107
121
|
},
|
|
122
|
+
// Vue 2 生命周期
|
|
108
123
|
beforeDestroy() {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
this.canvasResizeObserver.disconnect()
|
|
115
|
-
this.canvasResizeObserver = null
|
|
116
|
-
}
|
|
124
|
+
this.destroy()
|
|
125
|
+
},
|
|
126
|
+
// Vue 3 生命周期(Vue 2 下该选项会被忽略)
|
|
127
|
+
beforeUnmount() {
|
|
128
|
+
this.destroy()
|
|
117
129
|
},
|
|
118
130
|
methods: {
|
|
131
|
+
destroy() {
|
|
132
|
+
if (this.rafId) {
|
|
133
|
+
cancelAnimationFrame(this.rafId)
|
|
134
|
+
this.rafId = 0
|
|
135
|
+
}
|
|
136
|
+
if (this.canvasResizeObserver) {
|
|
137
|
+
this.canvasResizeObserver.disconnect()
|
|
138
|
+
this.canvasResizeObserver = null
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
// Vue 2 用 value,Vue 3 用 modelValue
|
|
142
|
+
activeValue() {
|
|
143
|
+
return this.modelValue === undefined ? this.value : this.modelValue
|
|
144
|
+
},
|
|
145
|
+
// 外部修改绑定值时同步内部颜色
|
|
146
|
+
applyPropValue(val) {
|
|
147
|
+
if (!isSameRgb(val, this.rgb)) {
|
|
148
|
+
this.rgb = normalizeRgb(val)
|
|
149
|
+
this.drawSelector()
|
|
150
|
+
}
|
|
151
|
+
},
|
|
119
152
|
// 初始化画布尺寸并重绘色盘与选择器
|
|
120
153
|
rebuildCanvas() {
|
|
121
154
|
this.$nextTick(() => {
|
|
@@ -127,6 +160,10 @@ export default {
|
|
|
127
160
|
const dpr = window.devicePixelRatio || 1
|
|
128
161
|
const discCanvas = this.$refs.discCanvas
|
|
129
162
|
const selectorCanvas = this.$refs.selectorCanvas
|
|
163
|
+
// 挂载后同一 tick 内就被卸载时,nextTick 回调里 $refs 已被清空
|
|
164
|
+
if (!discCanvas || !selectorCanvas) {
|
|
165
|
+
return
|
|
166
|
+
}
|
|
130
167
|
const canvases = [discCanvas, selectorCanvas]
|
|
131
168
|
canvases.forEach((canvas) => {
|
|
132
169
|
canvas.style.width = this.width + 'px'
|
|
@@ -223,7 +260,7 @@ export default {
|
|
|
223
260
|
this.rafId = requestAnimationFrame(() => {
|
|
224
261
|
this.rafId = 0
|
|
225
262
|
this.drawSelector()
|
|
226
|
-
this
|
|
263
|
+
this.emitInput()
|
|
227
264
|
})
|
|
228
265
|
},
|
|
229
266
|
// 立即提交尚未执行的更新(交互结束时调用)
|
|
@@ -234,27 +271,14 @@ export default {
|
|
|
234
271
|
cancelAnimationFrame(this.rafId)
|
|
235
272
|
this.rafId = 0
|
|
236
273
|
this.drawSelector()
|
|
237
|
-
this
|
|
274
|
+
this.emitInput()
|
|
275
|
+
},
|
|
276
|
+
// 同时派发 Vue 2 与 Vue 3 的 v-model 事件,未监听的一方会被忽略
|
|
277
|
+
emitInput() {
|
|
278
|
+
const rgb = { ...this.rgb }
|
|
279
|
+
this.$emit('input', rgb)
|
|
280
|
+
this.$emit('update:modelValue', rgb)
|
|
238
281
|
}
|
|
239
282
|
}
|
|
240
283
|
}
|
|
241
284
|
</script>
|
|
242
|
-
|
|
243
|
-
<style scoped>
|
|
244
|
-
.canvas-color-disc {
|
|
245
|
-
position: relative;
|
|
246
|
-
display: inline-block;
|
|
247
|
-
}
|
|
248
|
-
.disc-canvas {
|
|
249
|
-
display: block;
|
|
250
|
-
}
|
|
251
|
-
.disc-selector {
|
|
252
|
-
position: absolute;
|
|
253
|
-
top: 0;
|
|
254
|
-
left: 0;
|
|
255
|
-
cursor: crosshair;
|
|
256
|
-
touch-action: none;
|
|
257
|
-
-webkit-user-select: none;
|
|
258
|
-
user-select: none;
|
|
259
|
-
}
|
|
260
|
-
</style>
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import CanvasColorDisc from './components/CanvasColorDisc.vue'
|
|
2
2
|
|
|
3
|
+
// Vue 2 下 Vue.use() 传入构造函数,Vue 3 下 app.use() 传入应用实例,
|
|
4
|
+
// 两者的 component() 用法一致,因此无需区分版本
|
|
3
5
|
CanvasColorDisc.install = (Vue) => {
|
|
4
6
|
Vue.component(CanvasColorDisc.name, CanvasColorDisc)
|
|
5
7
|
}
|
|
6
8
|
|
|
9
|
+
// 只保留默认导出:UMD 构建下 window.CanvasColorDisc 才会是组件本身
|
|
7
10
|
export default CanvasColorDisc
|
package/src/style.css
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* 组件样式:独立于 SFC,供 Vue 2 / Vue 3 两次构建共用
|
|
2
|
+
选择器用后代选择器限定,避免未加 scoped 后影响页面其它元素 */
|
|
3
|
+
.canvas-color-disc {
|
|
4
|
+
position: relative;
|
|
5
|
+
display: inline-block;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.canvas-color-disc .disc-canvas {
|
|
9
|
+
display: block;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.canvas-color-disc .disc-selector {
|
|
13
|
+
position: absolute;
|
|
14
|
+
top: 0;
|
|
15
|
+
left: 0;
|
|
16
|
+
cursor: crosshair;
|
|
17
|
+
touch-action: none;
|
|
18
|
+
-webkit-user-select: none;
|
|
19
|
+
user-select: none;
|
|
20
|
+
}
|