u-canvas.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. <template>
  2. <view class="u-canvas"
  3. :id="rootId"
  4. :style="{
  5. width: useRootHeightAndWidth ? '100%' : 'auto',
  6. height: useRootHeightAndWidth ? '100%' : 'auto',
  7. }">
  8. <!-- #ifdef MP || H5 -->
  9. <canvas
  10. class="u-canvas__canvas"
  11. :id="canvasId"
  12. :canvas-id="canvasId"
  13. type="2d"
  14. :style="{ width: width + unit, height: height + unit }"
  15. @touchstart="onTouchStart"
  16. @touchmove="onTouchMove"
  17. @touchend="onTouchEnd"/>
  18. <!-- #endif -->
  19. <!-- #ifdef APP-PLUS -->
  20. <canvas
  21. class="u-canvas__canvas"
  22. :id="canvasId"
  23. :canvas-id="canvasId"
  24. :style="{ width: width + unit, height: height + unit }"
  25. @touchstart="onTouchStart"
  26. @touchmove="onTouchMove"
  27. @touchend="onTouchEnd"/>
  28. <!-- #endif -->
  29. <!-- #ifdef APP-NVUE -->
  30. <gcanvas class="u-canvas__canvas" ref="gcanvess"
  31. :style="{ width: width + unit, height: height + unit }"
  32. @touchstart="onTouchStart"
  33. @touchmove="onTouchMove"
  34. @touchend="onTouchEnd">
  35. </gcanvas>
  36. <!-- #endif -->
  37. </view>
  38. </template>
  39. <script>
  40. // #ifdef APP-NVUE
  41. // https://github.com/dcloudio/NvueCanvasDemo/blob/master/README.md
  42. import {
  43. enable,
  44. WeexBridge,
  45. Image as GImage
  46. } from '../../libs/util/gcanvas/index.js';
  47. // #endif
  48. let canvasNode = null;
  49. export default {
  50. name: "u-canvas",
  51. props: {
  52. canvasId: {
  53. type: String,
  54. default: () => {
  55. return `u-canvas${Math.floor(Math.random() * 1000000)}`
  56. }
  57. },
  58. width: {
  59. type: [String, Number],
  60. default: 300
  61. },
  62. height: {
  63. type: [String, Number],
  64. default: 300
  65. },
  66. unit: {
  67. type: String,
  68. default: 'px'
  69. },
  70. useRootHeightAndWidth: {
  71. type: Boolean,
  72. default: false
  73. },
  74. // 背景色
  75. bgColor: {
  76. type: String,
  77. default: '#ffffff'
  78. }
  79. },
  80. data() {
  81. return {
  82. rootId: `rootId${Number(Math.random() * 100).toFixed(0)}`,
  83. ganvas: null,
  84. canvasContext: null,
  85. widthLocal: this.width,
  86. heightLocal: this.height,
  87. ctx: null
  88. };
  89. },
  90. computed: {
  91. // 计算实际画布尺寸
  92. actualWidth() {
  93. return this.useRootHeightAndWidth ? this.widthLocal : Number(this.width);
  94. },
  95. actualHeight() {
  96. return this.useRootHeightAndWidth ? this.heightLocal : Number(this.height);
  97. }
  98. },
  99. methods: {
  100. // 添加触摸事件处理方法
  101. onTouchStart(e) {
  102. this.$emit('touchstart', e);
  103. },
  104. onTouchMove(e) {
  105. this.$emit('touchmove', e);
  106. },
  107. onTouchEnd(e) {
  108. this.$emit('touchend', e);
  109. },
  110. /**
  111. * 获取节点
  112. * @param id 节点id
  113. * @param isCanvas 是否为Canvas节点
  114. * @return {Promise<unknown>}
  115. */
  116. async getCanvasNode(id, isCanvas = true) {
  117. let that = this
  118. return new Promise((resolve, reject) => {
  119. try {
  120. // #ifdef APP-NVUE
  121. setTimeout(() => {
  122. /*获取元素引用*/
  123. this.ganvas = this.$refs["gcanvess"]
  124. /*通过元素引用获取canvas对象*/
  125. let canvasNode = enable(this.ganvas, {
  126. bridge: WeexBridge
  127. })
  128. resolve(canvasNode)
  129. }, 200)
  130. // #endif
  131. // #ifndef APP-PLUS-NVUE
  132. const query = uni.createSelectorQuery().in(that).select(`#${id}`);
  133. query.fields({
  134. node: true,
  135. size: true
  136. })
  137. .exec((res) => {
  138. if (isCanvas) {
  139. if (res[0]?.node) {
  140. resolve(res[0].node)
  141. } else {
  142. resolve(false)
  143. console.error("获取节点出错", res)
  144. }
  145. } else {
  146. resolve(res[0])
  147. }
  148. })
  149. // #endif
  150. } catch (e) {
  151. console.error("获取节点失败", e)
  152. }
  153. })
  154. },
  155. /**
  156. * 获取Canvas上下文
  157. */
  158. getCanvasContext() {
  159. // #ifdef APP-PLUS
  160. return uni.createCanvasContext(this.canvasId, this);
  161. // #endif
  162. // #ifdef APP-PLUS-NVUE || MP || H5
  163. return canvasNode.getContext('2d');
  164. // #endif
  165. },
  166. /**
  167. * 初始化Canvas
  168. */
  169. async initCanvas() {
  170. try {
  171. canvasNode = await this.getCanvasNode(this.canvasId);
  172. // #ifdef MP-WEIXIN
  173. // 在微信小程序中,为了提高清晰度,需要考虑设备像素比
  174. const dpr = uni.getSystemInfoSync().pixelRatio;
  175. if(canvasNode) {
  176. // 设置canvas实际绘制尺寸为显示尺寸的dpr倍
  177. canvasNode.width = this.actualWidth * dpr;
  178. canvasNode.height = this.actualHeight * dpr;
  179. }
  180. // #endif
  181. this.ctx = this.getCanvasContext();
  182. // #ifdef MP-WEIXIN
  183. if(this.ctx) {
  184. this.ctx.scale(dpr, dpr);
  185. }
  186. // #endif
  187. // 初始化背景,但不在微信小程序中调用draw
  188. this.clearCanvas();
  189. } catch (error) {
  190. console.error("初始化Canvas失败:", error);
  191. }
  192. },
  193. /**
  194. * 清空画布
  195. */
  196. clearCanvas() {
  197. if (!this.ctx) return;
  198. this.clearRect(0, 0, this.actualWidth, this.actualHeight);
  199. // 填充背景色
  200. this.beginPath();
  201. this.rect(0, 0, this.actualWidth, this.actualHeight);
  202. this.setFillStyle(this.bgColor);
  203. this.fill();
  204. this.draw();
  205. },
  206. rect(x, y, width, height) {
  207. if (!this.ctx) return;
  208. this.ctx.rect(x, y, width, height);
  209. },
  210. clearRect(x, y, width, height) {
  211. if (!this.ctx) return;
  212. this.ctx.clearRect(x, y, width, height);
  213. },
  214. fill() {
  215. if (!this.ctx) return;
  216. this.ctx.fill();
  217. },
  218. setFillStyle(color) {
  219. if (!this.ctx) return;
  220. // #ifndef APP-PLUS-NVUE
  221. if (this.ctx.setFillStyle) {
  222. this.ctx.setFillStyle(color);
  223. } else {
  224. this.ctx.fillStyle = color;
  225. }
  226. // #endif
  227. // #ifdef APP-PLUS-NVUE
  228. this.ctx.setFillStyle(color);
  229. // #endif
  230. },
  231. /**
  232. * 设置线条样式
  233. */
  234. setLineStyle(lineColor, lineWidth) {
  235. if (!this.ctx) return;
  236. this.setLineCap('round');
  237. this.setLineJoin('round');
  238. this.setStrokeStyle(lineColor);
  239. this.setLineWidth(lineWidth);
  240. },
  241. setLineCap(lineCap = 'round') {
  242. if (!this.ctx) return;
  243. if (this.ctx.setLineCap) {
  244. this.ctx.setLineCap(lineCap);
  245. } else {
  246. this.ctx.lineCap = lineCap;
  247. }
  248. },
  249. setLineJoin(lineJoin = 'round') {
  250. if (!this.ctx) return;
  251. if (this.ctx.setLineJoin) {
  252. this.ctx.setLineJoin(lineJoin);
  253. } else {
  254. this.ctx.lineJoin = lineJoin;
  255. }
  256. },
  257. setStrokeStyle(color) {
  258. if (!this.ctx) return;
  259. if (this.ctx.setStrokeStyle) {
  260. this.ctx.setStrokeStyle(color);
  261. } else {
  262. this.ctx.strokeStyle = color;
  263. }
  264. },
  265. setLineWidth(width) {
  266. if (!this.ctx) return;
  267. if (this.ctx.setLineWidth) {
  268. this.ctx.setLineWidth(width);
  269. } else {
  270. this.ctx.lineWidth = width;
  271. }
  272. },
  273. /**
  274. * 开始路径
  275. */
  276. beginPath() {
  277. if (!this.ctx) return;
  278. this.ctx.beginPath();
  279. },
  280. /**
  281. * 移动到某点
  282. */
  283. moveTo(x, y) {
  284. if (!this.ctx) return;
  285. this.ctx.moveTo(x, y);
  286. },
  287. /**
  288. * 画线到某点
  289. */
  290. lineTo(x, y) {
  291. if (!this.ctx) return;
  292. this.ctx.lineTo(x, y);
  293. },
  294. /**
  295. * 描边
  296. */
  297. stroke() {
  298. if (!this.ctx) return;
  299. this.ctx.stroke();
  300. },
  301. /**
  302. * 关闭路径
  303. */
  304. closePath() {
  305. if (!this.ctx) return;
  306. this.ctx.closePath();
  307. },
  308. /**
  309. * 绘制操作
  310. */
  311. draw(isLastDraw = false) {
  312. // #ifndef MP-WEIXIN
  313. if (this.ctx && typeof this.ctx.draw === 'function') {
  314. this.ctx.draw(isLastDraw);
  315. }
  316. // #endif
  317. },
  318. /**
  319. * 导出图片
  320. */
  321. exportImage(fileType = 'png', quality = 1) {
  322. return new Promise((resolve, reject) => {
  323. // #ifdef MP-WEIXIN
  324. // 微信小程序中需要先完成绘制,然后导出图片
  325. setTimeout(() => {
  326. uni.canvasToTempFilePath({
  327. x: 0,
  328. y: 0,
  329. width: this.actualWidth,
  330. height: this.actualHeight,
  331. destWidth: this.actualWidth * 2, // 使用双倍尺寸以提高清晰度
  332. destHeight: this.actualHeight * 2,
  333. canvas: canvasNode, // 2d必须
  334. canvasId: this.canvasId,
  335. fileType: fileType,
  336. quality: quality,
  337. success: (res) => {
  338. resolve(res.tempFilePath);
  339. },
  340. fail: (err) => {
  341. console.error('导出图片失败:', err);
  342. reject(err);
  343. }
  344. }, this);
  345. }, 50); // 等待50毫秒确保绘制完成
  346. // #endif
  347. // #ifndef MP-WEIXIN
  348. uni.canvasToTempFilePath({
  349. canvas: canvasNode, // 2d必须
  350. canvasId: this.canvasId,
  351. fileType: fileType,
  352. quality: quality,
  353. success: (res) => {
  354. resolve(res.tempFilePath);
  355. },
  356. fail: (err) => {
  357. console.error('导出图片失败:', err);
  358. reject(err);
  359. }
  360. }, this);
  361. // #endif
  362. });
  363. },
  364. /**
  365. * 使用根节点宽高 设置新的size
  366. * @return {Promise<void>}
  367. */
  368. async setNewSize(){
  369. const rootNode = await this.getCanvasNode(this.rootId, false);
  370. const { width , height } = rootNode;
  371. this.widthLocal = height;
  372. this.heightLocal = width;
  373. }
  374. },
  375. async mounted() {
  376. // 如果使用根节点的宽高 则 重新设置 size
  377. if(this.useRootHeightAndWidth){
  378. await this.setNewSize();
  379. }
  380. // 初始化Canvas
  381. await this.initCanvas();
  382. }
  383. };
  384. </script>
  385. <style lang="scss" scoped>
  386. .u-canvas {
  387. position: relative;
  388. overflow: hidden;
  389. }
  390. .u-canvas__canvas {
  391. display: block;
  392. }
  393. </style>