uview-plus 3.8.130 → 3.8.135

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/changelog.md CHANGED
@@ -1,3 +1,58 @@
1
+ ## 3.8.135
2
+ feat: 新增 u-lottie 动画组件,u-upload 新增 mediaType 并修复 chooseMedia 平台判断
3
+
4
+ 本次更新新增 Lottie 动画组件 u-lottie,覆盖 H5/App-Vue、微信小程序与鸿蒙多端渲染;同时为 u-upload 补上 mediaType 属性,并修复 chooseMedia 按平台硬编码导致的兼容性问题。
5
+
6
+ - 新增 u-lottie 组件:支持 filePath 传入动画 JSON 地址(http(s),H5 / App-Vue 额外支持本地静态路径),或 animationData 直接传入 Lottie JSON 对象(优先级高于 filePath);可配置 contentModel 内容填充模式(ScaleToFill / AspectFill / Center)、loop 循环播放、autoplay 数据与引擎就绪后自动播放
7
+ - u-lottie 跨端渲染:H5 / App-Vue 基于 lottie-web SVG 渲染器,H5 端引擎按需加载、加载失败自动清理;微信小程序基于 lottie-miniprogram 画布渲染;鸿蒙通过 harmony-canvas-adapter 画布适配,并修复同步渲染未提交绘制的问题
8
+ - u-lottie 实例方法:reload / playAnimation / pauseAnimation / resumeAnimation / cancelAnimation;statechange 事件统一回报 ready / play / pause / resume / stop / complete / error 状态(微信小程序从 event.detail 读取)
9
+ - u-upload:新增 mediaType 属性(默认 ['image', 'video']),accept 为 media 时透传给 chooseMedia 控制可选文件类型(image / video / mix)
10
+ - u-upload:chooseMedia 不再做平台编译判断,改为运行时检测接口是否存在,不存在时自动回退到 chooseImage,修复部分平台 accept="media" 直接报错的问题
11
+ - 新增 componentsD/lottie 示例页
12
+
13
+ ## 3.8.134
14
+ fix: 修复 up-cropper 小程序端底部操作栏透出页面与导出参数失效
15
+
16
+ 本次更新修复 u-cropper 的三个问题:iOS 上底部操作栏与页面浮动元素重叠、quality 参数完全失效,以及小数质量被悄悄改回 0.9。
17
+
18
+ - u-cropper:修复 iOS 上底部操作栏与页面浮动按钮/标签重叠的问题。画布高度减掉了底部安全区,而操作栏仍是 50px 贴在屏幕最底,两者之间露出一条约安全区高度的页面背景,页面上浮动的元素会从这条缝里透出来;安卓 safeAreaInsets.bottom 为 0,所以只有苹果机能看到。现在操作栏高度改为 50px + 安全区并用 padding-bottom 让出,与画布严丝合缝,按钮也完整位于 home 指示条上方
19
+ - u-cropper:新增 fileType 属性(默认 png,可选 png/jpg)。原先 5 处导出都把 fileType 硬编码为 png,PNG 是无损格式、没有质量档位,quality 形同虚设;只有 jpg 才会按 quality 压缩,chooseImage 的 params 也可覆盖
20
+ - u-cropper:修复 quality 归一化使用 parseInt 导致 parseInt(0.5) 为 0、被 `|| 0.9` 改回 0.9 的问题,改用 parseFloat 并夹取到 0-1,number 与数字字符串都认
21
+ - u-cropper:safeAreaInsets 取值增加兜底,个别端取不到时不再算出 NaN 把布局顶飞
22
+ - 升级说明:fileType 默认仍为 png,输出格式与升级前一致,不需要改代码;需要压缩体积时显式传 fileType="jpg" 并配合 quality
23
+ - 回归验证:新增 scripts/verify-cropper-export-and-safe-area.mjs,直接加载组件脚本真实调用 computed 与方法,锁住操作栏安全区样式、fileType 透传与 quality 归一化
24
+
25
+ ## 3.8.133
26
+ fix: 修复微信小程序数字键盘退格键短按无效,toast 新增 overlay 属性支持提示期间穿透页面
27
+
28
+ 本次更新修复 3.8.x 近期引入的两个小程序体验问题,并补上 toast 提示期间页面可操作的能力。
29
+
30
+ - u-number-keyboard:去掉退格键 @touchstart 上残留的 .stop,修复微信小程序 catchtouchstart 吞掉同一节点 tap,导致短按退格不删除内容、只剩长按连删有效的问题
31
+ - u-number-keyboard:补充 @touchcancel 清理定时器,避免触摸被系统手势或滚动打断后长按连删定时器空转
32
+ - u-toast:新增 overlay 属性(默认 true),设置 :overlay="false" 后遮罩由铺满全屏收缩为跟随内容大小并居中的浮动层,提示期间点击与滚动可穿透页面;show() 未显式传值时回落到该 prop,show({ overlay }) 仍然优先
33
+ - types/comps/toast.d.ts 的 ToastProps 补上 overlay,IDE 可提示
34
+ - 回归验证:新增键盘退格点击行为与 toast overlay 穿透两个验证脚本,改动前均失败
35
+
36
+ ## 3.8.132
37
+ feat: 新增 u-goods-sku 默认选中与安全区配置,并修复 calc.div 精度崩溃
38
+
39
+ 本次更新包含商品 SKU 组件的默认选中能力、底部安全区开关,以及修复浮点除法在无 xyutil 环境下崩溃的问题。
40
+
41
+ - u-goods-sku:新增 defaultSku 属性,支持设置默认选中的 SKU,并支持响应式更新与重置恢复(#1070)
42
+ - u-goods-sku:新增 safeAreaInsetBottom 属性(默认 true),可关闭底部安全距离,并透传给 u-popup
43
+ - calc.div:改用 digit.divide 实现,避免依赖未定义的 xyutil,修复运行环境为空时的崩溃
44
+ - 回归验证:新增 u-goods-sku 默认选中与 calc.div 精度验证脚本
45
+
46
+ ## 3.8.131
47
+ fix: 修复微信小程序 u-text 的 v-bind 编译报错
48
+
49
+ 修复 3.8.128 引入的 u-text 小程序编译兼容性回归,恢复组件原有的属性继承行为。
50
+
51
+ - u-text:移除原生 button 上不受小程序编译器支持的 v-bind="$attrs",修复 v-bind="" is not supported. 报错。
52
+ - u-text:恢复根节点属性继承,避免普通文本的 id、data-* 等属性丢失;保留原有 data-index、open-type 属性与事件绑定。
53
+ - 回归验证:新增真实微信小程序模板编译和 H5 属性继承测试,并通过微信小程序全量构建。
54
+ - 升级说明:撤回 3.8.128 中任意 data-* 属性向内部分享按钮的自动透传尝试;需要自定义分享数据时,请使用原生 button 显式绑定 data-* 并设置 open-type="share"。
55
+
1
56
  ## 3.8.130
2
57
  fix: 修复微信小程序 u-text 的 v-bind 编译报错
3
58
 
@@ -4,7 +4,7 @@
4
4
  <up-canvas ref="avatarCanvas" :canvas-id="'avatar-canvas-' + instanceId" class="my-canvas" :width="windowWidth || 0" :height="windowHeight || 0" :style="{ top: styleTop, height: cvsStyleHeight }" bg-color="transparent"></up-canvas>
5
5
  <up-canvas ref="operCanvas" :canvas-id="'oper-canvas-' + instanceId" class="oper-canvas" :width="windowWidth || 0" :height="windowHeight || 0" :style="{ top: styleTop, height: cvsStyleHeight }" bg-color="transparent" @touchstart="start" @touchmove="move" @touchend="end"></up-canvas>
6
6
  <up-canvas ref="prvCanvas" :canvas-id="'prv-canvas-' + instanceId" class="prv-canvas" :width="windowWidth || 0" :height="windowHeight || 0" bg-color="transparent" @touchstart="hideImg" :style="{ height: cvsStyleHeight, top: prvTop }"></up-canvas>
7
- <view class="oper-wrapper" :style="{ display: styleDisplay }">
7
+ <view class="oper-wrapper" :style="operStyle">
8
8
  <view class="oper">
9
9
  <view class="btn-wrapper" v-if="showOper">
10
10
  <view class="u-cropper__action" @click="select" hover-class="hover" :style="{ width: btnWidth }">
@@ -91,7 +91,9 @@
91
91
  lock: '',
92
92
  noTab: true,
93
93
  inner: false,
94
- quality: '',
94
+ quality: '',
95
+ // 导出图片格式:png 为无损压缩(quality 不生效),jpg 才会按 quality 压缩
96
+ fileType: 'png',
95
97
  index: '',
96
98
  canChangeSize: false,
97
99
  areaWidth: '300rpx',
@@ -107,11 +109,29 @@
107
109
  default: 'transparent'
108
110
  },
