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 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
+ ![avatar](https://udfs.unisiot.com/group1/M00/03/85/rBEBA2FqlXOAfw1hAAECEWBl9Kc837.png)
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
- # canvas-color-disc
1
+ [English](./README.md) | [简体中文](./README.cn.md)
2
2
 
3
- > 一个基于 Vue + Canvas 的圆形色盘选择组件,支持移动端和 PC 端
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
  ![avatar](https://udfs.unisiot.com/group1/M00/03/85/rBEBA2FqlXOAfw1hAAECEWBl9Kc837.png)
7
8
 
8
- <img src="https://udfs.unisiot.com/group1/M00/03/85/rBEBA2FqlXOAfw1hAAECEWBl9Kc837.png" width="50%">
9
+ ## Vue version support
9
10
 
10
- ## Build Setup
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
- ``` bash
13
- # install dependencies
14
- npm install
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
- ``` bash
18
- # serve examples at localhost:5173
19
- npm run dev
24
+ ```bash
25
+ yarn add canvas-color-disc
20
26
  ```
21
27
 
22
- ``` bash
23
- # library build (ES + UMD) to dist/
24
- npm run build
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 | 当前颜色,支持 `v-model` | Object `{ r, g, b }` | `{ r: 255, g: 255, b: 255 }` |
65
- | width | 画布宽度(CSS 像素) | Number | 300 |
66
- | height | 画布高度(CSS 像素) | Number | 300 |
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 | 颜色变化时实时触发(配合 `v-model` 使用) | `{ r, g, b }` |
73
- | change | 一次选择(拖动/点击)结束时触发 | `{ r, g, b }` |
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
- - 色盘为 HSV 模型:色相取角度、饱和度取离圆心的半径比例,明度固定为最高,圆心为白色
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 _ = Object.getOwnPropertySymbols;
3
- var U = Object.prototype.hasOwnProperty, k = Object.prototype.propertyIsEnumerable;
4
- var P = (t, e, s) => e in t ? T(t, e, { enumerable: !0, configurable: !0, writable: !0, value: s }) : t[e] = s, C = (t, e) => {
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) && P(t, s, e[s]);
7
- if (_)
8
- for (var s of _(e))
9
- k.call(e, s) && P(t, s, e[s]);
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 z(t, e, s) {
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 $(t, e, s, o, n) {
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 A(t, e, s, o, n) {
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 X(t, e, s, o, n) {
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 b = 0; b < a; b++) {
73
- const w = b - r, x = w * w + O;
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 p = (f * a + b) * 4, R = Math.sqrt(x) / g;
77
- let I = -Math.atan2(m, w) * 180 / Math.PI;
78
- I < 0 && (I += 360);
79
- const M = d[Math.round(I) % 360];
80
- v[p] = 255 + (M.r - 255) * R, v[p + 1] = 255 + (M.g - 255) * R, v[p + 2] = 255 + (M.b - 255) * R, v[p + 3] = 255;
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 Y(t, e, s, o, n, i, a, c) {
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), r._scopeId = "data-v-" + i, {
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 q = 15, E = 12, N = 18, F = 8;
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 B(t, e) {
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},支持 v-model
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.value),
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
- B(t, this.rgb) || (this.rgb = S(t), this.drawSelector());
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.rafId && (cancelAnimationFrame(this.rafId), this.rafId = 0), this.canvasResizeObserver && (this.canvasResizeObserver.disconnect(), this.canvasResizeObserver = null);
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 - N, 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), X(this.discCtx, this.centerX, this.centerY, this.radius, t), this.canvasRect = null;
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 = z(this.rgb.r, this.rgb.g, this.rgb.b), s = A(e.h, e.s, this.centerX, this.centerY, this.radius);
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, q, 0, Math.PI * 2), t.fillStyle = "#eee", t.fill(), t.restore(), t.save(), t.beginPath(), t.arc(s.x, s.y, E, 0, Math.PI * 2), t.fillStyle = "rgb(" + this.rgb.r + ", " + this.rgb.g + ", " + this.rgb.b + ")", t.fill(), t.restore();
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", C({}, this.rgb)));
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 = $(o, n, this.centerX, this.centerY, this.radius);
197
- this.rgb = i.dist < F ? { r: 255, g: 255, b: 255 } : D(i.h, i.s, 100), this.scheduleUpdate();
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.$emit("input", C({}, this.rgb));
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.$emit("input", C({}, this.rgb)));
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 H = function() {
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
- }, L = [], G = /* @__PURE__ */ Y(
249
+ }, H = [], L = /* @__PURE__ */ X(
215
250
  j,
216
- H,
217
- L,
218
- !1,
219
- null,
220
- "e9569c49"
251
+ B,
252
+ H
221
253
  );
222
- const y = G.exports;
223
- y.install = (t) => {
224
- t.component(y.name, y);
254
+ const M = L.exports;
255
+ M.install = (t) => {
256
+ t.component(M.name, M);
225
257
  };
226
258
  export {
227
- y as default
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 H=Object.defineProperty;var T=Object.getOwnPropertySymbols;var L=Object.prototype.hasOwnProperty,G=Object.prototype.propertyIsEnumerable;var O=(c,h,d)=>h in c?H(c,h,{enumerable:!0,configurable:!0,writable:!0,value:d}):c[h]=d,y=(c,h)=>{for(var d in h||(h={}))L.call(h,d)&&O(c,d,h[d]);if(T)for(var d of T(h))G.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),p=i*(1-n*(1-u));let v=0,f=0,g=0;switch(a){case 0:v=i,f=p,g=r;break;case 1:v=l,f=i,g=r;break;case 2:v=r,f=i,g=p;break;case 3:v=r,f=l,g=i;break;case 4:v=p,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 k(t,e,s,o,n){const i=t.canvas,a=i.width,u=i.height,r=e*n,l=s*n,p=o*n,v=p*p,f=new Array(360);for(let b=0;b<360;b++)f[b]=c(b,100,100);const g=t.createImageData(a,u),m=g.data;for(let b=0;b<u;b++){const I=b-l,B=I*I;for(let w=0;w<a;w++){const M=w-r,D=M*M+B;if(D>v)continue;const R=(b*a+w)*4,x=Math.sqrt(D)/p;let _=-Math.atan2(I,M)*180/Math.PI;_<0&&(_+=360);const P=f[Math.round(_)%360];m[R]=255+(P.r-255)*x,m[R+1]=255+(P.g-255)*x,m[R+2]=255+(P.b-255)*x,m[R+3]=255}}t.putImageData(g,0,0)}function z(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),r._scopeId="data-v-"+i,{exports:t,options:r}}const $=15,A=12,X=18,Y=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 q(t,e){return!!t&&!!e&&t.r===e.r&&t.g===e.g&&t.b===e.b}const E={name:"CanvasColorDisc",props:{value:{type:Object,default:()=>({r:255,g:255,b:255})},width:{type:Number,default:300},height:{type:Number,default:300}},data(){return{rgb:S(this.value),centerX:0,centerY:0,radius:0,dragging:!1}},watch:{value:{deep:!0,handler(t){q(t,this.rgb)||(this.rgb=S(t),this.drawSelector())}},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.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=0),this.canvasResizeObserver&&(this.canvasResizeObserver.disconnect(),this.canvasResizeObserver=null)},methods:{rebuildCanvas(){this.$nextTick(()=>{this.setupCanvas(),this.drawSelector()})},setupCanvas(){const t=window.devicePixelRatio||1,e=this.$refs.discCanvas,s=this.$refs.selectorCanvas;[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-X,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),k(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,$,0,Math.PI*2),t.fillStyle="#eee",t.fill(),t.restore(),t.save(),t.beginPath(),t.arc(s.x,s.y,A,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",y({},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<Y?{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.$emit("input",y({},this.rgb))}))},flushUpdate(){this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=0,this.drawSelector(),this.$emit("input",y({},this.rgb)))}}};var N=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}})])},F=[],j=z(E,N,F,!1,null,"e9569c49");const C=j.exports;return C.install=t=>{t.component(C.name,C)},C});
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[data-v-e9569c49]{position:relative;display:inline-block}.disc-canvas[data-v-e9569c49]{display:block}.disc-selector[data-v-e9569c49]{position:absolute;top:0;left:0;cursor:crosshair;touch-action:none;-webkit-user-select:none;user-select:none}
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.0",
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
- "./dist/style.css": "./dist/style.css"
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": "vite 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},支持 v-model
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.value),
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
- if (!isSameRgb(val, this.rgb)) {
84
- this.rgb = normalizeRgb(val)
85
- this.drawSelector()
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
- if (this.rafId) {
110
- cancelAnimationFrame(this.rafId)
111
- this.rafId = 0
112
- }
113
- if (this.canvasResizeObserver) {
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.$emit('input', { ...this.rgb })
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.$emit('input', { ...this.rgb })
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
+ }