diff --git a/package.json b/package.json
index 376eac9a7f..3d52c60abf 100644
--- a/package.json
+++ b/package.json
@@ -106,6 +106,7 @@
"unplugin-vue-markdown": "^0.24.3",
"vite": "^4.4.11",
"vitest": "^0.34.6",
+ "vitest-canvas-mock": "^0.3.3",
"vue": "^3.3.4",
"vue-tsc": "1.8.15"
},
diff --git a/packages/nutui-taro-demo/project.private.config.json b/packages/nutui-taro-demo/project.private.config.json
index 39f7acfbef..976aa25cb8 100644
--- a/packages/nutui-taro-demo/project.private.config.json
+++ b/packages/nutui-taro-demo/project.private.config.json
@@ -6,6 +6,13 @@
"condition": {
"miniprogram": {
"list": [
+ {
+ "name": "AvatarCropper",
+ "pathName": "business/pages/avatarcropper/index",
+ "query": "",
+ "launchMode": "default",
+ "scene": null
+ },
{
"name": "exhibition/pages/imagepreview/index",
"pathName": "exhibition/pages/imagepreview/index",
diff --git a/packages/nutui-taro-demo/src/business/pages/avatarcropper/index.config.ts b/packages/nutui-taro-demo/src/business/pages/avatarcropper/index.config.ts
new file mode 100644
index 0000000000..7c77e2da51
--- /dev/null
+++ b/packages/nutui-taro-demo/src/business/pages/avatarcropper/index.config.ts
@@ -0,0 +1,3 @@
+export default {
+ navigationBarTitleText: 'AvatarCropper'
+};
diff --git a/packages/nutui-taro-demo/src/business/pages/avatarcropper/index.vue b/packages/nutui-taro-demo/src/business/pages/avatarcropper/index.vue
new file mode 100644
index 0000000000..3e19c2ec0a
--- /dev/null
+++ b/packages/nutui-taro-demo/src/business/pages/avatarcropper/index.vue
@@ -0,0 +1,50 @@
+
+
+
+
基础用法
+
+
+
+
+
+
+
+
裁剪区域toolbar插槽
+
+
+
+
+
+
+
+ 取消
+ 重置
+ 旋转
+ 确认
+
+
+
+
+
+
+
+
+
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3bc53b3c74..afde6133b5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -108,6 +108,9 @@ importers:
vitest:
specifier: ^0.34.6
version: 0.34.6(@vitest/ui@0.34.6)(sass@1.69.3)
+ vitest-canvas-mock:
+ specifier: ^0.3.3
+ version: 0.3.3(vitest@0.34.6)
vue:
specifier: ^3.3.4
version: 3.3.4
@@ -6543,6 +6546,10 @@ packages:
engines: {node: '>=4'}
hasBin: true
+ /cssfontparser@1.2.1:
+ resolution: {integrity: sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==}
+ dev: true
+
/cssnano-preset-default@5.2.14(postcss@8.4.31):
resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==}
engines: {node: ^10 || ^12 || >=14.0}
@@ -9857,6 +9864,13 @@ packages:
resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==}
dev: true
+ /jest-canvas-mock@2.5.2:
+ resolution: {integrity: sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A==}
+ dependencies:
+ cssfontparser: 1.2.1
+ moo-color: 1.0.3
+ dev: true
+
/jest-worker@27.5.1:
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
engines: {node: '>= 10.13.0'}
@@ -11136,6 +11150,12 @@ packages:
yargs-unparser: 2.0.0
dev: true
+ /moo-color@1.0.3:
+ resolution: {integrity: sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==}
+ dependencies:
+ color-name: 1.1.4
+ dev: true
+
/move-concurrently@1.0.1:
resolution: {integrity: sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==}
dependencies:
@@ -15093,6 +15113,15 @@ packages:
fsevents: 2.3.3
dev: true
+ /vitest-canvas-mock@0.3.3(vitest@0.34.6):
+ resolution: {integrity: sha512-3P968tYBpqYyzzOaVtqnmYjqbe13576/fkjbDEJSfQAkHtC5/UjuRHOhFEN/ZV5HVZIkaROBUWgazDKJ+Ibw+Q==}
+ peerDependencies:
+ vitest: '*'
+ dependencies:
+ jest-canvas-mock: 2.5.2
+ vitest: 0.34.6(@vitest/ui@0.34.6)(sass@1.69.3)
+ dev: true
+
/vitest@0.34.6(@vitest/ui@0.34.6)(sass@1.69.3):
resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==}
engines: {node: '>=v14.18.0'}
diff --git a/src/config.json b/src/config.json
index 74e0d60a12..3bf7d2ffa3 100644
--- a/src/config.json
+++ b/src/config.json
@@ -1311,6 +1311,17 @@
"type": "component",
"author": "ailululu",
"taro": true
+ },
+ {
+ "version": "1.0.0",
+ "name": "AvatarCropper",
+ "type": "component",
+ "tarodoc": true,
+ "show": true,
+ "cName": "头像裁剪",
+ "desc": "仿微信头像裁剪功能",
+ "taro": true,
+ "author": "Marvin"
}
]
}
diff --git a/src/packages/__VUE/avatarcropper/__tests__/__snapshots__/avatarcropper.spec.ts.snap b/src/packages/__VUE/avatarcropper/__tests__/__snapshots__/avatarcropper.spec.ts.snap
new file mode 100644
index 0000000000..b62e9fc16d
--- /dev/null
+++ b/src/packages/__VUE/avatarcropper/__tests__/__snapshots__/avatarcropper.spec.ts.snap
@@ -0,0 +1,38 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`layout default slot 1`] = `
+"
+"
+`;
diff --git a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts
new file mode 100644
index 0000000000..e61bfdc078
--- /dev/null
+++ b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts
@@ -0,0 +1,83 @@
+import 'vitest-canvas-mock';
+import { mount } from '@vue/test-utils';
+import AvatarCropper from '../index.vue';
+import { sleep, trigger, triggerDrag } from '@/packages/utils/unit';
+import { h } from 'vue';
+
+const mockFile = new File([new ArrayBuffer(10000)], 'test.jpg', {
+ type: 'image/jpg'
+});
+
+test('layout default slot', () => {
+ const wrapper = mount(AvatarCropper, {
+ slots: {
+ default: h('img', {
+ src: 'https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png'
+ })
+ }
+ });
+
+ expect(wrapper.html()).toMatchSnapshot();
+ expect(wrapper.find('.nut-avatar-cropper').html()).toContain(
+ ' '
+ );
+});
+
+test('should render base cutAvatar and type', async () => {
+ const wrapper = mount(AvatarCropper);
+ const up_load = wrapper.find('.nut-avatar-cropper');
+ expect(up_load.exists()).toBe(true);
+ const up_load1 = wrapper.find('.nut-avatar-cropper__input');
+ expect(up_load1.attributes().type).toBe('file');
+});
+
+test('AvatarCropper: Select the image to open the crop window', async () => {
+ const wrapper = mount(AvatarCropper);
+ const input: any = wrapper.find('.nut-avatar-cropper__input');
+ expect(input.exists()).toBe(true);
+ const smallFile = new File([new ArrayBuffer(100)], 'small.jpg');
+ Object.defineProperty(input.element, 'files', {
+ get: vi.fn().mockReturnValue([mockFile, smallFile])
+ });
+ expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', 'display: none;');
+ await input.trigger('change');
+ await sleep();
+ expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', '');
+ const canvas = wrapper.find('.nut-cropper-popup__canvas');
+ expect(canvas.exists()).toBe(true);
+
+ const track = wrapper.find('.nut-cropper-popup__highlight');
+
+ trigger(track, 'touchstart', 0, 0, { x: 0, y: 0 });
+ trigger(track, 'touchmove', 20, 20, { x: 40, y: 60 });
+ trigger(track, 'touchend', 20, 100, { x: 40, y: 60 });
+
+ triggerDrag(track, 50, 60);
+ const toolbar = wrapper.findAll('.nut-cropper-popup__toolbar-item');
+ expect(toolbar.length).toBe(4);
+
+ const cancel = toolbar[0];
+ cancel.trigger('click');
+ expect(wrapper.emitted('cancel')).toBeTruthy();
+ expect(input.element.value).toBe('');
+ await sleep();
+ expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', 'display: none;');
+
+ const reset = toolbar[1];
+ reset.trigger('click');
+ expect(wrapper.vm.angle).toBe(0);
+
+ const rotate = toolbar[2];
+ rotate.trigger('click');
+ expect(wrapper.vm.angle).toBe(90);
+ triggerDrag(track, 1000, 2000);
+ rotate.trigger('click');
+ expect(wrapper.vm.angle).toBe(180);
+ rotate.trigger('click');
+ rotate.trigger('click');
+ expect(wrapper.vm.angle).toBe(0);
+
+ const confirm = toolbar[3];
+ confirm.trigger('click');
+ expect(wrapper.emitted('confirm')).toBeTruthy();
+});
diff --git a/src/packages/__VUE/avatarcropper/demo.vue b/src/packages/__VUE/avatarcropper/demo.vue
new file mode 100644
index 0000000000..d8638a9812
--- /dev/null
+++ b/src/packages/__VUE/avatarcropper/demo.vue
@@ -0,0 +1,46 @@
+
+
+
基础用法
+
+
+
+
+
+
+
+
裁剪区域toolbar插槽
+
+
+
+
+
+
+
+ 取消
+ 重置
+ 旋转
+ 确认
+
+
+
+
+
+
+
+
+
+
diff --git a/src/packages/__VUE/avatarcropper/doc.en-US.md b/src/packages/__VUE/avatarcropper/doc.en-US.md
new file mode 100644
index 0000000000..849493ced5
--- /dev/null
+++ b/src/packages/__VUE/avatarcropper/doc.en-US.md
@@ -0,0 +1,122 @@
+# AvatarCropper Head cropping
+
+### introduce
+
+Used to cut the profile picture to create a new image.
+
+### install
+
+```js
+import { createApp } from 'vue';
+import { AvatarCropper } from '@nutui/nutui';
+
+const app = createApp();
+app.use(AvatarCropper);
+```
+
+### 基础用法
+
+Use the avatar component directly in the middle, and the image content will be replaced with the new one after cropping.
+
+:::demo
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+:::
+
+### Clipping region toolbar slots
+
+Customize the clipping area toolbar, and toolbar-position controls the toolbar position
+
+:::demo
+
+```vue
+
+
+
+
+
+
+
+ cancel
+ reset
+ spin
+ confirm
+
+
+
+
+
+
+
+
+```
+
+:::
+
+## API
+
+### AvatarCropper Props
+
+| Attribute | Description | Type | Default |
+| ---------------- | ----------------------------------------------------------------------------------- | ------ | ------- |
+| max-zoom | Maximum zoom | number | 3 |
+| space | The gap reserved on both sides of the clipping area | number | 20 |
+| toolbar-position | Location of the toolbar in the clipping area. The optional value is:`top` `bottom` | string | bottom |
+| edit-text | The text content in the middle | string | 编辑 |
+| cancel-text | Cancel button text | string | 取消 |
+| cancel-confirm | Confirm button text | string | 确认 |
+
+### AvatarCropper Slots
+
+| Name | Description |
+| ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
+| default | The default slot for placing elements such as images, ICONS, and text |
+| toolbar | After selecting the file, crop the bottom element of the pop-up window can be customized, and invoke the method of the component through ref |
+
+### AvatarCropper Events
+
+| Name | Description | Callback Arguments |
+| ------- | --------------------------------------- | ---------------------- |
+| confirm | Click Confirm to trigger after cropping | url:The trimmed base64 |
+| cancel | Click cancel trigger | - |
+
+### AvatarCropper Ref
+
+| Event | Explain |
+| ------- | ------------------ |
+| cancel | uncrop |
+| reset | Reset to 0 degrees |
+| rotate | Rotate 90 degrees |
+| confirm | Definite cut |
diff --git a/src/packages/__VUE/avatarcropper/doc.md b/src/packages/__VUE/avatarcropper/doc.md
new file mode 100644
index 0000000000..72d11c587c
--- /dev/null
+++ b/src/packages/__VUE/avatarcropper/doc.md
@@ -0,0 +1,122 @@
+# AvatarCropper 头像剪切
+
+### 介绍
+
+用来对头像进行剪切生成一张新的图片。
+
+### 安装
+
+```js
+import { createApp } from 'vue';
+import { AvatarCropper } from '@nutui/nutui';
+
+const app = createApp();
+app.use(AvatarCropper);
+```
+
+### 基础用法
+
+中间直接使用avatar组件,裁剪后图片内容会被替换为新的。
+
+:::demo
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+:::
+
+### 裁剪区域toolbar插槽
+
+自定义裁剪区域工具栏,toolbar-position控制工具栏位置
+
+:::demo
+
+```vue
+
+
+
+
+
+
+
+ 取消
+ 重置
+ 旋转
+ 确认
+
+
+
+
+
+
+
+
+```
+
+:::
+
+## API
+
+### AvatarCropper Props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| ---------------- | ------------------------------------------- | ------ | ------ |
+| max-zoom | 最大缩放倍数 | number | 3 |
+| space | 裁剪区域两边预留的间隙 | number | 10 |
+| toolbar-position | 裁剪区域工具栏位置,可选值为:`top` `bottom` | string | bottom |
+| edit-text | 中间的文字内容 | string | 编辑 |
+| cancel-text | 取消按钮的文字 | string | 取消 |
+| cancel-confirm | 确认按钮的文字 | string | 确认 |
+
+### AvatarCropper Slots
+
+| 名称 | 描述 |
+| ------- | ----------------------------------------------------------- |
+| default | 默认插槽,可放置图片、图标、文本等元素 |
+| toolbar | 选择文件后裁剪弹窗底部元素可以自定义,通过ref调用组件的方法 |
+
+### AvatarCropper Events
+
+| 名称 | 描述 | 回调参数 |
+| ------- | ------------------ | ------------------ |
+| confirm | 裁剪后点击确认触发 | url:裁剪后的base64 |
+| cancel | 点击取消触发 | - |
+
+### AvatarCropper Ref
+
+| 事件名 | 说明 |
+| ------- | --------- |
+| cancel | 取消裁剪 |
+| reset | 重置为0度 |
+| rotate | 旋转90度 |
+| confirm | 确定裁剪 |
diff --git a/src/packages/__VUE/avatarcropper/doc.taro.md b/src/packages/__VUE/avatarcropper/doc.taro.md
new file mode 100644
index 0000000000..59611620e9
--- /dev/null
+++ b/src/packages/__VUE/avatarcropper/doc.taro.md
@@ -0,0 +1,124 @@
+# AvatarCropper 头像剪切
+
+### 介绍
+
+用来对头像进行剪切生成一张新的图片。
+
+### 安装
+
+```js
+import { createApp } from 'vue';
+import { AvatarCropper } from '@nutui/nutui-taro';
+
+const app = createApp();
+app.use(AvatarCropper);
+```
+
+### 基础用法
+
+中间直接使用avatar组件,裁剪后图片内容会被替换为新的。
+
+:::demo
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+:::
+
+### 裁剪区域toolbar插槽
+
+自定义裁剪区域工具栏,toolbar-position控制工具栏位置
+
+:::demo
+
+```vue
+
+
+
+
+
+
+
+ 取消
+ 重置
+ 旋转
+ 确认
+
+
+
+
+
+
+
+
+```
+
+:::
+
+## API
+
+### AvatarCropper Props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| ---------------- | -------------------------------------------------- | ------ | -------------------------- |
+| max-zoom | 最大缩放倍数 | number | 3 |
+| space | 裁剪区域两边预留的间隙 | number | 10 |
+| toolbar-position | 裁剪区域工具栏位置,可选值为:`top` `bottom` | string | bottom |
+| edit-text | 中间的文字内容 | string | 编辑 |
+| cancel-text | 取消按钮的文字 | string | 取消 |
+| cancel-confirm | 确认按钮的文字 | string | 确认 |
+| size-type | 所选的图片的尺寸: 可选值:`original` `compressed` | Array | ['original', 'compressed'] |
+| source-type | 选择图片的来源: 可选值:`album` `camera` | Array | ['album', 'camera'] |
+
+### AvatarCropper Slots
+
+| 名称 | 描述 |
+| ------- | ----------------------------------------------------------- |
+| default | 默认插槽,可放置图片、图标、文本等元素 |
+| toolbar | 选择文件后裁剪弹窗底部元素可以自定义,通过ref调用组件的方法 |
+
+### AvatarCropper Events
+
+| 名称 | 描述 | 回调参数 |
+| ------- | ------------------ | ------------------ |
+| confirm | 裁剪后点击确认触发 | url:裁剪后的base64 |
+| cancel | 点击取消触发 | - |
+
+### AvatarCropper Ref
+
+| 事件名 | 说明 |
+| ------- | --------- |
+| cancel | 取消裁剪 |
+| reset | 重置为0度 |
+| rotate | 旋转90度 |
+| confirm | 确定裁剪 |
diff --git a/src/packages/__VUE/avatarcropper/index.scss b/src/packages/__VUE/avatarcropper/index.scss
new file mode 100644
index 0000000000..016984a48e
--- /dev/null
+++ b/src/packages/__VUE/avatarcropper/index.scss
@@ -0,0 +1,98 @@
+.nut-avatar-cropper {
+ position: relative;
+ &::after,
+ &__edit-text {
+ content: attr(data-edit-text);
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba($color: #000000, $alpha: 0.3);
+ z-index: 1;
+ color: #fff;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ &.taro {
+ &::after {
+ content: none;
+ }
+ }
+ &__input {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ opacity: 0; // 隐藏原生上传按钮
+ cursor: pointer;
+ z-index: 2;
+ }
+}
+
+.nut-cropper-popup {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: var(--nut-overlay-bg-color, rgba(0, 0, 0, 0.7));
+ z-index: 1000;
+ &__canvas,
+ &__cut-canvas {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ }
+ &__cut-canvas {
+ z-index: 0;
+ }
+ &__toolbar {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ z-index: 2;
+ &.top {
+ top: 0;
+ bottom: inherit;
+ }
+ .flex-sb {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ }
+ &-item {
+ color: #fff;
+ padding: 15px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ }
+ }
+ &__highlight {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ background-color: transparent;
+ .highlight {
+ position: absolute;
+ width: 365px;
+ height: 365px;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 1;
+ background-color: transparent;
+ box-shadow: 0 0 1000px 1000px rgba(0, 0, 0, 0.6);
+ }
+ }
+}
diff --git a/src/packages/__VUE/avatarcropper/index.taro.vue b/src/packages/__VUE/avatarcropper/index.taro.vue
new file mode 100644
index 0000000000..5eee51091a
--- /dev/null
+++ b/src/packages/__VUE/avatarcropper/index.taro.vue
@@ -0,0 +1,657 @@
+
+
+
+ {{ editText }}
+
+
+
+
+
diff --git a/src/packages/__VUE/avatarcropper/index.vue b/src/packages/__VUE/avatarcropper/index.vue
new file mode 100644
index 0000000000..bd2031f181
--- /dev/null
+++ b/src/packages/__VUE/avatarcropper/index.vue
@@ -0,0 +1,452 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/packages/__VUE/avatarcropper/types.ts b/src/packages/__VUE/avatarcropper/types.ts
new file mode 100644
index 0000000000..155fcd07e8
--- /dev/null
+++ b/src/packages/__VUE/avatarcropper/types.ts
@@ -0,0 +1,3 @@
+export type AvatarCropperToolbarPosition = 'top' | 'bottom';
+export type AvatarCropperSizeType = 'original' | 'compressed';
+export type AvatarCropperSourceType = 'album' | 'camera';
diff --git a/src/packages/utils/canvas.ts b/src/packages/utils/canvas.ts
new file mode 100644
index 0000000000..fdaf27c918
--- /dev/null
+++ b/src/packages/utils/canvas.ts
@@ -0,0 +1,107 @@
+import Taro from '@tarojs/taro';
+import CanvasContext = Taro.CanvasContext;
+
+const compareVersion = (v1Old: string, v2Old: string) => {
+ let v1 = v1Old.split('.');
+ let v2 = v2Old.split('.');
+ const len = Math.max(v1.length, v2.length);
+
+ while (v1.length < len) {
+ v1.push('0');
+ }
+ while (v2.length < len) {
+ v2.push('0');
+ }
+
+ for (let i = 0; i < len; i++) {
+ const num1 = parseInt(v1[i]);
+ const num2 = parseInt(v2[i]);
+
+ if (num1 > num2) {
+ return 1;
+ } else if (num1 < num2) {
+ return -1;
+ }
+ }
+
+ return 0;
+};
+
+const isWeapp = () => {
+ return process.env.TARO_ENV === 'weapp';
+};
+
+//////////////////////////////////////////////////////////////////////////////////
+//////// 微信小程序自1.9.90起废除若干个CanvasContext的函数,改为属性,以下为兼容代码
+//////////////////////////////////////////////////////////////////////////////////
+
+function _easyCanvasContextBase(
+ systemInfo: any,
+ lowCallback: () => void,
+ highCallback: () => void,
+ targetVersion: string = '1.9.90'
+) {
+ if (isWeapp() && compareVersion(systemInfo.SDKVersion, targetVersion) >= 0) {
+ highCallback();
+ } else {
+ lowCallback();
+ }
+}
+/**
+ *
+ * 基础库 1.9.90 开始支持,低版本需做兼容处理。填充颜色。用法同 CanvasContext.setFillStyle()。
+ * @param systemInfo
+ * @param canvasContext
+ * @param color
+ */
+function easySetStrokeStyle(
+ systemInfo: Taro.getSystemInfoSync.Result,
+ canvasContext: CanvasContext,
+ color: string | CanvasGradient
+) {
+ _easyCanvasContextBase(
+ systemInfo,
+ () => {
+ canvasContext.setStrokeStyle(color);
+ console.log('???');
+ },
+ () => {
+ if (typeof color === 'string') {
+ canvasContext.strokeStyle = color;
+ }
+ console.log('2333');
+ }
+ );
+}
+
+function easySetLineWidth(systemInfo: Taro.getSystemInfoSync.Result, canvasContext: CanvasContext, lineWidth: number) {
+ _easyCanvasContextBase(
+ systemInfo,
+ () => {
+ canvasContext.setLineWidth(lineWidth);
+ },
+ () => {
+ canvasContext.lineWidth = lineWidth;
+ }
+ );
+}
+
+function easySetFillStyle(
+ systemInfo: Taro.getSystemInfoSync.Result,
+ canvasContext: CanvasContext,
+ color: string | CanvasGradient
+) {
+ _easyCanvasContextBase(
+ systemInfo,
+ () => {
+ canvasContext.setFillStyle(color);
+ },
+ () => {
+ if (typeof color === 'string') {
+ canvasContext.fillStyle = color;
+ }
+ }
+ );
+}
+
+export { compareVersion, easySetStrokeStyle, easySetLineWidth, easySetFillStyle };