l-signature.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  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. return {
  114. width: canvasWidth && (canvasWidth + 'px'),
  115. height: canvasHeight && (canvasHeight + 'px'),
  116. background: backgroundColor
  117. }
  118. },
  119. param() {
  120. const {penColor, penSize, backgroundColor, landscape, openSmooth, minLineWidth, maxLineWidth, minSpeed, maxWidthDiffRate, maxHistoryLength, disableScroll} = this
  121. return JSON.parse(JSON.stringify({penColor, penSize, backgroundColor, landscape, openSmooth, minLineWidth, maxLineWidth, minSpeed, maxWidthDiffRate, maxHistoryLength, disableScroll}))
  122. }
  123. },
  124. // #ifdef APP-NVUE
  125. watch: {
  126. param(v) {
  127. this.$refs.webview.evalJS(`update(${JSON.stringify(v)})`)
  128. }
  129. },
  130. // #endif
  131. // #ifndef APP-PLUS
  132. created() {
  133. this.useCanvas2d = this.type == '2d' && canIUseCanvas2d()
  134. },
  135. // #endif
  136. // #ifndef APP-PLUS
  137. async mounted() {
  138. if(this.beforeDelay) {
  139. await sleep(this.beforeDelay)
  140. }
  141. const config = await this.getContext()
  142. this.signature = new Signature(config)
  143. this.canvasEl = this.signature.canvas.get('el')
  144. this.canvasWidth = this.signature.canvas.get('width')
  145. this.canvasHeight = this.signature.canvas.get('height')
  146. this.stopWatch = this.$watch('param' , (v) => {
  147. this.signature.pen.setOption(v)
  148. }, {immediate: true})
  149. },
  150. // #endif
  151. // #ifndef APP-PLUS
  152. // #ifdef VUE3
  153. beforeUnmount() {
  154. this.stopWatch && this.stopWatch()
  155. this.signature.destroy()
  156. this.signature = null
  157. this.show = false;
  158. },
  159. // #endif
  160. // #ifdef VUE2
  161. beforeDestroy() {
  162. this.stopWatch && this.stopWatch()
  163. this.signature.destroy()
  164. this.show = false;
  165. this.signature = null
  166. },
  167. // #endif
  168. // #endif
  169. methods: {
  170. // #ifdef MP-QQ
  171. // toJSON() { return this },
  172. // #endif
  173. // #ifdef APP-PLUS
  174. onPageFinish() {
  175. this.$refs.webview.evalJS(`update(${JSON.stringify(this.param)})`)
  176. },
  177. onMessage(e = {}) {
  178. const {detail: {data: [res]}} = e
  179. if(res.event?.save) {
  180. this.toDataURL = res.event.save
  181. }
  182. if(res.event?.changeSize) {
  183. const {width, height} = res.event.changeSize
  184. }
  185. if(res.event.hasOwnProperty('isEmpty')) {
  186. this.risEmpty = res.event.isEmpty
  187. }
  188. if (res.event?.file) {
  189. this.tempFilePath.push(res.event.file)
  190. if (this.tempFilePath.length > 7) {
  191. this.tempFilePath.shift()
  192. }
  193. return
  194. }
  195. if (res.event?.success) {
  196. if (res.event.success) {
  197. this.tempFilePath.push(res.event.success)
  198. if (this.tempFilePath.length > 8) {
  199. this.tempFilePath.shift()
  200. }
  201. this.toDataURL = this.tempFilePath.join('')
  202. this.tempFilePath = []
  203. } else {
  204. this.$emit('fail', 'canvas no data')
  205. }
  206. return
  207. }
  208. },
  209. // #endif
  210. undo() {
  211. // #ifdef APP-VUE || APP-NVUE
  212. this.rundo += 1
  213. // #endif
  214. // #ifdef APP-NVUE
  215. this.$refs.webview.evalJS(`undo()`)
  216. // #endif
  217. // #ifndef APP-VUE
  218. if(this.signature)
  219. this.signature.undo()
  220. // #endif
  221. },
  222. clear() {
  223. // #ifdef APP-VUE || APP-NVUE
  224. this.rclear += 1
  225. // #endif
  226. // #ifdef APP-NVUE
  227. this.$refs.webview.evalJS(`clear()`)
  228. // #endif
  229. // #ifndef APP-VUE
  230. if(this.signature)
  231. this.signature.clear()
  232. // #endif
  233. },
  234. isEmpty() {
  235. // #ifdef APP-NVUE
  236. this.$refs.webview.evalJS(`isEmpty()`)
  237. // #endif
  238. // #ifdef APP-VUE || APP-NVUE
  239. this.rempty += 1
  240. // #endif
  241. // #ifndef APP-VUE || APP-NVUE
  242. return this.signature.isEmpty()
  243. // #endif
  244. },
  245. canvasToTempFilePath(param) {
  246. const isEmpty = this.isEmpty()
  247. // #ifdef APP-NVUE
  248. this.$refs.webview.evalJS(`save()`)
  249. // #endif
  250. // #ifdef APP-VUE || APP-NVUE
  251. const stopURLWatch = this.$watch('toDataURL', (v, n) => {
  252. if(v && v !== n) {
  253. // if(param.pathType == 'url') {
  254. base64ToPath(v).then(res => {
  255. param.success({tempFilePath: res,isEmpty: this.risEmpty })
  256. })
  257. // } else {
  258. // param.success({tempFilePath: v,isEmpty: this.risEmpty })
  259. // }
  260. this.toDataURL = ''
  261. }
  262. stopURLWatch && stopURLWatch()
  263. })
  264. this.rsave += 1
  265. // #endif
  266. // #ifndef APP-VUE || APP-NVUE
  267. const success = (success) => param.success && param.success(success)
  268. const fail = (fail) => param.fail && param.fail(err)
  269. const {canvas} = this.signature.canvas.get('el')
  270. const {backgroundColor, landscape} = this
  271. const width = this.signature.canvas.get('width')
  272. const height = this.signature.canvas.get('height')
  273. const canvasToTempFilePath = (image) => {
  274. const context = uni.createCanvasContext('offscreen', this)
  275. context.save()
  276. context.setTransform(1,0,0,1,0,0)
  277. if(landscape) {
  278. context.translate(0, width)
  279. context.rotate(-Math.PI / 2)
  280. }
  281. if(backgroundColor) {
  282. context.fillStyle = backgroundColor
  283. context.fillRect(0,0, width, height)
  284. }
  285. context.drawImage(image, 0, 0, width, height);
  286. context.draw(false, () => {
  287. toDataURL('offscreen', this, param).then((res) => {
  288. const size = Math.max(width, height)
  289. context.restore()
  290. context.clearRect(0,0, size, size)
  291. success({tempFilePath: res, isEmpty})
  292. })
  293. })
  294. }
  295. if(this.useCanvas2d) {
  296. try{
  297. // #ifndef MP-ALIPAY
  298. base64ToPath(canvas.toDataURL()).then(canvasToTempFilePath)
  299. // #endif
  300. // #ifdef MP-ALIPAY
  301. canvas.toTempFilePath({
  302. canvasid: this.canvasid,
  303. success(res){
  304. canvasToTempFilePath(res.tempFilePath)
  305. },
  306. fail
  307. })
  308. // #endif
  309. } catch(err){fail(err)}
  310. } else {
  311. toDataURL(this.canvasId, this).then(canvasToTempFilePath).catch(fail)
  312. }
  313. // #endif
  314. },
  315. // #ifndef APP-PLUS
  316. getContext() {
  317. return getRect(`#${this.canvasId}`, {context: this, type: this.useCanvas2d ? 'fields': 'boundingClientRect'}).then(res => {
  318. if(res) {
  319. let {width, height, node: canvas, left, top, right} = res
  320. let {pixelRatio} = uni.getSystemInfoSync()
  321. let context;
  322. if(canvas) {
  323. context = canvas.getContext('2d')
  324. canvas.width = width * pixelRatio;
  325. canvas.height = height * pixelRatio;
  326. } else {
  327. pixelRatio = 1
  328. context = uniContext(uni.createCanvasContext(this.canvasId, this))
  329. canvas = {
  330. createImage,
  331. toDataURL: () => toDataURL(this.canvasId, this),
  332. requestAnimationFrame
  333. }
  334. }
  335. // 支付宝小程序 使用stroke有个默认背景色
  336. context.clearRect(0,0,width,height)
  337. return { left, top, right, width, height, context, canvas, pixelRatio};
  338. }
  339. })
  340. },
  341. touchStart(e) {
  342. if(!this.canvasEl) return
  343. this.isStart = true
  344. this.canvasEl.dispatchEvent('touchstart', wrapEvent(e))
  345. },
  346. touchMove(e) {
  347. if(!this.canvasEl || !this.isStart && this.canvasEl) return
  348. this.canvasEl.dispatchEvent('touchmove', wrapEvent(e))
  349. },
  350. touchEnd(e) {
  351. if(!this.canvasEl) return
  352. this.isStart = false
  353. this.canvasEl.dispatchEvent('touchend', wrapEvent(e))
  354. },
  355. // #endif
  356. }
  357. }
  358. </script>
  359. <style lang="scss">
  360. .lime-signature,.lime-signature__canvas {
  361. /* #ifndef APP-NVUE */
  362. width: 100%;
  363. height: 100%;
  364. /* #endif */
  365. /* #ifdef APP-NVUE */
  366. flex: 1;
  367. /* #endif */
  368. }
  369. .offscreen {
  370. position: fixed;
  371. top: 0;
  372. left: 1500rpx;
  373. }
  374. </style>