l-signature.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. <template>
  2. <view class="lime-signature" :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. @touchstart="touchStart"
  21. @touchmove="touchMove"
  22. @touchend="touchEnd"
  23. @mousedown="touchStart"
  24. @mousemove="touchMove"
  25. @mouseup="touchEnd"
  26. ></canvas>
  27. <!-- #endif -->
  28. <!-- #ifdef APP-VUE -->
  29. <view
  30. :id="canvasId"
  31. :disableScroll="disableScroll"
  32. :rparam="param"
  33. :change:rparam="sign.update"
  34. :rclear="rclear"
  35. :change:rclear="sign.clear"
  36. :rundo="rundo"
  37. :change:rundo="sign.undo"
  38. :rsave="rsave"
  39. :change:rsave="sign.save"
  40. :rempty="rempty"
  41. :change:rempty="sign.isEmpty"
  42. ></view>
  43. <!-- #endif -->
  44. <!-- #ifdef APP-NVUE -->
  45. <web-view
  46. src="/uni_modules/lime-signature/static/index.html"
  47. class="lime-signature__canvas"
  48. ref="webview"
  49. @pagefinish="onPageFinish"
  50. @error="onError"
  51. @onPostMessage="onMessage"
  52. ></web-view>
  53. <!-- #endif -->
  54. </view>
  55. </template>
  56. <!-- #ifdef APP-VUE -->
  57. <script module="sign" lang="renderjs">
  58. // #ifdef APP-VUE
  59. // import { Signature } from '@signature'
  60. import { Signature } from './signature'
  61. // import {base64ToPath} from './utils'
  62. export default {
  63. data() {
  64. return {
  65. canvasid: null,
  66. signature: null,
  67. observer: null,
  68. options: {},
  69. saveCount: 0,
  70. }
  71. },
  72. mounted() {
  73. this.$nextTick(this.init)
  74. },
  75. methods: {
  76. init() {
  77. const el = this.$refs.limeSignature;
  78. const canvas = document.createElement('canvas')
  79. canvas.style = 'width:100%; height: 100%;'
  80. el.appendChild(canvas)
  81. this.signature = new Signature({el: canvas})
  82. this.signature.pen.setOption(this.options)
  83. const width = this.signature.canvas.get('width')
  84. const height = this.signature.canvas.get('height')
  85. this.emit({
  86. changeSize: {width, height}
  87. })
  88. },
  89. undo(v) {
  90. if(v && this.signature) {
  91. this.signature.undo()
  92. }
  93. },
  94. clear(v) {
  95. if(v && this.signature) {
  96. this.signature.clear()
  97. }
  98. },
  99. save(v) {
  100. if(v !== this.saveCount) {
  101. this.saveCount = v;
  102. const image = this.signature.canvas.get('el').toDataURL()
  103. this.emit({save: image})
  104. // base64ToPath(image).then((res) => {
  105. // this.emit({save: res})
  106. // })
  107. }
  108. },
  109. isEmpty(v) {
  110. if(v && this.signature) {
  111. const isEmpty = this.signature.isEmpty()
  112. this.emit({isEmpty})
  113. }
  114. },
  115. emit(event) {
  116. this.$ownerInstance.callMethod('onMessage', {
  117. detail: {
  118. data: [
  119. {
  120. event
  121. }
  122. ]
  123. }
  124. })
  125. },
  126. update(v) {
  127. if(v) {
  128. if(this.signature) {
  129. this.options = v
  130. this.signature.pen.setOption(v)
  131. } else {
  132. this.options = v
  133. }
  134. }
  135. }
  136. }
  137. }
  138. // #endif
  139. </script>
  140. <!-- #endif -->
  141. <script>
  142. // #ifndef APP-NVUE
  143. import {getCanvas2d, wrapEvent, requestAnimationFrame, sleep} from './utils'
  144. import {Signature} from './signature'
  145. // import {Signature} from '@signature';
  146. import {uniContext, createImage, toDataURL} from './context'
  147. // #endif
  148. import {base64ToPath} from './utils'
  149. export default {
  150. props: {
  151. styles: String,
  152. disableScroll: Boolean,
  153. type: {
  154. type: String,
  155. default: '2d'
  156. },
  157. // 画笔颜色
  158. penColor: {
  159. type: String,
  160. default: 'black'
  161. },
  162. penSize: {
  163. type: Number,
  164. default: 2
  165. },
  166. // 画板背景颜色 未实现
  167. backgroundColor: String,
  168. // 笔锋
  169. openSmooth: Boolean,
  170. // 画笔最小值
  171. minLineWidth: {
  172. type: Number,
  173. default: 2
  174. },
  175. // 画笔最大值
  176. maxLineWidth: {
  177. type: Number,
  178. default: 6
  179. },
  180. // 画笔达到最小宽度所需最小速度(px/ms),取值范围1.0-10.0,值越小,画笔越容易变细,笔锋效果会比较明显,可以自行调整查看效果,选出自己满意的值。
  181. minSpeed: {
  182. type: Number,
  183. default: 1.5
  184. },
  185. // 相邻两线宽度增(减)量最大百分比,取值范围1-100,为了达到笔锋效果,画笔宽度会随画笔速度而改变,如果相邻两线宽度差太大,过渡效果就会很突兀,使用maxWidthDiffRate限制宽度差,让过渡效果更自然。可以自行调整查看效果,选出自己满意的值。
  186. maxWidthDiffRate: {
  187. type: Number,
  188. default: 20
  189. },
  190. // 限制历史记录数,即最大可撤销数,传入0则关闭历史记录功能
  191. maxHistoryLength: {
  192. type: Number,
  193. default: 20
  194. },
  195. beforeDelay: {
  196. type: Number,
  197. default: 0
  198. }
  199. },
  200. data() {
  201. return {
  202. canvasWidth: null,
  203. canvasHeight: null,
  204. useCanvas2d: true,
  205. // #ifdef APP-PLUS
  206. rclear: 0,
  207. rundo: 0,
  208. rsave: 0,
  209. rempty: 0,
  210. risEmpty: true,
  211. toDataURL: null,
  212. tempFilePath: [],
  213. // #endif
  214. }
  215. },
  216. computed: {
  217. canvasId() {
  218. return `lime-signature${this._uid||this._.uid}`
  219. },
  220. canvasStyle() {
  221. const {canvasWidth, canvasHeight} = this
  222. return {
  223. width: canvasWidth && (canvasWidth + 'px'),
  224. height: canvasHeight && (canvasHeight + 'px'),
  225. }
  226. },
  227. param() {
  228. const {penColor, penSize, backgroundColor, openSmooth, minLineWidth, maxLineWidth, minSpeed, maxWidthDiffRate, maxHistoryLength, disableScroll} = this
  229. return JSON.parse(JSON.stringify({penColor, penSize, backgroundColor, openSmooth, minLineWidth, maxLineWidth, minSpeed, maxWidthDiffRate, maxHistoryLength, disableScroll}))
  230. }
  231. },
  232. // #ifdef APP-NVUE
  233. watch: {
  234. param(v) {
  235. this.$refs.webview.evalJS(`update(${JSON.stringify(v)})`)
  236. }
  237. },
  238. // #endif
  239. // #ifndef APP-PLUS
  240. created() {
  241. this.useCanvas2d = this.type=== '2d' && getCanvas2d()
  242. },
  243. // #endif
  244. // #ifndef APP-PLUS
  245. async mounted() {
  246. if(this.beforeDelay) {
  247. await sleep(this.beforeDelay)
  248. }
  249. const config = await this.getContext()
  250. this.signature = new Signature(config)
  251. this.canvasEl = this.signature.canvas.get('el')
  252. this.canvasWidth = this.signature.canvas.get('width')
  253. this.canvasHeight = this.signature.canvas.get('height')
  254. this.stopWatch = this.$watch('param' , (v) => {
  255. this.signature.pen.setOption(v)
  256. }, {immediate: true})
  257. },
  258. // #endif
  259. // #ifndef APP-PLUS
  260. // #ifdef VUE3
  261. beforeUnmount() {
  262. this.stopWatch()
  263. this.signature.destroy()
  264. },
  265. // #endif
  266. // #ifdef VUE2
  267. beforeDestroy() {
  268. this.stopWatch()
  269. this.signature.destroy()
  270. },
  271. // #endif
  272. // #endif
  273. methods: {
  274. // #ifdef APP-PLUS
  275. onPageFinish() {
  276. this.$refs.webview.evalJS(`update(${JSON.stringify(this.param)})`)
  277. },
  278. onMessage(e = {}) {
  279. const {detail: {data: [res]}} = e
  280. if(res.event?.save) {
  281. this.toDataURL = res.event.save
  282. }
  283. if(res.event?.changeSize) {
  284. const {width, height} = res.event.changeSize
  285. }
  286. if(res.event.hasOwnProperty('isEmpty')) {
  287. this.risEmpty = res.event.isEmpty
  288. }
  289. if (res.event?.file) {
  290. this.tempFilePath.push(res.event.file)
  291. if (this.tempFilePath.length > 7) {
  292. this.tempFilePath.shift()
  293. }
  294. return
  295. }
  296. if (res.event?.success) {
  297. if (res.event.success) {
  298. this.tempFilePath.push(res.event.success)
  299. if (this.tempFilePath.length > 8) {
  300. this.tempFilePath.shift()
  301. }
  302. this.toDataURL = this.tempFilePath.join('')
  303. this.tempFilePath = []
  304. // base64ToPath(this.tempFilePath.join('')).then(res => {
  305. // })
  306. } else {
  307. this.$emit('fail', 'canvas no data')
  308. }
  309. return
  310. }
  311. },
  312. // #endif
  313. undo() {
  314. // #ifdef APP-VUE || APP-NVUE
  315. this.rundo += 1
  316. // #endif
  317. // #ifdef APP-NVUE
  318. this.$refs.webview.evalJS(`undo()`)
  319. // #endif
  320. // #ifndef APP-VUE
  321. if(this.signature)
  322. this.signature.undo()
  323. // #endif
  324. },
  325. clear() {
  326. // #ifdef APP-VUE || APP-NVUE
  327. this.rclear += 1
  328. // #endif
  329. // #ifdef APP-NVUE
  330. this.$refs.webview.evalJS(`clear()`)
  331. // #endif
  332. // #ifndef APP-VUE
  333. if(this.signature)
  334. this.signature.clear()
  335. // #endif
  336. },
  337. isEmpty() {
  338. // #ifdef APP-NVUE
  339. this.$refs.webview.evalJS(`isEmpty()`)
  340. // #endif
  341. // #ifdef APP-VUE || APP-NVUE
  342. this.rempty += 1
  343. // #endif
  344. // #ifndef APP-VUE || APP-NVUE
  345. return this.signature.isEmpty()
  346. // #endif
  347. },
  348. canvasToTempFilePath(param) {
  349. const isEmpty = this.isEmpty()
  350. // #ifdef APP-NVUE
  351. this.$refs.webview.evalJS(`save()`)
  352. // #endif
  353. // #ifdef APP-VUE || APP-NVUE
  354. const stopURLWatch = this.$watch('toDataURL', (v, n) => {
  355. if(v && v !== n) {
  356. if(param.pathType == 'url') {
  357. base64ToPath(v).then(res => {
  358. param.success({tempFilePath: res,isEmpty: this.risEmpty })
  359. })
  360. } else {
  361. param.success({tempFilePath: v,isEmpty: this.risEmpty })
  362. }
  363. }
  364. stopURLWatch && stopURLWatch()
  365. })
  366. this.rsave += 1
  367. // #endif
  368. // #ifndef APP-VUE || APP-NVUE
  369. const success = (success) => param.success && param.success(success)
  370. const fail = (fail) => param.fail && param.fail(err)
  371. if(this.useCanvas2d) {
  372. try{
  373. // #ifndef MP-ALIPAY
  374. const {canvas} = this.signature.canvas.get('el')
  375. const tempFilePath = canvas.toDataURL()
  376. success({tempFilePath, isEmpty})
  377. // #endif
  378. // #ifdef MP-ALIPAY
  379. canvas.toTempFilePath({
  380. canvasid: this.canvasid,
  381. success(res){success({tempFilePath: res, isEmpty})},
  382. fail
  383. })
  384. // #endif
  385. }catch(err){
  386. console.warn(err)
  387. fail(err)
  388. }
  389. } else {
  390. toDataURL(this.canvasId, this).then(res => {
  391. success({tempFilePath: res, isEmpty})
  392. }).catch(err => {
  393. console.warn(err)
  394. fail(err)
  395. })
  396. }
  397. // #endif
  398. },
  399. // #ifndef APP-PLUS
  400. getContext() {
  401. const {pixelRatio} = uni.getSystemInfoSync()
  402. return new Promise(resolve => {
  403. if(this.useCanvas2d) {
  404. uni.createSelectorQuery().in(this)
  405. .select(`#${this.canvasId}`)
  406. .fields({
  407. node: true,
  408. size: true,
  409. rect: true,
  410. })
  411. .exec(res => {
  412. if(res) {
  413. const {width, height, node, left, top, right} = res[0]
  414. const context = node.getContext('2d')
  415. node.width = width * pixelRatio;
  416. node.height = height * pixelRatio;
  417. resolve({ left, top, right, width, height, context, canvas: node, pixelRatio})
  418. }
  419. })
  420. } else {
  421. uni.createSelectorQuery().in(this)
  422. .select(`#${this.canvasId}`)
  423. .boundingClientRect()
  424. .exec(res => {
  425. if(res) {
  426. const {width, height, left, top, right} = res[0]
  427. const context = uniContext(uni.createCanvasContext(this.canvasId, this))
  428. const canvas = {
  429. createImage,
  430. toDataURL: () => toDataURL(this.canvasId, this),
  431. requestAnimationFrame
  432. }
  433. resolve({ left, top, right, width, height, context, pixelRatio:1, canvas})
  434. }
  435. })
  436. }
  437. })
  438. },
  439. touchStart(e) {
  440. if(!this.canvasEl) return
  441. this.isStart = true
  442. this.canvasEl.dispatchEvent('touchstart', wrapEvent(e))
  443. },
  444. touchMove(e) {
  445. if(!this.canvasEl || !this.isStart && this.canvasEl) return
  446. this.canvasEl.dispatchEvent('touchmove', wrapEvent(e))
  447. },
  448. touchEnd(e) {
  449. if(!this.canvasEl) return
  450. this.isStart = false
  451. this.canvasEl.dispatchEvent('touchend', wrapEvent(e))
  452. },
  453. // #endif
  454. }
  455. }
  456. </script>
  457. <style lang="stylus">
  458. .lime-signature,.lime-signature__canvas
  459. // #ifndef APP-NVUE
  460. width: 100%;
  461. height: 100%
  462. // #endif
  463. // #ifdef APP-NVUE
  464. flex: 1;
  465. // #endif
  466. </style>