l-signature.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. <template>
  2. <view class="lime-signature" v-if="show" :style="[canvasStyle, styles]" ref="limeSignature">
  3. <!-- #ifndef APP-VUE || APP-NVUE -->
  4. <canvas
  5. v-if="useCanvas2d"
  6. class="lime-signature__canvas"
  7. :id="canvasId"
  8. type="2d"
  9. :disableScroll="disableScroll"
  10. @touchstart="touchStart"
  11. @touchmove="touchMove"
  12. @touchend="touchEnd"
  13. ></canvas>
  14. <canvas
  15. v-else
  16. :disableScroll="disableScroll"
  17. class="lime-signature__canvas"
  18. :canvas-id="canvasId"
  19. :id="canvasId"
  20. :width="canvasWidth"
  21. :height="canvasHeight"
  22. @touchstart="touchStart"
  23. @touchmove="touchMove"
  24. @touchend="touchEnd"
  25. @mousedown="touchStart"
  26. @mousemove="touchMove"
  27. @mouseup="touchEnd"
  28. ></canvas>
  29. <canvas
  30. class="offscreen"
  31. canvas-id="offscreen"
  32. id="offscreen"
  33. :style="'width:' + offscreenSize[0] + 'px;height:' + offscreenSize[1] + 'px'"
  34. :width="offscreenSize[0]"
  35. :height="offscreenSize[1]">
  36. </canvas>
  37. <!-- #endif -->
  38. <!-- #ifdef APP-VUE -->
  39. <view
  40. :id="canvasId"
  41. :disableScroll="disableScroll"
  42. :rparam="param"
  43. :change:rparam="sign.update"
  44. :rclear="rclear"
  45. :change:rclear="sign.clear"
  46. :rundo="rundo"
  47. :change:rundo="sign.undo"
  48. :rsave="rsave"
  49. :change:rsave="sign.save"
  50. :rempty="rempty"
  51. :change:rempty="sign.isEmpty"
  52. ></view>
  53. <!-- #endif -->
  54. <!-- #ifdef APP-NVUE -->
  55. <web-view
  56. src="/uni_modules/lime-signature/hybrid/html/index.html"
  57. class="lime-signature__canvas"
  58. ref="webview"
  59. @pagefinish="onPageFinish"
  60. @error="onError"
  61. @onPostMessage="onMessage"
  62. ></web-view>
  63. <!-- #endif -->
  64. </view>
  65. </template>
  66. <!-- #ifdef APP-VUE -->
  67. <script module="sign" lang="renderjs">
  68. export {default} from './render'
  69. </script>
  70. <!-- #endif -->
  71. <script>
  72. // #ifndef APP-NVUE
  73. import {canIUseCanvas2d, wrapEvent, requestAnimationFrame, sleep} from './utils'
  74. import {Signature} from './signature'
  75. // import {Signature} from '@signature';
  76. import {uniContext, createImage, toDataURL} from './context'
  77. // #endif
  78. import props from './props';
  79. import {base64ToPath, getRect} from './utils'
  80. export default {
  81. props,
  82. data() {
  83. return {
  84. canvasWidth: null,
  85. canvasHeight: null,
  86. useCanvas2d: true,
  87. show: true,
  88. offscreenStyles: '',
  89. // #ifdef APP-PLUS
  90. rclear: 0,
  91. rundo: 0,
  92. rsave: 0,
  93. rempty: 0,
  94. risEmpty: true,
  95. toDataURL: null,
  96. tempFilePath: [],
  97. // #endif
  98. }
  99. },
  100. computed: {
  101. canvasId() {
  102. return `lime-signature${this._uid||this._.uid}`
  103. },
  104. offscreenId() {
  105. return this.canvasId + 'offscreen'
  106. },
  107. offscreenSize() {
  108. const {canvasWidth, canvasHeight} = this
  109. return this.landscape ? [canvasHeight, canvasWidth] : [canvasWidth, canvasHeight]
  110. },
  111. canvasStyle() {
  112. const {canvasWidth, canvasHeight, backgroundColor} = this
  113. //canvasWidth && (canvasWidth + 'px')
  114. return {
  115. width: "100%",
  116. height: canvasHeight && (canvasHeight + 'px'),
  117. background: backgroundColor
  118. }
  119. },
  120. param() {
  121. const {penColor, penSize, backgroundColor, landscape, openSmooth, minLineWidth, maxLineWidth, minSpeed, maxWidthDiffRate, maxHistoryLength, disableScroll} = this
  122. return JSON.parse(JSON.stringify({penColor, penSize, backgroundColor, landscape, openSmooth, minLineWidth, maxLineWidth, minSpeed, maxWidthDiffRate, maxHistoryLength, disableScroll}))
  123. }
  124. },
  125. // #ifdef APP-NVUE
  126. watch: {
  127. param(v) {
  128. this.$refs.webview.evalJS(`update(${JSON.stringify(v)})`)
  129. }
  130. },
  131. // #endif
  132. // #ifndef APP-PLUS
  133. created() {
  134. this.useCanvas2d = this.type == '2d' && canIUseCanvas2d()
  135. },
  136. // #endif
  137. // #ifndef APP-PLUS
  138. async mounted() {
  139. if(this.beforeDelay) {
  140. await sleep(this.beforeDelay)
  141. }
  142. const config = await this.getContext()
  143. this.signature = new Signature(config)
  144. this.canvasEl = this.signature.canvas.get('el')
  145. this.canvasWidth = this.signature.canvas.get('width')
  146. this.canvasHeight = this.signature.canvas.get('height')
  147. this.stopWatch = this.$watch('param' , (v) => {
  148. this.signature.pen.setOption(v)
  149. }, {immediate: true})
  150. },
  151. // #endif
  152. // #ifndef APP-PLUS
  153. // #ifdef VUE3
  154. beforeUnmount() {
  155. this.stopWatch && this.stopWatch()
  156. this.signature.destroy()
  157. this.signature = null
  158. this.show = false;
  159. },
  160. // #endif
  161. // #ifdef VUE2
  162. beforeDestroy() {
  163. this.stopWatch && this.stopWatch()
  164. this.signature.destroy()
  165. this.show = false;
  166. this.signature = null
  167. },
  168. // #endif
  169. // #endif
  170. methods: {
  171. // #ifdef MP-QQ
  172. // toJSON() { return this },
  173. // #endif
  174. // #ifdef APP-PLUS
  175. onPageFinish() {
  176. this.$refs.webview.evalJS(`update(${JSON.stringify(this.param)})`)
  177. },
  178. onMessage(e = {}) {
  179. const {detail: {data: [res]}} = e
  180. if(res.event?.save) {
  181. this.toDataURL = res.event.save
  182. }
  183. if(res.event?.changeSize) {
  184. const {width, height} = res.event.changeSize
  185. }
  186. if(res.event.hasOwnProperty('isEmpty')) {
  187. this.risEmpty = res.event.isEmpty
  188. }
  189. if (res.event?.file) {
  190. this.tempFilePath.push(res.event.file)
  191. if (this.tempFilePath.length > 7) {
  192. this.tempFilePath.shift()
  193. }
  194. return
  195. }
  196. if (res.event?.success) {
  197. if (res.event.success) {
  198. this.tempFilePath.push(res.event.success)
  199. if (this.tempFilePath.length > 8) {
  200. this.tempFilePath.shift()
  201. }
  202. this.toDataURL = this.tempFilePath.join('')
  203. this.tempFilePath = []
  204. } else {
  205. this.$emit('fail', 'canvas no data')
  206. }
  207. return
  208. }
  209. },
  210. // #endif
  211. undo() {
  212. // #ifdef APP-VUE || APP-NVUE
  213. this.rundo += 1
  214. // #endif
  215. // #ifdef APP-NVUE
  216. this.$refs.webview.evalJS(`undo()`)
  217. // #endif
  218. // #ifndef APP-VUE
  219. if(this.signature)
  220. this.signature.undo()
  221. // #endif
  222. },
  223. clear() {
  224. // #ifdef APP-VUE || APP-NVUE
  225. this.rclear += 1
  226. // #endif
  227. // #ifdef APP-NVUE
  228. this.$refs.webview.evalJS(`clear()`)
  229. // #endif
  230. // #ifndef APP-VUE
  231. if(this.signature)
  232. this.signature.clear()
  233. // #endif
  234. },
  235. isEmpty() {
  236. // #ifdef APP-NVUE
  237. this.$refs.webview.evalJS(`isEmpty()`)
  238. // #endif
  239. // #ifdef APP-VUE || APP-NVUE
  240. this.rempty += 1
  241. // #endif
  242. // #ifndef APP-VUE || APP-NVUE
  243. return this.signature.isEmpty()
  244. // #endif
  245. },
  246. canvasToTempFilePath(param) {
  247. const isEmpty = this.isEmpty()
  248. // #ifdef APP-NVUE
  249. this.$refs.webview.evalJS(`save()`)
  250. // #endif
  251. // #ifdef APP-VUE || APP-NVUE
  252. const stopURLWatch = this.$watch('toDataURL', (v, n) => {
  253. if(v && v !== n) {
  254. // if(param.pathType == 'url') {
  255. base64ToPath(v).then(res => {
  256. param.success({tempFilePath: res,isEmpty: this.risEmpty })
  257. })
  258. // } else {
  259. // param.success({tempFilePath: v,isEmpty: this.risEmpty })
  260. // }
  261. this.toDataURL = ''
  262. }
  263. stopURLWatch && stopURLWatch()
  264. })
  265. this.rsave += 1
  266. // #endif
  267. // #ifndef APP-VUE || APP-NVUE
  268. const success = (success) => param.success && param.success(success)
  269. const fail = (fail) => param.fail && param.fail(err)
  270. const {canvas} = this.signature.canvas.get('el')
  271. const {backgroundColor, landscape} = this
  272. const width = this.signature.canvas.get('width')
  273. const height = this.signature.canvas.get('height')
  274. const canvasToTempFilePath = (image) => {
  275. const context = uni.createCanvasContext('offscreen', this)
  276. context.save()
  277. context.setTransform(1,0,0,1,0,0)
  278. if(landscape) {
  279. context.translate(0, width)
  280. context.rotate(-Math.PI / 2)
  281. }
  282. if(backgroundColor) {
  283. context.fillStyle = backgroundColor
  284. context.fillRect(0,0, width, height)
  285. }
  286. context.drawImage(image, 0, 0, width, height);
  287. context.draw(false, () => {
  288. toDataURL('offscreen', this, param).then((res) => {
  289. const size = Math.max(width, height)
  290. context.restore()
  291. context.clearRect(0,0, size, size)
  292. success({tempFilePath: res, isEmpty})
  293. })
  294. })
  295. }
  296. if(this.useCanvas2d) {
  297. try{
  298. // #ifndef MP-ALIPAY
  299. base64ToPath(canvas.toDataURL()).then(canvasToTempFilePath)
  300. // #endif
  301. // #ifdef MP-ALIPAY
  302. canvas.toTempFilePath({
  303. canvasid: this.canvasid,
  304. success(res){
  305. canvasToTempFilePath(res.tempFilePath)
  306. },
  307. fail
  308. })
  309. // #endif
  310. } catch(err){fail(err)}
  311. } else {
  312. toDataURL(this.canvasId, this).then(canvasToTempFilePath).catch(fail)
  313. }
  314. // #endif
  315. },
  316. // #ifndef APP-PLUS
  317. getContext() {
  318. return getRect(`#${this.canvasId}`, {context: this, type: this.useCanvas2d ? 'fields': 'boundingClientRect'}).then(res => {
  319. if(res) {
  320. let {width, height, node: canvas, left, top, right} = res
  321. let {pixelRatio} = uni.getSystemInfoSync()
  322. let context;
  323. if(canvas) {
  324. context = canvas.getContext('2d')
  325. canvas.width = width * pixelRatio;
  326. canvas.height = height * pixelRatio;
  327. } else {
  328. pixelRatio = 1
  329. context = uniContext(uni.createCanvasContext(this.canvasId, this))
  330. canvas = {
  331. createImage,
  332. toDataURL: () => toDataURL(this.canvasId, this),
  333. requestAnimationFrame
  334. }
  335. }
  336. // 支付宝小程序 使用stroke有个默认背景色
  337. context.clearRect(0,0,width,height)
  338. return { left, top, right, width, height, context, canvas, pixelRatio};
  339. }
  340. })
  341. },
  342. touchStart(e) {
  343. if(!this.canvasEl) return
  344. this.isStart = true
  345. this.canvasEl.dispatchEvent('touchstart', wrapEvent(e))
  346. },
  347. touchMove(e) {
  348. if(!this.canvasEl || !this.isStart && this.canvasEl) return
  349. this.canvasEl.dispatchEvent('touchmove', wrapEvent(e))
  350. },
  351. touchEnd(e) {
  352. if(!this.canvasEl) return
  353. this.isStart = false
  354. this.canvasEl.dispatchEvent('touchend', wrapEvent(e))
  355. },
  356. // #endif
  357. }
  358. }
  359. </script>
  360. <style lang="scss">
  361. .lime-signature,.lime-signature__canvas {
  362. /* #ifndef APP-NVUE */
  363. width: 100%;
  364. height: 100%;
  365. /* #endif */
  366. /* #ifdef APP-NVUE */
  367. flex: 1;
  368. /* #endif */
  369. }
  370. .offscreen {
  371. position: fixed;
  372. top: 0;
  373. left: 1500rpx;
  374. }
  375. </style>