index.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
  1. import { getDevicePixelRatio, isEmpty } from "@/cool";
  2. import { getCurrentInstance } from "vue";
  3. import type {
  4. CropImageResult,
  5. DivRenderOptions,
  6. ImageRenderOptions,
  7. TextRenderOptions,
  8. TransformOptions
  9. } from "../types";
  10. /**
  11. * Canvas 绘图类,封装了常用的绘图操作
  12. */
  13. export class Canvas {
  14. // uni-app CanvasContext 对象
  15. context: CanvasContext | null = null;
  16. // 2D 渲染上下文
  17. ctx: CanvasRenderingContext2D | null = null;
  18. // 组件作用域(用于小程序等环境)
  19. scope: ComponentPublicInstance | null = null;
  20. // 画布ID
  21. canvasId: string | null = null;
  22. // 渲染队列,存储所有待渲染的异步操作
  23. renderQuene: (() => Promise<void>)[] = [];
  24. // 图片渲染队列,存储所有待处理的图片参数
  25. imageQueue: ImageRenderOptions[] = [];
  26. /**
  27. * 构造函数
  28. * @param canvasId 画布ID
  29. */
  30. constructor(canvasId: string) {
  31. const { proxy } = getCurrentInstance()!;
  32. // 当前页面作用域
  33. this.scope = proxy;
  34. // 画布ID
  35. this.canvasId = canvasId;
  36. }
  37. /**
  38. * 创建画布上下文
  39. * @returns Promise<void>
  40. */
  41. async create(): Promise<void> {
  42. const dpr = getDevicePixelRatio(); // 获取设备像素比
  43. return new Promise((resolve) => {
  44. uni.createCanvasContextAsync({
  45. id: this.canvasId!,
  46. component: this.scope,
  47. success: (context: CanvasContext) => {
  48. this.context = context;
  49. this.ctx = context.getContext("2d")!;
  50. this.ctx.scale(dpr, dpr); // 按照 dpr 缩放,保证高清
  51. resolve();
  52. }
  53. });
  54. });
  55. }
  56. /**
  57. * 设置画布高度
  58. * @param value 高度
  59. * @returns Canvas
  60. */
  61. height(value: number): Canvas {
  62. this.ctx!.canvas.height = value;
  63. return this;
  64. }
  65. /**
  66. * 设置画布宽度
  67. * @param value 宽度
  68. * @returns Canvas
  69. */
  70. width(value: number): Canvas {
  71. this.ctx!.canvas.width = value;
  72. return this;
  73. }
  74. /**
  75. * 添加块(矩形/圆角矩形)渲染到队列
  76. * @param options DivRenderOptions
  77. * @returns Canvas
  78. */
  79. div(options: DivRenderOptions): Canvas {
  80. const render = async () => {
  81. this.divRender(options);
  82. };
  83. this.renderQuene.push(render);
  84. return this;
  85. }
  86. /**
  87. * 添加文本渲染到队列
  88. * @param options TextRenderOptions
  89. * @returns Canvas
  90. */
  91. text(options: TextRenderOptions): Canvas {
  92. const render = async () => {
  93. this.textRender(options);
  94. };
  95. this.renderQuene.push(render);
  96. return this;
  97. }
  98. /**
  99. * 添加图片渲染到队列
  100. * @param options ImageRenderOptions
  101. * @returns Canvas
  102. */
  103. image(options: ImageRenderOptions): Canvas {
  104. const render = async () => {
  105. await this.imageRender(options);
  106. };
  107. this.imageQueue.push(options);
  108. this.renderQuene.push(render);
  109. return this;
  110. }
  111. /**
  112. * 执行绘制流程(预加载图片后依次渲染队列)
  113. */
  114. async draw(): Promise<void> {
  115. // 如果有图片,先预加载
  116. if (!isEmpty(this.imageQueue)) {
  117. await this.preloadImage();
  118. }
  119. this.render();
  120. }
  121. /**
  122. * 下载图片(获取本地路径,兼容APP等平台)
  123. * @param item ImageRenderOptions
  124. * @returns Promise<void>
  125. */
  126. downloadImage(item: ImageRenderOptions): Promise<void> {
  127. return new Promise((resolve, reject) => {
  128. uni.getImageInfo({
  129. src: item.url,
  130. success: (res) => {
  131. // #ifdef APP
  132. item.url = res.path; // APP端需用本地路径
  133. // #endif
  134. resolve();
  135. },
  136. fail: (err) => {
  137. console.error(err);
  138. reject(err);
  139. }
  140. });
  141. });
  142. }
  143. /**
  144. * 预加载所有图片,确保图片可用
  145. */
  146. async preloadImage(): Promise<void> {
  147. await Promise.all(
  148. this.imageQueue.map((e) => {
  149. return this.downloadImage(e);
  150. })
  151. );
  152. }
  153. /**
  154. * 设置背景颜色
  155. * @param color 颜色字符串
  156. */
  157. private setBackground(color: string) {
  158. this.ctx!.fillStyle = color;
  159. }
  160. /**
  161. * 设置边框(支持圆角)
  162. * @param options DivRenderOptions
  163. */
  164. private setBorder(options: DivRenderOptions) {
  165. const { x, y, width: w = 0, height: h = 0, borderWidth, borderColor, radius: r } = options;
  166. if (borderWidth == null || borderColor == null) return;
  167. this.ctx!.lineWidth = borderWidth;
  168. this.ctx!.strokeStyle = borderColor;
  169. // 偏移距离,保证边框居中
  170. let p = borderWidth / 2;
  171. // 是否有圆角
  172. if (r != null) {
  173. this.drawRadius(x - p, y - p, w + 2 * p, h + 2 * p, r + p);
  174. this.ctx!.stroke();
  175. } else {
  176. this.ctx!.strokeRect(x - p, y - p, w + 2 * p, h + 2 * p);
  177. }
  178. }
  179. /**
  180. * 设置变换(缩放、旋转、平移)
  181. * @param options TransformOptions
  182. */
  183. private setTransform(options: TransformOptions) {
  184. const ctx = this.ctx!;
  185. // 平移
  186. if (options.translateX != null || options.translateY != null) {
  187. ctx.translate(options.translateX ?? 0, options.translateY ?? 0);
  188. }
  189. // 旋转(角度转弧度)
  190. if (options.rotate != null) {
  191. ctx.rotate((options.rotate * Math.PI) / 180);
  192. }
  193. // 缩放
  194. if (options.scale != null) {
  195. // 统一缩放
  196. ctx.scale(options.scale, options.scale);
  197. } else if (options.scaleX != null || options.scaleY != null) {
  198. // 分别缩放
  199. ctx.scale(options.scaleX ?? 1, options.scaleY ?? 1);
  200. }
  201. }
  202. /**
  203. * 绘制带圆角的路径
  204. * @param x 左上角x
  205. * @param y 左上角y
  206. * @param w 宽度
  207. * @param h 高度
  208. * @param r 圆角半径
  209. */
  210. private drawRadius(x: number, y: number, w: number, h: number, r: number) {
  211. // 圆角半径不能超过宽高一半
  212. const maxRadius = Math.min(w / 2, h / 2);
  213. const radius = Math.min(r, maxRadius);
  214. this.ctx!.beginPath();
  215. // 从左上角圆弧的结束点开始
  216. this.ctx!.moveTo(x + radius, y);
  217. // 顶边
  218. this.ctx!.lineTo(x + w - radius, y);
  219. // 右上角圆弧
  220. this.ctx!.arc(x + w - radius, y + radius, radius, -Math.PI / 2, 0);
  221. // 右边
  222. this.ctx!.lineTo(x + w, y + h - radius);
  223. // 右下角圆弧
  224. this.ctx!.arc(x + w - radius, y + h - radius, radius, 0, Math.PI / 2);
  225. // 底边
  226. this.ctx!.lineTo(x + radius, y + h);
  227. // 左下角圆弧
  228. this.ctx!.arc(x + radius, y + h - radius, radius, Math.PI / 2, Math.PI);
  229. // 左边
  230. this.ctx!.lineTo(x, y + radius);
  231. // 左上角圆弧
  232. this.ctx!.arc(x + radius, y + radius, radius, Math.PI, -Math.PI / 2);
  233. this.ctx!.closePath();
  234. }
  235. /**
  236. * 裁剪图片,支持多种裁剪模式
  237. * @param mode 裁剪模式
  238. * @param canvasWidth 目标区域宽度
  239. * @param canvasHeight 目标区域高度
  240. * @param imageWidth 原图宽度
  241. * @param imageHeight 原图高度
  242. * @param drawX 绘制起点X
  243. * @param drawY 绘制起点Y
  244. * @returns CropImageResult
  245. */
  246. private cropImage(
  247. mode:
  248. | "scaleToFill"
  249. | "aspectFit"
  250. | "aspectFill"
  251. | "center"
  252. | "top"
  253. | "bottom"
  254. | "left"
  255. | "right"
  256. | "topLeft"
  257. | "topRight"
  258. | "bottomLeft"
  259. | "bottomRight",
  260. canvasWidth: number,
  261. canvasHeight: number,
  262. imageWidth: number,
  263. imageHeight: number,
  264. drawX: number,
  265. drawY: number
  266. ): CropImageResult {
  267. // sx, sy, sw, sh: 原图裁剪区域
  268. // dx, dy, dw, dh: 画布绘制区域
  269. let sx = 0,
  270. sy = 0,
  271. sw = imageWidth,
  272. sh = imageHeight;
  273. let dx = drawX,
  274. dy = drawY,
  275. dw = canvasWidth,
  276. dh = canvasHeight;
  277. // 计算宽高比
  278. const imageRatio = imageWidth / imageHeight;
  279. const canvasRatio = canvasWidth / canvasHeight;
  280. switch (mode) {
  281. case "scaleToFill":
  282. // 拉伸填充整个区域,可能变形
  283. break;
  284. case "aspectFit":
  285. // 保持比例完整显示,可能有留白
  286. if (imageRatio > canvasRatio) {
  287. // 图片更宽,以宽度为准
  288. dw = canvasWidth;
  289. dh = canvasWidth / imageRatio;
  290. dx = drawX;
  291. dy = drawY + (canvasHeight - dh) / 2;
  292. } else {
  293. // 图片更高,以高度为准
  294. dw = canvasHeight * imageRatio;
  295. dh = canvasHeight;
  296. dx = drawX + (canvasWidth - dw) / 2;
  297. dy = drawY;
  298. }
  299. break;
  300. case "aspectFill":
  301. // 保持比例填充,可能裁剪
  302. if (imageRatio > canvasRatio) {
  303. // 图片更宽,裁剪左右
  304. const scaledWidth = imageHeight * canvasRatio;
  305. sx = (imageWidth - scaledWidth) / 2;
  306. sw = scaledWidth;
  307. } else {
  308. // 图片更高,裁剪上下
  309. const scaledHeight = imageWidth / canvasRatio;
  310. sy = (imageHeight - scaledHeight) / 2;
  311. sh = scaledHeight;
  312. }
  313. break;
  314. case "center":
  315. // 居中显示,不缩放,超出裁剪
  316. sx = Math.max(0, (imageWidth - canvasWidth) / 2);
  317. sy = Math.max(0, (imageHeight - canvasHeight) / 2);
  318. sw = Math.min(imageWidth, canvasWidth);
  319. sh = Math.min(imageHeight, canvasHeight);
  320. dx = drawX + Math.max(0, (canvasWidth - imageWidth) / 2);
  321. dy = drawY + Math.max(0, (canvasHeight - imageHeight) / 2);
  322. dw = sw;
  323. dh = sh;
  324. break;
  325. case "top":
  326. // 顶部对齐,水平居中
  327. sx = Math.max(0, (imageWidth - canvasWidth) / 2);
  328. sy = 0;
  329. sw = Math.min(imageWidth, canvasWidth);
  330. sh = Math.min(imageHeight, canvasHeight);
  331. dx = drawX + Math.max(0, (canvasWidth - imageWidth) / 2);
  332. dy = drawY;
  333. dw = sw;
  334. dh = sh;
  335. break;
  336. case "bottom":
  337. // 底部对齐,水平居中
  338. sx = Math.max(0, (imageWidth - canvasWidth) / 2);
  339. sy = Math.max(0, imageHeight - canvasHeight);
  340. sw = Math.min(imageWidth, canvasWidth);
  341. sh = Math.min(imageHeight, canvasHeight);
  342. dx = drawX + Math.max(0, (canvasWidth - imageWidth) / 2);
  343. dy = drawY + Math.max(0, canvasHeight - imageHeight);
  344. dw = sw;
  345. dh = sh;
  346. break;
  347. case "left":
  348. // 左对齐,垂直居中
  349. sx = 0;
  350. sy = Math.max(0, (imageHeight - canvasHeight) / 2);
  351. sw = Math.min(imageWidth, canvasWidth);
  352. sh = Math.min(imageHeight, canvasHeight);
  353. dx = drawX;
  354. dy = drawY + Math.max(0, (canvasHeight - imageHeight) / 2);
  355. dw = sw;
  356. dh = sh;
  357. break;
  358. case "right":
  359. // 右对齐,垂直居中
  360. sx = Math.max(0, imageWidth - canvasWidth);
  361. sy = Math.max(0, (imageHeight - canvasHeight) / 2);
  362. sw = Math.min(imageWidth, canvasWidth);
  363. sh = Math.min(imageHeight, canvasHeight);
  364. dx = drawX + Math.max(0, canvasWidth - imageWidth);
  365. dy = drawY + Math.max(0, (canvasHeight - imageHeight) / 2);
  366. dw = sw;
  367. dh = sh;
  368. break;
  369. case "topLeft":
  370. // 左上角对齐
  371. sx = 0;
  372. sy = 0;
  373. sw = Math.min(imageWidth, canvasWidth);
  374. sh = Math.min(imageHeight, canvasHeight);
  375. dx = drawX;
  376. dy = drawY;
  377. dw = sw;
  378. dh = sh;
  379. break;
  380. case "topRight":
  381. // 右上角对齐
  382. sx = Math.max(0, imageWidth - canvasWidth);
  383. sy = 0;
  384. sw = Math.min(imageWidth, canvasWidth);
  385. sh = Math.min(imageHeight, canvasHeight);
  386. dx = drawX + Math.max(0, canvasWidth - imageWidth);
  387. dy = drawY;
  388. dw = sw;
  389. dh = sh;
  390. break;
  391. case "bottomLeft":
  392. // 左下角对齐
  393. sx = 0;
  394. sy = Math.max(0, imageHeight - canvasHeight);
  395. sw = Math.min(imageWidth, canvasWidth);
  396. sh = Math.min(imageHeight, canvasHeight);
  397. dx = drawX;
  398. dy = drawY + Math.max(0, canvasHeight - imageHeight);
  399. dw = sw;
  400. dh = sh;
  401. break;
  402. case "bottomRight":
  403. // 右下角对齐
  404. sx = Math.max(0, imageWidth - canvasWidth);
  405. sy = Math.max(0, imageHeight - canvasHeight);
  406. sw = Math.min(imageWidth, canvasWidth);
  407. sh = Math.min(imageHeight, canvasHeight);
  408. dx = drawX + Math.max(0, canvasWidth - imageWidth);
  409. dy = drawY + Math.max(0, canvasHeight - imageHeight);
  410. dw = sw;
  411. dh = sh;
  412. break;
  413. }
  414. return {
  415. // 源图片裁剪区域
  416. sx,
  417. sy,
  418. sw,
  419. sh,
  420. // 目标绘制区域
  421. dx,
  422. dy,
  423. dw,
  424. dh
  425. } as CropImageResult;
  426. }
  427. /**
  428. * 获取文本每行内容(自动换行、支持省略号)
  429. * @param options TextRenderOptions
  430. * @returns string[] 每行内容
  431. */
  432. private getTextRows({
  433. content,
  434. fontSize = 14,
  435. width = 100,
  436. lineClamp = 1,
  437. overflow,
  438. letterSpace = 0,
  439. fontFamily = "sans-serif",
  440. fontWeight = "normal"
  441. }: TextRenderOptions) {
  442. // 临时设置字体以便准确测量
  443. this.ctx!.save();
  444. this.ctx!.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
  445. let arr: string[] = [""];
  446. let currentLineWidth = 0;
  447. for (let i = 0; i < content.length; i++) {
  448. const char = content.charAt(i);
  449. const charWidth = this.ctx!.measureText(char).width;
  450. // 计算当前字符加上字符间距后的总宽度
  451. const needSpace = arr[arr.length - 1].length > 0 && letterSpace > 0;
  452. const totalWidth = charWidth + (needSpace ? letterSpace : 0);
  453. if (currentLineWidth + totalWidth > width) {
  454. // 换行:新行的第一个字符不需要字符间距
  455. currentLineWidth = charWidth;
  456. arr.push(char);
  457. } else {
  458. // 最后一行且设置超出省略号
  459. if (overflow == "ellipsis" && arr.length == lineClamp) {
  460. const ellipsisWidth = this.ctx!.measureText("...").width;
  461. const ellipsisSpaceWidth = needSpace ? letterSpace : 0;
  462. if (
  463. currentLineWidth + totalWidth + ellipsisSpaceWidth + ellipsisWidth >
  464. width
  465. ) {
  466. arr[arr.length - 1] += "...";
  467. break;
  468. }
  469. }
  470. currentLineWidth += totalWidth;
  471. arr[arr.length - 1] += char;
  472. }
  473. }
  474. this.ctx!.restore();
  475. return arr;
  476. }
  477. /**
  478. * 渲染块(矩形/圆角矩形)
  479. * @param options DivRenderOptions
  480. */
  481. private divRender(options: DivRenderOptions) {
  482. const {
  483. x,
  484. y,
  485. width = 0,
  486. height = 0,
  487. radius,
  488. backgroundColor = "#fff",
  489. opacity = 1,
  490. scale,
  491. scaleX,
  492. scaleY,
  493. rotate,
  494. translateX,
  495. translateY
  496. } = options;
  497. this.ctx!.save();
  498. // 设置透明度
  499. this.ctx!.globalAlpha = opacity;
  500. // 设置背景色
  501. this.setBackground(backgroundColor);
  502. // 设置边框
  503. this.setBorder(options);
  504. // 设置变换
  505. this.setTransform({
  506. scale,
  507. scaleX,
  508. scaleY,
  509. rotate,
  510. translateX,
  511. translateY
  512. });
  513. // 判断是否有圆角
  514. if (radius != null) {
  515. // 绘制圆角路径
  516. this.drawRadius(x, y, width, height, radius);
  517. // 填充
  518. this.ctx!.fill();
  519. } else {
  520. // 普通矩形
  521. this.ctx!.fillRect(x, y, width, height);
  522. }
  523. this.ctx!.restore();
  524. }
  525. /**
  526. * 渲染文本
  527. * @param options TextRenderOptions
  528. */
  529. private textRender(options: TextRenderOptions) {
  530. let {
  531. fontSize = 14,
  532. textAlign,
  533. width,
  534. color = "#000000",
  535. x,
  536. y,
  537. letterSpace,
  538. lineHeight,
  539. fontFamily = "sans-serif",
  540. fontWeight = "normal",
  541. opacity = 1,
  542. scale,
  543. scaleX,
  544. scaleY,
  545. rotate,
  546. translateX,
  547. translateY
  548. } = options;
  549. // 如果行高为空,则设置为字体大小的1.4倍
  550. if (lineHeight == null) {
  551. lineHeight = fontSize * 1.4;
  552. }
  553. this.ctx!.save();
  554. // 应用变换
  555. this.setTransform({
  556. scale,
  557. scaleX,
  558. scaleY,
  559. rotate,
  560. translateX,
  561. translateY
  562. });
  563. // 设置字体样式
  564. this.ctx!.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
  565. // 设置透明度
  566. this.ctx!.globalAlpha = opacity;
  567. // 设置字体颜色
  568. this.ctx!.fillStyle = color;
  569. // 获取每行文本内容
  570. const rows = this.getTextRows(options);
  571. // 左偏移量
  572. let offsetLeft = 0;
  573. // 字体对齐(无字符间距时使用Canvas的textAlign)
  574. if (textAlign != null && width != null && (letterSpace == null || letterSpace <= 0)) {
  575. this.ctx!.textAlign = textAlign;
  576. switch (textAlign) {
  577. case "left":
  578. break;
  579. case "center":
  580. offsetLeft = width / 2;
  581. break;
  582. case "right":
  583. offsetLeft = width;
  584. break;
  585. }
  586. } else {
  587. // 有字符间距时,使用左对齐,手动控制位置
  588. this.ctx!.textAlign = "left";
  589. }
  590. // 计算行间距
  591. const lineGap = lineHeight - fontSize;
  592. // 逐行渲染
  593. for (let i = 0; i < rows.length; i++) {
  594. const currentRow = rows[i];
  595. const yPos = (i + 1) * fontSize + y + lineGap * i;
  596. if (letterSpace != null && letterSpace > 0) {
  597. // 逐字符计算宽度,确保字符间距准确
  598. let lineWidth = 0;
  599. for (let j = 0; j < currentRow.length; j++) {
  600. lineWidth += this.ctx!.measureText(currentRow.charAt(j)).width;
  601. if (j < currentRow.length - 1) {
  602. lineWidth += letterSpace;
  603. }
  604. }
  605. // 计算起始位置(考虑 textAlign)
  606. let startX = x;
  607. if (textAlign == "center" && width != null) {
  608. startX = x + (width - lineWidth) / 2;
  609. } else if (textAlign == "right" && width != null) {
  610. startX = x + width - lineWidth;
  611. }
  612. // 逐字符渲染
  613. let charX = startX;
  614. for (let j = 0; j < currentRow.length; j++) {
  615. const char = currentRow.charAt(j);
  616. this.ctx!.fillText(char, charX, yPos);
  617. // 移动到下一个字符位置
  618. charX += this.ctx!.measureText(char).width;
  619. if (j < currentRow.length - 1) {
  620. charX += letterSpace;
  621. }
  622. }
  623. } else {
  624. // 普通渲染(无字符间距)
  625. this.ctx!.fillText(currentRow, x + offsetLeft, yPos);
  626. }
  627. }
  628. this.ctx!.restore();
  629. }
  630. /**
  631. * 渲染图片
  632. * @param options ImageRenderOptions
  633. */
  634. private async imageRender(options: ImageRenderOptions): Promise<void> {
  635. return new Promise((resolve) => {
  636. this.ctx!.save();
  637. // 设置透明度
  638. this.ctx!.globalAlpha = options.opacity ?? 1;
  639. // 应用变换
  640. this.setTransform({
  641. scale: options.scale,
  642. scaleX: options.scaleX,
  643. scaleY: options.scaleY,
  644. rotate: options.rotate,
  645. translateX: options.translateX,
  646. translateY: options.translateY
  647. });
  648. // 如果有圆角,先绘制路径并裁剪
  649. if (options.radius != null) {
  650. this.drawRadius(
  651. options.x,
  652. options.y,
  653. options.width,
  654. options.height,
  655. options.radius
  656. );
  657. this.ctx!.clip();
  658. }
  659. const temp = this.imageQueue[0];
  660. let img: Image;
  661. // 微信小程序/鸿蒙环境创建图片
  662. // #ifdef MP-WEIXIN || APP-HARMONY
  663. img = this.context!.createImage();
  664. // #endif
  665. // 其他环境创建图片
  666. // #ifndef MP-WEIXIN || APP-HARMONY
  667. img = new Image();
  668. // #endif
  669. img.src = temp.url;
  670. img.onload = () => {
  671. if (options.mode != null) {
  672. let h: number;
  673. let w: number;
  674. // #ifdef H5
  675. h = img["height"];
  676. w = img["width"];
  677. // #endif
  678. // #ifndef H5
  679. h = img.height;
  680. w = img.width;
  681. // #endif
  682. // 按模式裁剪并绘制
  683. const { sx, sy, sw, sh, dx, dy, dw, dh } = this.cropImage(
  684. options.mode,
  685. temp.width, // 目标绘制区域宽度
  686. temp.height, // 目标绘制区域高度
  687. w, // 原图片宽度
  688. h, // 原图片高度
  689. temp.x, // 绘制X坐标
  690. temp.y // 绘制Y坐标
  691. );
  692. // 使用 drawImage 的完整参数形式进行精确裁剪和绘制
  693. this.ctx!.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
  694. } else {
  695. // 不指定模式时,直接绘制整个图片
  696. this.ctx!.drawImage(img, temp.x, temp.y, temp.width, temp.height);
  697. }
  698. this.ctx!.restore();
  699. this.imageQueue.shift(); // 移除已渲染图片
  700. resolve();
  701. };
  702. });
  703. }
  704. /**
  705. * 依次执行渲染队列中的所有操作
  706. */
  707. async render() {
  708. for (let i = 0; i < this.renderQuene.length; i++) {
  709. const r = this.renderQuene[i];
  710. await r();
  711. }
  712. }
  713. }
  714. /**
  715. * useCanvas 钩子函数,返回 Canvas 实例
  716. * @param canvasId 画布ID
  717. * @returns Canvas
  718. */
  719. export const useCanvas = (canvasId: string) => {
  720. return new Canvas(canvasId);
  721. };