Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 

735 linhas
25 KiB

  1. <template>
  2. <view
  3. class="u-canvas"
  4. :id="rootId"
  5. :style="{
  6. width: useRootHeightAndWidth ? '100%' : actualWidth + unit,
  7. height: useRootHeightAndWidth ? '100%' : actualHeight + unit
  8. }"
  9. >
  10. <!-- #ifdef MP || H5 -->
  11. <canvas
  12. class="u-canvas__canvas"
  13. :id="canvasId"
  14. :canvas-id="canvasId"
  15. type="2d"
  16. :disable-scroll="disableScroll"
  17. :style="{ width: actualWidth + unit, height: actualHeight + unit }"
  18. @touchstart="onTouchStart"
  19. @touchmove="onTouchMove"
  20. @touchend="onTouchEnd"
  21. />
  22. <!-- #endif -->
  23. <!-- #ifdef APP-PLUS -->
  24. <canvas
  25. class="u-canvas__canvas"
  26. :id="canvasId"
  27. :canvas-id="canvasId"
  28. :disable-scroll="disableScroll"
  29. :style="{ width: actualWidth + unit, height: actualHeight + unit }"
  30. @touchstart="onTouchStart"
  31. @touchmove="onTouchMove"
  32. @touchend="onTouchEnd"
  33. />
  34. <!-- #endif -->
  35. <!-- #ifdef APP-NVUE -->
  36. <gcanvas
  37. class="u-canvas__canvas"
  38. ref="gcanvas"
  39. :style="{ width: actualWidth + unit, height: actualHeight + unit }"
  40. @touchstart="onTouchStart"
  41. @touchmove="onTouchMove"
  42. @touchend="onTouchEnd"
  43. />
  44. <!-- #endif -->
  45. </view>
  46. </template>
  47. <script>
  48. // #ifdef APP-NVUE
  49. import {
  50. enable,
  51. WeexBridge,
  52. Image as GImage
  53. } from '../../libs/util/gcanvas/index.js';
  54. // #endif
  55. export default {
  56. name: 'u-canvas',
  57. emits: ['ready', 'touchstart', 'touchmove', 'touchend'],
  58. props: {
  59. canvasId: {
  60. type: String,
  61. default: () => `u-canvas${Math.floor(Math.random() * 1000000)}`
  62. },
  63. width: {
  64. type: [String, Number],
  65. default: 300
  66. },
  67. height: {
  68. type: [String, Number],
  69. default: 300
  70. },
  71. unit: {
  72. type: String,
  73. default: 'px'
  74. },
  75. useRootHeightAndWidth: {
  76. type: Boolean,
  77. default: false
  78. },
  79. bgColor: {
  80. type: String,
  81. default: '#ffffff'
  82. },
  83. disableScroll: {
  84. type: Boolean,
  85. default: false
  86. }
  87. },
  88. data() {
  89. return {
  90. rootId: `u-canvas-root-${Math.floor(Math.random() * 1000000)}`,
  91. ctx: null,
  92. widthLocal: this.parseSize(this.width),
  93. heightLocal: this.parseSize(this.height),
  94. fontSize: 12,
  95. fontFamily: 'sans-serif',
  96. fontWeight: 'normal',
  97. dpr: 1
  98. };
  99. },
  100. computed: {
  101. actualWidth() {
  102. return this.useRootHeightAndWidth ? this.widthLocal : this.parseSize(this.width);
  103. },
  104. actualHeight() {
  105. return this.useRootHeightAndWidth ? this.heightLocal : this.parseSize(this.height);
  106. }
  107. },
  108. watch: {
  109. width() {
  110. this.refresh();
  111. },
  112. height() {
  113. this.refresh();
  114. },
  115. bgColor() {
  116. this.clearCanvas();
  117. }
  118. },
  119. created() {
  120. this._canvasNode = null;
  121. this._selectorResult = null;
  122. this._canvasElement = null;
  123. this._imageCache = Object.create(null);
  124. this._isNvue = false;
  125. },
  126. mounted() {
  127. this.$nextTick(() => {
  128. this.initCanvas();
  129. });
  130. },
  131. methods: {
  132. parseSize(value) {
  133. if (typeof value === 'number') {
  134. return value;
  135. }
  136. if (typeof value !== 'string') {
  137. return 0;
  138. }
  139. if (value.endsWith('rpx') || value.endsWith('upx')) {
  140. return uni.upx2px(parseFloat(value));
  141. }
  142. if (value.endsWith('px')) {
  143. return parseFloat(value) || 0;
  144. }
  145. return parseFloat(value) || 0;
  146. },
  147. onTouchStart(event) {
  148. this.$emit('touchstart', event);
  149. },
  150. onTouchMove(event) {
  151. this.$emit('touchmove', event);
  152. },
  153. onTouchEnd(event) {
  154. this.$emit('touchend', event);
  155. },
  156. async getCanvasNode(id = this.canvasId, isCanvas = true) {
  157. return new Promise((resolve) => {
  158. try {
  159. // #ifdef APP-NVUE
  160. setTimeout(() => {
  161. const gcanvas = this.$refs.gcanvas;
  162. if (!gcanvas) {
  163. resolve(false);
  164. return;
  165. }
  166. this._isNvue = true;
  167. resolve(enable(gcanvas, { bridge: WeexBridge }));
  168. }, 100);
  169. // #endif
  170. // #ifndef APP-NVUE
  171. uni.createSelectorQuery()
  172. .in(this)
  173. .select(`#${id}`)
  174. .fields(
  175. {
  176. node: isCanvas,
  177. size: true
  178. },
  179. (res) => {
  180. resolve(res || false);
  181. }
  182. )
  183. .exec();
  184. // #endif
  185. } catch (error) {
  186. console.error('获取画布节点失败:', error);
  187. resolve(false);
  188. }
  189. });
  190. },
  191. getCanvasElement() {
  192. return this._canvasElement || null;
  193. },
  194. getRawContext() {
  195. return this.ctx;
  196. },
  197. getCanvasContext() {
  198. // #ifdef APP-PLUS
  199. return uni.createCanvasContext(this.canvasId, this);
  200. // #endif
  201. // #ifdef APP-NVUE
  202. return this._canvasElement && typeof this._canvasElement.getContext === 'function'
  203. ? this._canvasElement.getContext('2d')
  204. : null;
  205. // #endif
  206. // #ifdef MP || H5
  207. return this._canvasElement && typeof this._canvasElement.getContext === 'function'
  208. ? this._canvasElement.getContext('2d')
  209. : null;
  210. // #endif
  211. },
  212. async setNewSize() {
  213. const rootNode = await this.getCanvasNode(this.rootId, false);
  214. if (!rootNode) {
  215. return;
  216. }
  217. if (rootNode.width) {
  218. this.widthLocal = rootNode.width;
  219. }
  220. if (rootNode.height) {
  221. this.heightLocal = rootNode.height;
  222. }
  223. },
  224. async initCanvas(force = false) {
  225. try {
  226. if (this.useRootHeightAndWidth) {
  227. await this.setNewSize();
  228. }
  229. if (this.ctx && !force) {
  230. this.$emit('ready', {
  231. width: this.actualWidth,
  232. height: this.actualHeight
  233. });
  234. return true;
  235. }
  236. this._canvasNode = await this.getCanvasNode(this.canvasId);
  237. if (!this._canvasNode) {
  238. return false;
  239. }
  240. this._selectorResult = this._canvasNode;
  241. this._canvasElement = this._canvasNode.node || this._canvasNode;
  242. this.dpr = uni.getSystemInfoSync().pixelRatio || 1;
  243. // #ifdef MP || H5
  244. if (this._canvasElement) {
  245. this._canvasElement.width = Math.ceil(this.actualWidth * this.dpr);
  246. this._canvasElement.height = Math.ceil(this.actualHeight * this.dpr);
  247. }
  248. // #endif
  249. this.ctx = this.getCanvasContext();
  250. if (!this.ctx) {
  251. return false;
  252. }
  253. // #ifdef MP || H5
  254. if (typeof this.ctx.setTransform === 'function') {
  255. this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
  256. } else if (typeof this.ctx.scale === 'function') {
  257. this.ctx.scale(this.dpr, this.dpr);
  258. }
  259. // #endif
  260. this.applyFont();
  261. this.clearCanvas();
  262. this.$emit('ready', {
  263. width: this.actualWidth,
  264. height: this.actualHeight
  265. });
  266. return true;
  267. } catch (error) {
  268. console.error('初始化Canvas失败:', error);
  269. return false;
  270. }
  271. },
  272. refresh() {
  273. return this.initCanvas(true);
  274. },
  275. getWidth() {
  276. return this.actualWidth;
  277. },
  278. getHeight() {
  279. return this.actualHeight;
  280. },
  281. clearCanvas() {
  282. if (!this.ctx) return;
  283. this.clearRect(0, 0, this.actualWidth, this.actualHeight);
  284. if (this.bgColor && this.bgColor !== 'transparent') {
  285. this.beginPath();
  286. this.rect(0, 0, this.actualWidth, this.actualHeight);
  287. this.setFillStyle(this.bgColor);
  288. this.fill();
  289. }
  290. this.draw();
  291. },
  292. callContext(method, ...args) {
  293. if (this.ctx && typeof this.ctx[method] === 'function') {
  294. return this.ctx[method](...args);
  295. }
  296. return undefined;
  297. },
  298. rect(x, y, width, height) {
  299. return this.callContext('rect', x, y, width, height);
  300. },
  301. clearRect(x, y, width, height) {
  302. return this.callContext('clearRect', x, y, width, height);
  303. },
  304. fillRect(x, y, width, height) {
  305. return this.callContext('fillRect', x, y, width, height);
  306. },
  307. strokeRect(x, y, width, height) {
  308. return this.callContext('strokeRect', x, y, width, height);
  309. },
  310. fill() {
  311. return this.callContext('fill');
  312. },
  313. stroke() {
  314. return this.callContext('stroke');
  315. },
  316. beginPath() {
  317. return this.callContext('beginPath');
  318. },
  319. closePath() {
  320. return this.callContext('closePath');
  321. },
  322. moveTo(x, y) {
  323. return this.callContext('moveTo', x, y);
  324. },
  325. lineTo(x, y) {
  326. return this.callContext('lineTo', x, y);
  327. },
  328. arc(x, y, radius, startAngle, endAngle, anticlockwise = false) {
  329. return this.callContext('arc', x, y, radius, startAngle, endAngle, anticlockwise);
  330. },
  331. bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
  332. return this.callContext('bezierCurveTo', cp1x, cp1y, cp2x, cp2y, x, y);
  333. },
  334. quadraticCurveTo(cpx, cpy, x, y) {
  335. return this.callContext('quadraticCurveTo', cpx, cpy, x, y);
  336. },
  337. clip() {
  338. return this.callContext('clip');
  339. },
  340. save() {
  341. return this.callContext('save');
  342. },
  343. restore() {
  344. return this.callContext('restore');
  345. },
  346. translate(x, y) {
  347. return this.callContext('translate', x, y);
  348. },
  349. rotate(angle) {
  350. return this.callContext('rotate', angle);
  351. },
  352. scale(x, y) {
  353. return this.callContext('scale', x, y);
  354. },
  355. setFillStyle(color) {
  356. if (!this.ctx) return;
  357. if (typeof this.ctx.setFillStyle === 'function') {
  358. this.ctx.setFillStyle(color);
  359. } else {
  360. this.ctx.fillStyle = color;
  361. }
  362. },
  363. setStrokeStyle(color) {
  364. if (!this.ctx) return;
  365. if (typeof this.ctx.setStrokeStyle === 'function') {
  366. this.ctx.setStrokeStyle(color);
  367. } else {
  368. this.ctx.strokeStyle = color;
  369. }
  370. },
  371. setLineWidth(width) {
  372. if (!this.ctx) return;
  373. if (typeof this.ctx.setLineWidth === 'function') {
  374. this.ctx.setLineWidth(width);
  375. } else {
  376. this.ctx.lineWidth = width;
  377. }
  378. },
  379. setLineCap(lineCap = 'round') {
  380. if (!this.ctx) return;
  381. if (typeof this.ctx.setLineCap === 'function') {
  382. this.ctx.setLineCap(lineCap);
  383. } else {
  384. this.ctx.lineCap = lineCap;
  385. }
  386. },
  387. setLineJoin(lineJoin = 'round') {
  388. if (!this.ctx) return;
  389. if (typeof this.ctx.setLineJoin === 'function') {
  390. this.ctx.setLineJoin(lineJoin);
  391. } else {
  392. this.ctx.lineJoin = lineJoin;
  393. }
  394. },
  395. setTextAlign(align = 'left') {
  396. if (!this.ctx) return;
  397. if (typeof this.ctx.setTextAlign === 'function') {
  398. this.ctx.setTextAlign(align);
  399. } else {
  400. this.ctx.textAlign = align;
  401. }
  402. },
  403. setTextBaseline(baseline = 'alphabetic') {
  404. if (!this.ctx) return;
  405. if (typeof this.ctx.setTextBaseline === 'function') {
  406. this.ctx.setTextBaseline(baseline);
  407. } else {
  408. this.ctx.textBaseline = baseline;
  409. }
  410. },
  411. setFontSize(fontSize) {
  412. this.fontSize = Number(fontSize) || this.fontSize;
  413. if (this.ctx && typeof this.ctx.setFontSize === 'function') {
  414. this.ctx.setFontSize(this.fontSize);
  415. }
  416. this.applyFont();
  417. },
  418. setFont(font) {
  419. if (!this.ctx) return;
  420. if ('font' in this.ctx) {
  421. this.ctx.font = font;
  422. return;
  423. }
  424. const matched = String(font).match(/(\d+(?:\.\d+)?)px/);
  425. if (matched) {
  426. this.fontSize = Number(matched[1]);
  427. }
  428. this.applyFont();
  429. },
  430. setGlobalAlpha(alpha) {
  431. if (!this.ctx) return;
  432. if (typeof this.ctx.setGlobalAlpha === 'function') {
  433. this.ctx.setGlobalAlpha(alpha);
  434. } else {
  435. this.ctx.globalAlpha = alpha;
  436. }
  437. },
  438. setShadow(offsetX = 0, offsetY = 0, blur = 0, color = 'rgba(0,0,0,0)') {
  439. if (!this.ctx) return;
  440. if (typeof this.ctx.setShadow === 'function') {
  441. this.ctx.setShadow(offsetX, offsetY, blur, color);
  442. return;
  443. }
  444. this.ctx.shadowOffsetX = offsetX;
  445. this.ctx.shadowOffsetY = offsetY;
  446. this.ctx.shadowBlur = blur;
  447. this.ctx.shadowColor = color;
  448. },
  449. setLineStyle(lineColor, lineWidth) {
  450. this.setLineCap('round');
  451. this.setLineJoin('round');
  452. this.setStrokeStyle(lineColor);
  453. this.setLineWidth(lineWidth);
  454. },
  455. applyFont() {
  456. if (!this.ctx) return;
  457. const font = `${this.fontWeight === 'normal' ? '' : `${this.fontWeight} `}${this.fontSize}px ${this.fontFamily}`.trim();
  458. if ('font' in this.ctx) {
  459. this.ctx.font = font;
  460. } else if (typeof this.ctx.setFont === 'function') {
  461. this.ctx.setFont(font);
  462. } else if (typeof this.ctx.setFontSize === 'function') {
  463. this.ctx.setFontSize(this.fontSize);
  464. }
  465. },
  466. fillText(text, x, y) {
  467. return this.callContext('fillText', String(text), x, y);
  468. },
  469. measureText(text) {
  470. if (this.ctx && typeof this.ctx.measureText === 'function') {
  471. return this.ctx.measureText(String(text));
  472. }
  473. return {
  474. width: String(text).length * this.fontSize * 0.6
  475. };
  476. },
  477. createLinearGradient(x0, y0, x1, y1) {
  478. if (this.ctx && typeof this.ctx.createLinearGradient === 'function') {
  479. return this.ctx.createLinearGradient(x0, y0, x1, y1);
  480. }
  481. return null;
  482. },
  483. createRadialGradient(x0, y0, r0, x1, y1, r1) {
  484. if (this.ctx && typeof this.ctx.createRadialGradient === 'function') {
  485. return this.ctx.createRadialGradient(x0, y0, r0, x1, y1, r1);
  486. }
  487. return null;
  488. },
  489. loadImage(src) {
  490. if (this._imageCache[src]) {
  491. return Promise.resolve(this._imageCache[src]);
  492. }
  493. return new Promise((resolve, reject) => {
  494. let image = null;
  495. // #ifdef APP-NVUE
  496. image = new GImage();
  497. // #endif
  498. // #ifdef MP
  499. const canvas = this.getCanvasElement();
  500. if (canvas && typeof canvas.createImage === 'function') {
  501. image = canvas.createImage();
  502. }
  503. // #endif
  504. // #ifdef H5
  505. image = new Image();
  506. image.crossOrigin = 'anonymous';
  507. // #endif
  508. if (!image) {
  509. resolve(src);
  510. return;
  511. }
  512. image.onload = () => {
  513. this._imageCache[src] = image;
  514. resolve(image);
  515. };
  516. image.onerror = reject;
  517. image.src = src;
  518. });
  519. },
  520. async drawImage(source, ...args) {
  521. if (!this.ctx || typeof this.ctx.drawImage !== 'function') {
  522. return false;
  523. }
  524. if (typeof source !== 'string' || (typeof this.ctx.setFillStyle === 'function' && !this._isNvue)) {
  525. this.ctx.drawImage(source, ...args);
  526. return true;
  527. }
  528. const image = await this.loadImage(source);
  529. this.ctx.drawImage(image, ...args);
  530. return true;
  531. },
  532. draw(isLastDraw = false, callback) {
  533. if (this.ctx && typeof this.ctx.draw === 'function') {
  534. return this.ctx.draw(isLastDraw, callback);
  535. }
  536. if (typeof callback === 'function') {
  537. setTimeout(callback, 0);
  538. }
  539. return undefined;
  540. },
  541. toTempFilePath(options = {}) {
  542. return new Promise((resolve, reject) => {
  543. const width = options.width || this.actualWidth;
  544. const height = options.height || this.actualHeight;
  545. const request = {
  546. x: options.x || 0,
  547. y: options.y || 0,
  548. width,
  549. height,
  550. destWidth: options.destWidth || width,
  551. destHeight: options.destHeight || height,
  552. fileType: options.fileType || 'png',
  553. quality: options.quality === undefined ? 1 : options.quality
  554. };
  555. const success = (res) => {
  556. if (typeof options.success === 'function') options.success(res);
  557. resolve(res);
  558. };
  559. const fail = (err) => {
  560. if (typeof options.fail === 'function') options.fail(err);
  561. reject(err);
  562. };
  563. const complete = (res) => {
  564. if (typeof options.complete === 'function') options.complete(res);
  565. };
  566. // #ifdef H5
  567. const canvas = this.getCanvasElement() || (this.ctx && this.ctx.canvas);
  568. if (canvas && typeof canvas.toDataURL === 'function') {
  569. try {
  570. let exportCanvas = canvas;
  571. if (
  572. request.x !== 0 ||
  573. request.y !== 0 ||
  574. request.width !== this.actualWidth ||
  575. request.height !== this.actualHeight ||
  576. request.destWidth !== request.width ||
  577. request.destHeight !== request.height
  578. ) {
  579. exportCanvas = document.createElement('canvas');
  580. exportCanvas.width = request.destWidth;
  581. exportCanvas.height = request.destHeight;
  582. const exportCtx = exportCanvas.getContext('2d');
  583. exportCtx.drawImage(
  584. canvas,
  585. request.x * this.dpr,
  586. request.y * this.dpr,
  587. request.width * this.dpr,
  588. request.height * this.dpr,
  589. 0,
  590. 0,
  591. request.destWidth,
  592. request.destHeight
  593. );
  594. }
  595. const mime = request.fileType === 'jpg' || request.fileType === 'jpeg'
  596. ? 'image/jpeg'
  597. : 'image/png';
  598. const res = {
  599. tempFilePath: exportCanvas.toDataURL(mime, request.quality)
  600. };
  601. success(res);
  602. complete(res);
  603. return;
  604. } catch (error) {
  605. fail(error);
  606. complete(error);
  607. return;
  608. }
  609. }
  610. // #endif
  611. // #ifdef APP-NVUE
  612. if (this.ctx && typeof this.ctx.toTempFilePath === 'function') {
  613. this.ctx.toTempFilePath(
  614. request.x,
  615. request.y,
  616. request.width,
  617. request.height,
  618. request.destWidth,
  619. request.destHeight,
  620. request.fileType,
  621. request.quality,
  622. (res) => {
  623. success(res);
  624. complete(res);
  625. }
  626. );
  627. return;
  628. }
  629. // #endif
  630. const canvasNode = this.getCanvasElement();
  631. const uniOptions = {
  632. ...request,
  633. canvasId: this.canvasId,
  634. success,
  635. fail,
  636. complete
  637. };
  638. if (canvasNode) {
  639. uniOptions.canvas = canvasNode;
  640. }
  641. uni.canvasToTempFilePath(uniOptions, this);
  642. });
  643. },
  644. async exportImage(fileType = 'png', quality = 1) {
  645. const res = await this.toTempFilePath({
  646. fileType,
  647. quality,
  648. width: this.actualWidth,
  649. height: this.actualHeight,
  650. destWidth: this.actualWidth,
  651. destHeight: this.actualHeight
  652. });
  653. return res.tempFilePath || res.apFilePath;
  654. },
  655. getImageData(options = {}) {
  656. return new Promise((resolve, reject) => {
  657. const request = {
  658. canvasId: this.canvasId,
  659. x: options.x || 0,
  660. y: options.y || 0,
  661. width: options.width || this.actualWidth,
  662. height: options.height || this.actualHeight,
  663. success: (res) => {
  664. if (typeof options.success === 'function') options.success(res);
  665. resolve(res);
  666. },
  667. fail: (err) => {
  668. if (typeof options.fail === 'function') options.fail(err);
  669. reject(err);
  670. },
  671. complete: options.complete
  672. };
  673. uni.canvasGetImageData(request, this);
  674. });
  675. },
  676. putImageData(options = {}) {
  677. return new Promise((resolve, reject) => {
  678. const request = {
  679. canvasId: this.canvasId,
  680. x: options.x || 0,
  681. y: options.y || 0,
  682. width: options.width || this.actualWidth,
  683. height: options.height || this.actualHeight,
  684. data: options.data,
  685. success: (res) => {
  686. if (typeof options.success === 'function') options.success(res);
  687. resolve(res);
  688. },
  689. fail: (err) => {
  690. if (typeof options.fail === 'function') options.fail(err);
  691. reject(err);
  692. },
  693. complete: options.complete
  694. };
  695. uni.canvasPutImageData(request, this);
  696. });
  697. }
  698. }
  699. };
  700. </script>
  701. <style lang="scss" scoped>
  702. .u-canvas {
  703. position: relative;
  704. overflow: hidden;
  705. }
  706. .u-canvas__canvas {
  707. display: block;
  708. width: 100%;
  709. height: 100%;
  710. }
  711. </style>