109
111
  },
112
+ computed: {
113
+ // 导出图片格式:png 无损,quality 不生效;只有 jpg 才会按 quality 压缩
114
+ expFileType() {
115
+ const type = String(this.fileType || '').toLowerCase();
116
+ return type === 'jpg' || type === 'jpeg' ? 'jpg' : 'png';
117
+ },
118
+ // 底部操作栏样式:把底部安全区算进高度并用 padding-bottom 留出。
119
+ // 画布高度减掉了安全区,而操作栏仍是 50px 贴屏幕最底,两者之间会露出一
120
+ // 条页面背景,页面上浮动的按钮/标签会从这条缝里透出来,和裁剪底部按钮重叠。
121
+ operStyle() {
122
+ const safe = Math.max(0, parseInt(this.safeAreaInsetsBottom) || 0);
123
+ return {
124
+ display: this.styleDisplay,
125
+ height: (tabHeight + safe) + 'px',
126
+ paddingBottom: safe + 'px'
127
+ };
128
+ }
129
+ },
110
130
  created() {
111
131
  this.ctxCanvas = null;
112
132
  this.ctxCanvasOper = null;
113
133
  this.ctxCanvasPrv = null;
114
- this.qlty = parseInt(this.quality) || 0.9;
134
+ this.qlty = this.parseQuality(this.quality);
115
135
  this.imgSrc.imgSrc = this.imageSrc;
116
136
  this.letRotate = (this.canRotate === false || this.inner === true) ? 0 : 1;
117
137
  this.letScale = this.canScale === false ? 0 : 1;
@@ -147,7 +167,14 @@
147
167
  mounted() {
148
168
  this.initCanvasRefs(true);
149
169
  },
150
- methods: {
170
+ methods: {
171
+ // 图片质量归一化:接受 number 与数字字符串,越界夹到 0-1,无效值回落 0.9
172
+ // 原来用 parseInt,parseInt(0.5) 是 0,`|| 0.9` 会把所有小数质量悄悄改回 0.9
173
+ parseQuality(value) {
174
+ const num = parseFloat(value);
175
+ if (isNaN(num)) return 0.9;
176
+ return Math.min(1, Math.max(0, num));
177
+ },
151
178
  t,
152
179
  async initCanvasRefs(force = false) {
153
180
  await this.$nextTick();
@@ -170,7 +197,7 @@
170
197
  this.platform = sysInfo.platform;
171
198
  this.pixelRatio = sysInfo.pixelRatio;
172
199
  this.windowWidth = sysInfo.windowWidth;
173
- this.safeAreaInsetsBottom = sysInfo.safeAreaInsets.bottom;
200
+ this.safeAreaInsetsBottom = (sysInfo.safeAreaInsets && sysInfo.safeAreaInsets.bottom) || 0;
174
201
  // #ifdef H5
175
202
  this.drawTop = sysInfo.windowTop;
176
203
  this.windowHeight = sysInfo.windowHeight + sysInfo.windowBottom;
@@ -319,7 +346,7 @@
319
346
  height: height,
320
347
  destWidth: expWidth,
321
348
  destHeight: expHeight,
322
- fileType: 'png',
349
+ fileType: this.expFileType,
323
350
  quality: this.qlty,
324
351
  success: (r) => {
325
352
  r = r.tempFilePath;
@@ -339,7 +366,7 @@
339
366
  height: expHeight,
340
367
  destWidth: expWidth,
341
368
  destHeight: expHeight,
342
- fileType: 'png',
369
+ fileType: this.expFileType,
343
370
  quality: this.qlty,
344
371
  success: (r) => {
345
372
  r = r.tempFilePath;
@@ -413,7 +440,7 @@
413
440
  height: prvHeight,
414
441
  destWidth: expWidth,
415
442
  destHeight: expHeight,
416
- fileType: 'png',
443
+ fileType: this.expFileType,
417
444
  quality: this.qlty,
418
445
  success: (r) => {
419
446
  r = r.tempFilePath;
@@ -433,7 +460,7 @@
433
460
  height: expHeight,
434
461
  destWidth: expWidth,
435
462
  destHeight: expHeight,
436
- fileType: 'png',
463
+ fileType: this.expFileType,
437
464
  quality: this.qlty,
438
465
  success: (r) => {
439
466
  r = r.tempFilePath;
@@ -670,7 +697,7 @@
670
697
  y: y,
671
698
  width: width,
672
699
  height: height,
673
- fileType: 'png',
700
+ fileType: this.expFileType,
674
701
  quality: this.qlty,
675
702
  success: async (r) => {
676
703
  // console.log(r)
@@ -737,7 +764,8 @@
737
764
  areaHeight = params.areaHeight || this.areaHeight,
738
765
  expWidth = params.exportWidth || this.exportWidth,
739
766
  expHeight = params.exportHeight || this.exportHeight,
740
- quality = params.quality,
767
+ quality = params.quality,
768
+ fileType = params.fileType,
741
769
  canRotate = params.canRotate,
742
770
  canScale = params.canScale,
743
771
  canChangeSize = params.canChangeSize,
@@ -755,7 +783,8 @@
755
783
  this.letScale = canScale === false ? 0 : 1;
756
784
  // 设置是否允许调整裁剪框大小
757
785
  this.letChangeSize = canChangeSize || false;
758
- this.qlty = parseInt(quality) || 0.9;
786
+ this.qlty = this.parseQuality(quality);
787
+ if (fileType) this.fileType = fileType;
759
788
  this.mnScale = minScale || 0.3;
760
789
  this.mxScale = maxScale || 4;
761
790
  this.stc = stretch;
@@ -8,6 +8,7 @@
8
8
  mode="bottom"
9
9
  :closeable="pageInline ? false : closeable"
10
10
  :pageInline="pageInline"
11
+ :safeAreaInsetBottom="safeAreaInsetBottom"
11
12
  :border-radius="20"
12
13
  @close="close"
13
14
  @closed="$emit('closed')"
@@ -96,6 +97,11 @@
96
97
  type: Array,
97
98
  default: () => []
98
99
  },
100
+ // 默认选中的SKU
101
+ defaultSku: {
102
+ type: Object,
103
+ default: () => ({})
104
+ },
99
105
  // 最大购买数量
100
106
  maxBuy: {
101
107
  type: Number,
@@ -115,13 +121,18 @@
115
121
  pageInline: {
116
122
  type: Boolean,
117
123
  default: false
124
+ },
125
+ // 是否留出底部安全距离
126
+ safeAreaInsetBottom: {
127
+ type: Boolean,
128
+ default: true
118
129
  }
119
130
  },
120
131
  data() {
121
132
  return {
122
133
  show: false,
123
134
  // 已选择的SKU
124
- selectedSku: {},
135
+ selectedSku: { ...this.defaultSku },
125
136
  // 购买数量
126
137
  buyNum: 1
127
138
  }
@@ -175,6 +186,11 @@
175
186
  }
176
187
  },
177
188
  watch: {
189
+ defaultSku: {
190
+ handler(sku) {
191
+ this.selectedSku = { ...(sku || {}) }
192
+ }
193
+ }
178
194
  },
179
195
  emits: ['open', 'confirm', 'close', 'closed'],
180
196
  created() {
@@ -281,7 +297,7 @@
281
297
  },
282
298
  // 重置选择
283
299
  reset() {
284
- this.selectedSku = {}
300
+ this.selectedSku = { ...this.defaultSku }
285
301
  this.buyNum = 1
286
302
  },
287
303
  open() {
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 wechat-miniprogram
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Bodymovin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,394 @@
1
+ function parseFontSize(font) {
2
+ const matched = String(font || '').match(/(\d+(?:\.\d+)?)px/)
3
+ return matched ? Number(matched[1]) : 10
4
+ }
5
+
6
+ export function createHarmonyCanvasAdapter(options = {}) {
7
+ const legacy = options.context
8
+ if (!legacy) {
9
+ throw new Error('harmony canvas context is required')
10
+ }
11
+
12
+ const width = Number(options.width) > 0 ? Number(options.width) : 1
13
+ const height = Number(options.height) > 0 ? Number(options.height) : 1
14
+
15
+ const state = {
16
+ fillStyle: '#000000',
17
+ strokeStyle: '#000000',
18
+ lineWidth: 1,
19
+ lineCap: 'butt',
20
+ lineJoin: 'miter',
21
+ miterLimit: 10,
22
+ lineDashOffset: 0,
23
+ lineDash: [],
24
+ globalAlpha: 1,
25
+ globalCompositeOperation: 'source-over',
26
+ font: '10px sans-serif'
27
+ }
28
+ const stack = []
29
+ const rafIds = new Set()
30
+ let dirty = false
31
+ let flushing = false
32
+ let destroyed = false
33
+ let flushTimer = null
34
+
35
+ const flush = () => {
36
+ if (destroyed || !dirty || flushing) return
37
+ dirty = false
38
+ flushing = true
39
+ let released = false
40
+ const release = () => {
41
+ if (released) return
42
+ released = true
43
+ flushing = false
44
+ if (dirty) scheduleFlush()
45
+ }
46
+ try {
47
+ if (typeof legacy.draw === 'function') {
48
+ legacy.draw(false, release)
49
+ setTimeout(release, 80)
50
+ } else {
51
+ release()
52
+ }
53
+ } catch (error) {
54
+ release()
55
+ }
56
+ }
57
+
58
+ const scheduleFlush = () => {
59
+ if (destroyed || flushTimer !== null) return
60
+ const timer = setTimeout(() => {
61
+ rafIds.delete(timer)
62
+ if (flushTimer === timer) flushTimer = null
63
+ flush()
64
+ }, 0)
65
+ flushTimer = timer
66
+ rafIds.add(timer)
67
+ }
68
+
69
+ const markDirty = () => {
70
+ dirty = true
71
+ scheduleFlush()
72
+ }
73
+
74
+ const sync = () => {
75
+ if (typeof legacy.setFillStyle === 'function') {
76
+ legacy.setFillStyle(state.fillStyle)
77
+ }
78
+ if (typeof legacy.setStrokeStyle === 'function') {
79
+ legacy.setStrokeStyle(state.strokeStyle)
80
+ }
81
+ if (typeof legacy.setLineWidth === 'function') {
82
+ legacy.setLineWidth(state.lineWidth)
83
+ }
84
+ if (typeof legacy.setLineCap === 'function') {
85
+ legacy.setLineCap(state.lineCap)
86
+ }
87
+ if (typeof legacy.setLineJoin === 'function') {
88
+ legacy.setLineJoin(state.lineJoin)
89
+ }
90
+ if (typeof legacy.setMiterLimit === 'function') {
91
+ legacy.setMiterLimit(state.miterLimit)
92
+ }
93
+ if (typeof legacy.setGlobalAlpha === 'function') {
94
+ legacy.setGlobalAlpha(state.globalAlpha)
95
+ }
96
+ if (typeof legacy.setGlobalCompositeOperation === 'function') {
97
+ legacy.setGlobalCompositeOperation(state.globalCompositeOperation)
98
+ }
99
+ if (typeof legacy.setLineDash === 'function') {
100
+ legacy.setLineDash(state.lineDash, state.lineDashOffset)
101
+ }
102
+ if (typeof legacy.setFontSize === 'function') {
103
+ legacy.setFontSize(parseFontSize(state.font))
104
+ }
105
+ }
106
+
107
+ const canvas = {
108
+ width,
109
+ height,
110
+ getContext() {
111
+ return context
112
+ },
113
+ requestAnimationFrame(callback) {
114
+ const timer = setTimeout(() => {
115
+ rafIds.delete(timer)
116
+ if (destroyed) return
117
+ try {
118
+ callback(Date.now())
119
+ } finally {
120
+ flush()
121
+ }
122
+ }, 33)
123
+ rafIds.add(timer)
124
+ return timer
125
+ },
126
+ cancelAnimationFrame(timer) {
127
+ if (rafIds.has(timer)) {
128
+ clearTimeout(timer)
129
+ rafIds.delete(timer)
130
+ }
131
+ },
132
+ createImage() {
133
+ const image = {
134
+ width: 0,
135
+ height: 0,
136
+ _src: '',
137
+ _localPath: '',
138
+ onload: null,
139
+ onerror: null,
140
+ addEventListener(type, callback) {
141
+ if (type === 'load') image.onload = callback
142
+ if (type === 'error') image.onerror = callback
143
+ }
144
+ }
145
+ Object.defineProperty(image, 'src', {
146
+ get() {
147
+ return image._src
148
+ },
149
+ set(value) {
150
+ image._src = value
151
+ uni.getImageInfo({
152
+ src: value,
153
+ success(result) {
154
+ image.width = result.width || 0
155
+ image.height = result.height || 0
156
+ image._localPath = result.path || value
157
+ if (typeof image.onload === 'function') image.onload()
158
+ },
159
+ fail() {
160
+ if (typeof image.onerror === 'function') image.onerror()
161
+ }
162
+ })
163
+ }
164
+ })
165
+ return image
166
+ }
167
+ }
168
+
169
+ const context = {
170
+ canvas,
171
+ get fillStyle() {
172
+ return state.fillStyle
173
+ },
174
+ set fillStyle(value) {
175
+ state.fillStyle = value
176
+ },
177
+ get strokeStyle() {
178
+ return state.strokeStyle
179
+ },
180
+ set strokeStyle(value) {
181
+ state.strokeStyle = value
182
+ },
183
+ get lineWidth() {
184
+ return state.lineWidth
185
+ },
186
+ set lineWidth(value) {
187
+ state.lineWidth = value
188
+ },
189
+ get lineCap() {
190
+ return state.lineCap
191
+ },
192
+ set lineCap(value) {
193
+ state.lineCap = value
194
+ },
195
+ get lineJoin() {
196
+ return state.lineJoin
197
+ },
198
+ set lineJoin(value) {
199
+ state.lineJoin = value
200
+ },
201
+ get miterLimit() {
202
+ return state.miterLimit
203
+ },
204
+ set miterLimit(value) {
205
+ state.miterLimit = value
206
+ },
207
+ get lineDashOffset() {
208
+ return state.lineDashOffset
209
+ },
210
+ set lineDashOffset(value) {
211
+ state.lineDashOffset = value
212
+ },
213
+ get globalAlpha() {
214
+ return state.globalAlpha
215
+ },
216
+ set globalAlpha(value) {
217
+ state.globalAlpha = value
218
+ },
219
+ get globalCompositeOperation() {
220
+ return state.globalCompositeOperation
221
+ },
222
+ set globalCompositeOperation(value) {
223
+ state.globalCompositeOperation = value
224
+ },
225
+ get font() {
226
+ return state.font
227
+ },
228
+ set font(value) {
229
+ state.font = value
230
+ },
231
+ save() {
232
+ stack.push(Object.assign({}, state))
233
+ if (typeof legacy.save === 'function') legacy.save()
234
+ },
235
+ restore() {
236
+ if (stack.length) Object.assign(state, stack.pop())
237
+ if (typeof legacy.restore === 'function') legacy.restore()
238
+ sync()
239
+ },
240
+ translate(x, y) {
241
+ legacy.translate(x, y)
242
+ },
243
+ scale(x, y) {
244
+ legacy.scale(x, y)
245
+ },
246
+ rotate(angle) {
247
+ legacy.rotate(angle)
248
+ },
249
+ transform(a, b, c, d, e, f) {
250
+ if (typeof legacy.transform === 'function') {
251
+ legacy.transform(a, b, c, d, e, f)
252
+ }
253
+ },
254
+ setTransform(a, b, c, d, e, f) {
255
+ if (typeof legacy.setTransform === 'function') {
256
+ legacy.setTransform(a, b, c, d, e, f)
257
+ } else if (typeof legacy.transform === 'function') {
258
+ legacy.transform(a, b, c, d, e, f)
259
+ }
260
+ },
261
+ beginPath() {
262
+ legacy.beginPath()
263
+ },
264
+ closePath() {
265
+ legacy.closePath()
266
+ },
267
+ moveTo(x, y) {
268
+ legacy.moveTo(x, y)
269
+ },
270
+ lineTo(x, y) {
271
+ legacy.lineTo(x, y)
272
+ },
273
+ bezierCurveTo(a, b, c, d, e, f) {
274
+ legacy.bezierCurveTo(a, b, c, d, e, f)
275
+ },
276
+ quadraticCurveTo(a, b, c, d) {
277
+ if (typeof legacy.quadraticCurveTo === 'function') {
278
+ legacy.quadraticCurveTo(a, b, c, d)
279
+ }
280
+ },
281
+ rect(x, y, w, h) {
282
+ legacy.rect(x, y, w, h)
283
+ },
284
+ arc(x, y, radius, start, end, anticlockwise) {
285
+ if (typeof legacy.arc === 'function') {
286
+ legacy.arc(x, y, radius, start, end, anticlockwise)
287
+ }
288
+ },
289
+ fill() {
290
+ sync()
291
+ legacy.fill()
292
+ markDirty()
293
+ },
294
+ stroke() {
295
+ sync()
296
+ legacy.stroke()
297
+ markDirty()
298
+ },
299
+ clip() {
300
+ if (typeof legacy.clip === 'function') {
301
+ legacy.clip()
302
+ }
303
+ },
304
+ clearRect(x, y, w, h) {
305
+ if (typeof legacy.clearRect === 'function') {
306
+ legacy.clearRect(x, y, w, h)
307
+ }
308
+ markDirty()
309
+ },
310
+ fillRect(x, y, w, h) {
311
+ sync()
312
+ if (typeof legacy.fillRect === 'function') {
313
+ legacy.fillRect(x, y, w, h)
314
+ }
315
+ markDirty()
316
+ },
317
+ strokeRect(x, y, w, h) {
318
+ sync()
319
+ if (typeof legacy.strokeRect === 'function') {
320
+ legacy.strokeRect(x, y, w, h)
321
+ }
322
+ markDirty()
323
+ },
324
+ drawImage() {
325
+ const image = arguments[0]
326
+ const source = image && (image._localPath || image._src || image.src)
327
+ if (!source) return
328
+ sync()
329
+ const args = Array.prototype.slice.call(arguments, 1)
330
+ try {
331
+ if (args.length >= 8) {
332
+ legacy.drawImage(source, args[4], args[5], args[6], args[7])
333
+ } else if (args.length >= 4) {
334
+ legacy.drawImage(source, args[0], args[1], args[2], args[3])
335
+ } else {
336
+ legacy.drawImage(
337
+ source,
338
+ args[0] || 0,
339
+ args[1] || 0,
340
+ image.width || 0,
341
+ image.height || 0
342
+ )
343
+ }
344
+ markDirty()
345
+ } catch (error) {
346
+ // 单张图片绘制失败时跳过,避免中断整个动画
347
+ }
348
+ },
349
+ createLinearGradient(x0, y0, x1, y1) {
350
+ return legacy.createLinearGradient(x0, y0, x1, y1)
351
+ },
352
+ createRadialGradient(x0, y0, r0, x1, y1, r1) {
353
+ return legacy.createRadialGradient(x0, y0, r0, x1, y1, r1)
354
+ },
355
+ setLineDash(segments, offset) {
356
+ state.lineDash = Array.isArray(segments) ? segments.slice() : []
357
+ if (typeof offset === 'number') {
358
+ state.lineDashOffset = offset
359
+ }
360
+ },
361
+ fillText(text, x, y) {
362
+ sync()
363
+ if (typeof legacy.fillText === 'function') {
364
+ legacy.fillText(text, x, y)
365
+ }
366
+ markDirty()
367
+ },
368
+ strokeText(text, x, y) {
369
+ sync()
370
+ if (typeof legacy.strokeText === 'function') {
371
+ legacy.strokeText(text, x, y)
372
+ }
373
+ markDirty()
374
+ },
375
+ measureText(text) {
376
+ const fontSize = parseFontSize(state.font)
377
+ return { width: String(text == null ? '' : text).length * fontSize * 0.6 }
378
+ }
379
+ }
380
+
381
+ return {
382
+ canvas,
383
+ context,
384
+ flush,
385
+ destroy() {
386
+ destroyed = true
387
+ rafIds.forEach((timer) => clearTimeout(timer))
388
+ rafIds.clear()
389
+ flushTimer = null
390
+ dirty = false
391
+ stack.length = 0
392
+ }
393
+ }
394
+ }