FreeWrite.vue 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. <template>
  2. <view class="container">
  3. <view v-if="pinyin" class="pinyin">{{pinyin}}</view>
  4. <view class="canvas-box">
  5. <SvgIcon icon-class="hanzi-writer-bg" class="character-target-bg" :size="300" />
  6. <canvas :canvas-id="canvasId" ref="canvas" class="canvas" @touchstart.prevent="handleTouchStart"
  7. @touchmove.prevent="handleTouchMove" @touchend.prevent="handleTouchEnd"></canvas>
  8. </view>
  9. <view class="btn-list">
  10. <view class="btn-box" @click="resetHanzi" :style="{'opacity':disabled?'0.5':'1'}">
  11. <SvgIcon icon-class="reset" class="reset-btn" :size="18" />
  12. <text class="btn-text">重写</text>
  13. </view>
  14. <view class="btn-box" @click="play()">
  15. <SvgIcon icon-class="triangle" class="reset-btn" :size="14" />
  16. <text :class="['btn-text',currenHzData && currenHzData.history ? '' : 'disabled']">笔迹</text>
  17. </view>
  18. <view class="btn-box" @click="saveImage" :style="{'opacity':disabled?'0.5':'1'}">
  19. <SvgIcon icon-class="save" class="reset-btn" :size="14" />
  20. <text class="btn-text">保存</text>
  21. </view>
  22. </view>
  23. </view>
  24. </template>
  25. <script>
  26. export default {
  27. name: 'FreeWrite',
  28. props: {
  29. width: {
  30. type: Number,
  31. default: 308,
  32. },
  33. height: {
  34. type: Number,
  35. default: 308,
  36. },
  37. lineWidth: {
  38. type: Number,
  39. default: 4,
  40. },
  41. lineColor: {
  42. type: String,
  43. default: '#000000',
  44. },
  45. bgColor: {
  46. type: String,
  47. default: '',
  48. },
  49. isCrop: {
  50. type: Boolean,
  51. default: false,
  52. },
  53. pinyin: {
  54. type: String,
  55. default: '',
  56. },
  57. canvasId: {
  58. type: String,
  59. default: '',
  60. },
  61. mark: {
  62. type: String,
  63. default: '',
  64. },
  65. answerIndex: {
  66. type: Number,
  67. default: -1,
  68. },
  69. userAnswerList: {
  70. type: Array,
  71. default: () => [],
  72. },
  73. disabled: {
  74. type: Boolean,
  75. default: false,
  76. },
  77. },
  78. data() {
  79. return {
  80. ctx: null,
  81. isWriting: false,
  82. lastLoc: {
  83. x: 0,
  84. y: 0
  85. },
  86. currenHzData: {
  87. strokes_image_url: '',
  88. history: [], // 存储路径坐标
  89. },
  90. hasPlay: false
  91. };
  92. },
  93. watch: {
  94. userAnswerList: {
  95. handler(val, oldVal) {
  96. this.setUserAnswer();
  97. },
  98. immediate: true,
  99. deep: true,
  100. },
  101. },
  102. created() {
  103. this.ctx = uni.createCanvasContext(this.canvasId, this);
  104. this.ctx.setLineWidth(4);
  105. this.ctx.setStrokeStyle('#000');
  106. this.ctx.lineCap = 'round'; //表示线条的末端以圆形结束
  107. this.ctx.lineJoin = 'round'; //表示线条的相交处为圆弧
  108. },
  109. methods: {
  110. handleTouchStart(e) {
  111. if (this.disabled) return;
  112. this.isWriting = true;
  113. this.lastLoc = {
  114. x: Math.round(e.mp.touches[0].x),
  115. y: Math.round(e.mp.touches[0].y)
  116. };
  117. e.preventDefault();
  118. },
  119. handleTouchMove(e) {
  120. if (this.disabled) return;
  121. if (this.isWriting) {
  122. let curLoc = {
  123. x: Math.round(e.mp.touches[0].x),
  124. y: Math.round(e.mp.touches[0].y)
  125. }; // 获得当前坐标
  126. this.draw(this.ctx, this.lastLoc, curLoc);
  127. this.currenHzData.history.push([this.lastLoc, curLoc]);
  128. this.lastLoc = Object.assign({}, this.lastLoc, curLoc);
  129. }
  130. e.preventDefault();
  131. },
  132. handleTouchEnd(e) {
  133. this.isWriting = false;
  134. e.preventDefault();
  135. },
  136. draw(context, lastLoc, curLoc) {
  137. if (context) {
  138. context.beginPath();
  139. context.moveTo(lastLoc.x, lastLoc.y);
  140. context.lineTo(curLoc.x, curLoc.y);
  141. context.stroke();
  142. context.draw(true);
  143. }
  144. },
  145. //播放笔画
  146. play() {
  147. let that = this;
  148. if (this.currenHzData && this.currenHzData.history) {
  149. this.ctx.clearRect(0, 0, 320, 320);
  150. let history = null;
  151. history = that.currenHzData.history;
  152. const len = history.length;
  153. let i = 0;
  154. const runner = () => {
  155. i += 1;
  156. if (i < len) {
  157. that.draw(that.ctx, history[i][0], history[i][1]);
  158. requestAnimationFrame(runner);
  159. } else {
  160. that.hasPlay = false;
  161. }
  162. };
  163. requestAnimationFrame(runner);
  164. }
  165. },
  166. //重写
  167. resetHanzi() {
  168. if (this.disabled) return;
  169. this.ctx.clearRect(0, 0, 300, 300);
  170. // 绘制完成
  171. this.ctx.draw();
  172. this.currenHzData.strokes_image_url = '';
  173. this.currenHzData.history = [];
  174. this.$emit("saveStrock", this.mark, null, '', this.answerIndex);
  175. },
  176. //存储文字图片路径
  177. saveImage() {
  178. if (this.disabled) return;
  179. var that = this;
  180. this.ctx.draw(true, () => {
  181. if (that.currenHzData.history.length) {
  182. uni.canvasToTempFilePath({
  183. canvasId: this.canvasId,
  184. success(res) {
  185. //tempFilePath 路径
  186. that.currenHzData.strokes_image_url = res.tempFilePath;
  187. that.$emit("saveStrock", that.mark, that.currenHzData.history, that.currenHzData
  188. .strokes_image_url, that.answerIndex);
  189. }
  190. }, this);
  191. }
  192. });
  193. },
  194. setUserAnswer() {
  195. var curAns = this.userAnswerList.find(p => p.mark == this.mark);
  196. if (!curAns) return;
  197. var ans_index_en = curAns.strokes_content_list[this.answerIndex];
  198. if (!ans_index_en) return;
  199. ans_index_en = JSON.parse(ans_index_en);
  200. this.ctx = uni.createCanvasContext(this.canvasId, this);
  201. this.ctx.setLineWidth(4);
  202. this.ctx.setStrokeStyle('#000');
  203. this.ctx.lineCap = 'round'; //表示线条的末端以圆形结束
  204. this.ctx.lineJoin = 'round'; //表示线条的相交处为圆弧
  205. this.currenHzData.history = JSON.parse(ans_index_en.strokes_content);
  206. this.play();
  207. },
  208. }
  209. };
  210. </script>
  211. <style lang="scss" scoped>
  212. .container {
  213. width: 308px;
  214. height: 760rpx;
  215. margin: 0px auto 24rpx auto;
  216. display: flex;
  217. flex-direction: column;
  218. align-items: center;
  219. row-gap: 24rpx;
  220. .pinyin {
  221. font-size: 32px;
  222. }
  223. .canvas-box {
  224. position: relative;
  225. border: 4px solid #E81B1B;
  226. .character-target-bg {
  227. color: #DEDEDE;
  228. }
  229. .canvas {
  230. width: 100%;
  231. height: 100%;
  232. position: absolute;
  233. top: 0;
  234. left: 0;
  235. }
  236. }
  237. .btn-list {
  238. width: 100%;
  239. display: flex;
  240. justify-content: space-evenly;
  241. column-gap: 30rpx;
  242. .btn-box {
  243. display: flex;
  244. justify-content: center;
  245. align-items: center;
  246. column-gap: 16rpx;
  247. .btn-text {
  248. margin-top: -2rpx;
  249. font-size: 32rpx;
  250. }
  251. }
  252. }
  253. }
  254. </style>