tools.ts
11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
/* eslint-disable prefer-destructuring */
import Taro from '@tarojs/taro'
// 全局变量声明
// eslint-disable-next-line no-undef
const wx = globalThis.wx || {};
/**
* @description 生成随机字符串
* @param { number } length - 字符串长度
* @returns { string }
*/
export function randomString(length) {
let str = Math.random().toString(36).substr(2);
if (str.length >= length) {
return str.substr(0, length);
}
str += randomString(length - str.length);
return str;
}
/**
* 随机创造一个id
* @param { number } length - 字符串长度
* @returns { string }
*/
export function getRandomId(prefix = 'canvas', length = 10) {
return prefix + randomString(length);
}
/**
* @description 获取最大高度
* @param {} config
* @returns { number }
*/
// export function getHeight (config) {
// const getTextHeight = text => {
// const fontHeight = text.lineHeight || text.fontSize
// let height = 0
// if (text.baseLine === 'top') {
// height = fontHeight
// } else if (text.baseLine === 'middle') {
// height = fontHeight / 2
// } else {
// height = 0
// }
// return height
// }
// const heightArr: number[] = [];
// (config.blocks || []).forEach(item => {
// heightArr.push(item.y + item.height)
// });
// (config.texts || []).forEach(item => {
// let height
// if (Object.prototype.toString.call(item.text) === '[object Array]') {
// item.text.forEach(i => {
// height = getTextHeight({ ...i, baseLine: item.baseLine })
// heightArr.push(item.y + height)
// })
// } else {
// height = getTextHeight(item)
// heightArr.push(item.y + height)
// }
// });
// (config.images || []).forEach(item => {
// heightArr.push(item.y + item.height)
// });
// (config.lines || []).forEach(item => {
// heightArr.push(item.startY)
// heightArr.push(item.endY)
// })
// const sortRes = heightArr.sort((a, b) => b - a)
// let canvasHeight = 0
// if (sortRes.length > 0) {
// canvasHeight = sortRes[0]
// }
// if (config.height < canvasHeight || !config.height) {
// return canvasHeight
// }
// return config.height
// }
/**
* 将http转为https
* @param {String}} rawUrl 图片资源url
* @returns { string }
*/
export function mapHttpToHttps(rawUrl) {
if (rawUrl.indexOf(':') < 0 || rawUrl.startsWith('http://tmp')) {
return rawUrl;
}
const urlComponent = rawUrl.split(':');
if (urlComponent.length === 2) {
if (urlComponent[0] === 'http') {
urlComponent[0] = 'https';
return `${urlComponent[0]}:${urlComponent[1]}`;
}
}
return rawUrl;
}
/**
* 获取 rpx => px 的转换系数
* @returns { number } factor 单位转换系数 1rpx = factor * px
*/
export const getFactor = () => {
try {
// 使用新的API替代已废弃的getSystemInfoSync
const windowInfo = Taro.getWindowInfo();
const { screenWidth } = windowInfo;
return screenWidth / 750;
} catch (error) {
console.warn('获取窗口信息失败,使用默认转换系数:', error);
// 如果新API不可用,使用默认值(基于iPhone6的屏幕宽度375px)
return 375 / 750;
}
};
/**
* rpx => px 单位转换
* @param { number } rpx - 需要转换的数值
* @param { number } factor - 转化因子
* @returns { number }
*/
export const toPx = (rpx, factor = getFactor()) =>
parseInt(String(rpx * factor), 10);
/**
* px => rpx 单位转换
* @param { number } px - 需要转换的数值
* @param { number } factor - 转化因子
* @returns { number }
*/
export const toRpx = (px, factor = getFactor()) =>
parseInt(String(px / factor), 10);
// 图片缓存对象
const imageCache = new Map<string, string>();
/**
* 下载图片资源(优化版本,支持缓存和更快的超时设置)
* @param { string } url
* @param { number } retryCount - 重试次数
* @returns { Promise }
*/
export function downImage(url: string, retryCount = 2): Promise<string> {
return new Promise<string>((resolve, reject) => {
// 检查缓存
if (imageCache.has(url)) {
resolve(imageCache.get(url)!);
return;
}
// eslint-disable-next-line no-undef
if (/^http/.test(url) && !new RegExp(wx.env.USER_DATA_PATH).test(url)) {
// wx.env.USER_DATA_PATH 文件系统中的用户目录路径
const attemptDownload = (attempt = 0) => {
Taro.downloadFile({
url: mapHttpToHttps(url),
timeout: 6000, // 减少超时时间到6秒,提高响应速度
success: (res) => {
if (res.statusCode === 200) {
// 缓存下载结果
imageCache.set(url, res.tempFilePath as string);
resolve(res.tempFilePath as string);
} else {
if (attempt < retryCount) {
// 减少重试延时到500ms
setTimeout(() => attemptDownload(attempt + 1), 500);
} else {
reject(res);
}
}
},
fail(err) {
// 如果是代理连接问题,尝试使用原始URL
if (err.errMsg && err.errMsg.includes('127.0.0.1:7890')) {
if (attempt < retryCount) {
setTimeout(() => {
Taro.downloadFile({
url: url, // 使用原始URL,不转换https
timeout: 6000,
success: (res) => {
if (res.statusCode === 200) {
imageCache.set(url, res.tempFilePath as string);
resolve(res.tempFilePath as string);
} else {
reject(res);
}
},
fail: () => {
if (attempt + 1 < retryCount) {
attemptDownload(attempt + 1);
} else {
reject(err);
}
}
});
}, 500);
} else {
reject(err);
}
} else if (attempt < retryCount) {
setTimeout(() => attemptDownload(attempt + 1), 500);
} else {
reject(err);
}
}
});
};
attemptDownload();
} else {
resolve(url as string); // 支持本地地址
}
});
}
/**
* 批量下载图片并获取图片信息(优化版本,支持并发下载)
* @param {Array} images 图片数组
* @returns { Promise } result 整理后的图片信息数组
*/
export const batchGetImageInfo = (images) => {
// 限制并发数量,避免过多请求导致性能问题
const concurrencyLimit = 3;
const chunks = [];
// 将图片数组分块处理
for (let i = 0; i < images.length; i += concurrencyLimit) {
chunks.push(images.slice(i, i + concurrencyLimit));
}
// 串行处理每个块,块内并行处理
return chunks.reduce((promise, chunk) => {
return promise.then(results => {
const chunkPromises = chunk.map((item, index) =>
getImageInfo(item, results.length + index)
);
return Promise.all(chunkPromises).then(chunkResults =>
results.concat(chunkResults)
);
});
}, Promise.resolve([]));
};
/**
* 下载图片并获取图片信息(优化版本)
* @param {} item 图片参数信息
* @param {} index 图片下标
* @returns { Promise } result 整理后的图片信息
*/
export const getImageInfo = (item, index) =>
new Promise((resolve, reject) => {
const { x, y, width, height, url, zIndex } = item;
// 预先计算一些固定值,避免重复计算
const borderRadius = item.borderRadius || 0;
const borderWidth = item.borderWidth || 0;
const borderColor = item.borderColor;
const borderRadiusGroup = item.borderRadiusGroup;
const itemZIndex = typeof zIndex !== 'undefined' ? zIndex : index;
downImage(url).then((imgPath) => {
// 使用更快的图片信息获取方式
Taro.getImageInfo({ src: imgPath })
.then((imgInfo) => {
// 获取图片信息
// 根据画布的宽高计算出图片绘制的大小,这里会保证图片绘制不变形, 即宽高比不变,截取再拉伸
const imgWidth = toRpx(imgInfo.width); // 图片真实宽度 单位 px
const imgHeight = toRpx(imgInfo.height); // 图片真实高度 单位 px
// 优化计算逻辑,减少重复计算
const aspectRatio = imgWidth / imgHeight;
const targetRatio = width / height;
let sx, sy; // 截图的起点坐标
if (aspectRatio <= targetRatio) {
sx = 0;
sy = (imgHeight - (imgWidth / width) * height) / 2;
} else {
sy = 0;
sx = (imgWidth - (imgHeight / height) * width) / 2;
}
// 给 canvas 画图准备参数,详见 ./draw.ts-drawImage
const result = {
type: 'image',
borderRadius,
borderWidth,
borderColor,
borderRadiusGroup,
zIndex: itemZIndex,
imgPath: imgPath, // 使用下载后的临时文件路径
sx,
sy,
sw: imgWidth - sx * 2,
sh: imgHeight - sy * 2,
x,
y,
w: width,
h: height
};
resolve(result);
})
.catch((err) => {
reject(err);
});
}).catch(reject);
});
/**
* 获取线性渐变色
* @param {CanvasContext} ctx canvas 实例对象
* @param {String} color 线性渐变色,如 'linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #fff 100%)'
* @param {Number} startX 起点 x 坐标
* @param {Number} startY 起点 y 坐标
* @param {Number} w 宽度
* @param {Number} h 高度
* @returns {}
*/
// TODO: 待优化, 支持所有角度,多个颜色的线性渐变
export function getLinearColor(
ctx,
color,
startX,
startY,
w,
h
) {
if (
typeof startX !== 'number' ||
typeof startY !== 'number' ||
typeof w !== 'number' ||
typeof h !== 'number'
) {
console.warn('坐标或者宽高只支持数字');
return color;
}
let grd = color;
if (color.includes('linear-gradient')) {
// fillStyle 不支持线性渐变色
const colorList = color.match(/\((\d+)deg,\s(.+)\s\d+%,\s(.+)\s\d+%/);
const radian = colorList[1]; // 渐变弧度(角度)
const color1 = colorList[2];
const color2 = colorList[3];
const L = Math.sqrt(w * w + h * h);
const x = Math.ceil(Math.sin(180 - radian) * L);
const y = Math.ceil(Math.cos(180 - radian) * L);
// 根据弧度和宽高确定渐变色的两个点的坐标
if (Number(radian) === 180 || Number(radian) === 0) {
if (Number(radian) === 180) {
grd = ctx.createLinearGradient(startX, startY, startX, startY + h);
}
if (Number(radian) === 0) {
grd = ctx.createLinearGradient(startX, startY + h, startX, startY);
}
} else if (radian > 0 && radian < 180) {
grd = ctx.createLinearGradient(startX, startY, x + startX, y + startY);
} else {
throw new Error('只支持0 <= 颜色弧度 <= 180');
}
grd.addColorStop(0, color1);
grd.addColorStop(1, color2);
}
return grd;
}
/**
* 根据文字对齐方式设置坐标
* @param {*} imgPath
* @param {*} index
* @returns { Promise }
*/
export function getTextX(textAlign, x, width) {
let newX = x;
if (textAlign === 'center') {
newX = width / 2 + x;
} else if (textAlign === 'right') {
newX = width + x;
}
return newX;
}