l-signature.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  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. const {backgroundColor } = this.options
  104. if(backgroundColor) {
  105. const canvas = document.createElement('canvas')
  106. const width = this.signature.canvas.get('width')
  107. const height = this.signature.canvas.get('height')
  108. const pixelRatio = this.signature.canvas.get('pixelRatio')
  109. canvas.width = width * pixelRatio
  110. canvas.height = height * pixelRatio
  111. const context = canvas.getContext('2d')
  112. context.scale(pixelRatio, pixelRatio)
  113. context.fillStyle = backgroundColor
  114. context.fillRect(0,0, width, height)
  115. context.drawImage(this.signature.canvas.get('el'), 0, 0, width, height)
  116. this.emit({save: canvas.toDataURL()})
  117. canvas.remove()
  118. } else {
  119. this.emit({save: image})
  120. }
  121. // base64ToPath(image).then((res) => {
  122. // this.emit({save: res})
  123. // })
  124. }
  125. },
  126. isEmpty(v) {
  127. if(v && this.signature) {
  128. const isEmpty = this.signature.isEmpty()
  129. this.emit({isEmpty})
  130. }
  131. },
  132. emit(event) {
  133. this.$ownerInstance.callMethod('onMessage', {
  134. detail: {
  135. data: [
  136. {
  137. event
  138. }
  139. ]
  140. }
  141. })
  142. },
  143. update(v) {
  144. if(v) {
  145. if(this.signature) {
  146. this.options = v
  147. this.signature.pen.setOption(v)
  148. } else {
  149. this.options = v
  150. }
  151. }
  152. }
  153. }
  154. }
  155. // #endif
  156. </script>
  157. <!-- #endif -->
  158. <script>
  159. // #ifndef APP-NVUE
  160. import {getCanvas2d, wrapEvent, requestAnimationFrame, sleep} from './utils'
  161. import {Signature} from './signature'
  162. // import {Signature} from '@signature';
  163. import {uniContext, createImage, toDataURL} from './context'
  164. // #endif
  165. import {base64ToPath} from './utils'
  166. export default {
  167. props: {
  168. styles: String,
  169. disableScroll: Boolean,
  170. type: {
  171. type: String,
  172. default: '2d'
  173. },
  174. // 画笔颜色
  175. penColor: {
  176. type: String,
  177. default: 'black'
  178. },
  179. penSize: {
  180. type: Number,
  181. default: 2
  182. },
  183. // 画板背景颜色
  184. backgroundColor: String,
  185. // 笔锋
  186. openSmooth: Boolean,
  187. // 画笔最小值
  188. minLineWidth: {
  189. type: Number,
  190. default: 2
  191. },
  192. // 画笔最大值
  193. maxLineWidth: {
  194. type: Number,
  195. default: 6
  196. },
  197. // 画笔达到最小宽度所需最小速度(px/ms),取值范围1.0-10.0,值越小,画笔越容易变细,笔锋效果会比较明显,可以自行调整查看效果,选出自己满意的值。
  198. minSpeed: {
  199. type: Number,
  200. default: 1.5
  201. },
  202. // 相邻两线宽度增(减)量最大百分比,取值范围1-100,为了达到笔锋效果,画笔宽度会随画笔速度而改变,如果相邻两线宽度差太大,过渡效果就会很突兀,使用maxWidthDiffRate限制宽度差,让过渡效果更自然。可以自行调整查看效果,选出自己满意的值。
  203. maxWidthDiffRate: {
  204. type: Number,
  205. default: 20
  206. },
  207. // 限制历史记录数,即最大可撤销数,传入0则关闭历史记录功能
  208. maxHistoryLength: {
  209. type: Number,
  210. default: 20
  211. },
  212. beforeDelay: {
  213. type: Number,
  214. default: 0
  215. }
  216. },
  217. data() {
  218. return {
  219. canvasWidth: null,
  220. canvasHeight: null,
  221. useCanvas2d: true,
  222. // #ifdef APP-PLUS
  223. rclear: 0,
  224. rundo: 0,
  225. rsave: 0,
  226. rempty: 0,
  227. risEmpty: true,
  228. toDataURL: null,
  229. tempFilePath: [],
  230. // #endif
  231. }
  232. },
  233. computed: {
  234. canvasId() {
  235. return `lime-signature${this._uid||this._.uid}`
  236. },
  237. canvasStyle() {
  238. const {canvasWidth, canvasHeight, backgroundColor} = this
  239. return {
  240. width: canvasWidth && (canvasWidth + 'px'),
  241. height: canvasHeight && (canvasHeight + 'px'),
  242. background: backgroundColor
  243. }
  244. },
  245. param() {
  246. const {penColor, penSize, backgroundColor, openSmooth, minLineWidth, maxLineWidth, minSpeed, maxWidthDiffRate, maxHistoryLength, disableScroll} = this
  247. return JSON.parse(JSON.stringify({penColor, penSize, backgroundColor, openSmooth, minLineWidth, maxLineWidth, minSpeed, maxWidthDiffRate, maxHistoryLength, disableScroll}))
  248. }
  249. },
  250. // #ifdef APP-NVUE
  251. watch: {
  252. param(v) {
  253. this.$refs.webview.evalJS(`update(${JSON.stringify(v)})`)
  254. }
  255. },
  256. // #endif
  257. // #ifndef APP-PLUS
  258. created() {
  259. this.useCanvas2d = this.type=== '2d' && getCanvas2d()
  260. },
  261. // #endif
  262. // #ifndef APP-PLUS
  263. async mounted() {
  264. if(this.beforeDelay) {
  265. await sleep(this.beforeDelay)
  266. }
  267. const config = await this.getContext()
  268. this.signature = new Signature(config)
  269. this.canvasEl = this.signature.canvas.get('el')
  270. this.canvasWidth = this.signature.canvas.get('width')
  271. this.canvasHeight = this.signature.canvas.get('height')
  272. this.stopWatch = this.$watch('param' , (v) => {
  273. this.signature.pen.setOption(v)
  274. }, {immediate: true})
  275. },
  276. // #endif
  277. // #ifndef APP-PLUS
  278. // #ifdef VUE3
  279. beforeUnmount() {
  280. this.stopWatch()
  281. this.signature.destroy()
  282. },
  283. // #endif
  284. // #ifdef VUE2
  285. beforeDestroy() {
  286. this.stopWatch()
  287. this.signature.destroy()
  288. },
  289. // #endif
  290. // #endif
  291. methods: {
  292. // #ifdef APP-PLUS
  293. onPageFinish() {
  294. this.$refs.webview.evalJS(`update(${JSON.stringify(this.param)})`)
  295. },
  296. onMessage(e = {}) {
  297. const {detail: {data: [res]}} = e
  298. if(res.event?.save) {
  299. this.toDataURL = res.event.save
  300. }
  301. if(res.event?.changeSize) {
  302. const {width, height} = res.event.changeSize
  303. }
  304. if(res.event.hasOwnProperty('isEmpty')) {
  305. this.risEmpty = res.event.isEmpty
  306. }
  307. if (res.event?.file) {
  308. this.tempFilePath.push(res.event.file)
  309. if (this.tempFilePath.length > 7) {
  310. this.tempFilePath.shift()
  311. }
  312. return
  313. }
  314. if (res.event?.success) {
  315. if (res.event.success) {
  316. this.tempFilePath.push(res.event.success)
  317. if (this.tempFilePath.length > 8) {
  318. this.tempFilePath.shift()
  319. }
  320. this.toDataURL = this.tempFilePath.join('')
  321. this.tempFilePath = []
  322. // base64ToPath(this.tempFilePath.join('')).then(res => {
  323. // })
  324. } else {
  325. this.$emit('fail', 'canvas no data')
  326. }
  327. return
  328. }
  329. },
  330. // #endif
  331. undo() {
  332. // #ifdef APP-VUE || APP-NVUE
  333. this.rundo += 1
  334. // #endif
  335. // #ifdef APP-NVUE
  336. this.$refs.webview.evalJS(`undo()`)
  337. // #endif
  338. // #ifndef APP-VUE
  339. if(this.signature)
  340. this.signature.undo()
  341. // #endif
  342. },
  343. clear() {
  344. // #ifdef APP-VUE || APP-NVUE
  345. this.rclear += 1
  346. // #endif
  347. // #ifdef APP-NVUE
  348. this.$refs.webview.evalJS(`clear()`)
  349. // #endif
  350. // #ifndef APP-VUE
  351. if(this.signature)
  352. this.signature.clear()
  353. // #endif
  354. },
  355. isEmpty() {
  356. // #ifdef APP-NVUE
  357. this.$refs.webview.evalJS(`isEmpty()`)
  358. // #endif
  359. // #ifdef APP-VUE || APP-NVUE
  360. this.rempty += 1
  361. // #endif
  362. // #ifndef APP-VUE || APP-NVUE
  363. return this.signature.isEmpty()
  364. // #endif
  365. },
  366. canvasToTempFilePath(param) {
  367. const isEmpty = this.isEmpty()
  368. // #ifdef APP-NVUE
  369. this.$refs.webview.evalJS(`save()`)
  370. // #endif
  371. // #ifdef APP-VUE || APP-NVUE
  372. const stopURLWatch = this.$watch('toDataURL', (v, n) => {
  373. if(v && v !== n) {
  374. if(param.pathType == 'url') {
  375. base64ToPath(v).then(res => {
  376. param.success({tempFilePath: res,isEmpty: this.risEmpty })
  377. })
  378. } else {
  379. param.success({tempFilePath: v,isEmpty: this.risEmpty })
  380. }
  381. this.toDataURL = ''
  382. }
  383. stopURLWatch && stopURLWatch()
  384. })
  385. this.rsave += 1
  386. // #endif
  387. // #ifndef APP-VUE || APP-NVUE
  388. const success = (success) => param.success && param.success(success)
  389. const fail = (fail) => param.fail && param.fail(err)
  390. const {canvas} = this.signature.canvas.get('el')
  391. const context = this.signature.canvas.get('context')
  392. const {backgroundColor} = this
  393. const width = this.signature.canvas.get('width')
  394. const height = this.signature.canvas.get('height')
  395. if(this.useCanvas2d) {
  396. try{
  397. // #ifndef MP-ALIPAY
  398. const tempFilePath = canvas.toDataURL()
  399. if(backgroundColor) {
  400. const image = canvas.createImage()
  401. image.src = tempFilePath
  402. image.onload = () => {
  403. context.fillStyle = backgroundColor
  404. context.fillRect(0, 0, width, height)
  405. context.drawImage(image, 0, 0, width, height);
  406. const tempFilePath = canvas.toDataURL()
  407. success({tempFilePath, isEmpty})
  408. context.clearRect(0,0, width, height)
  409. context.drawImage(image, 0, 0, width, height);
  410. }
  411. } else {
  412. success({tempFilePath, isEmpty})
  413. }
  414. // #endif
  415. // #ifdef MP-ALIPAY
  416. canvas.toTempFilePath({
  417. canvasid: this.canvasid,
  418. success(res){
  419. if(backgroundColor) {
  420. const image = canvas.createImage()
  421. image.src = tempFilePath
  422. image.onload = () => {
  423. canvas.toTempFilePath({
  424. canvasid: this.canvasid,
  425. success(res) {
  426. context.fillStyle = backgroundColor
  427. context.fillRect(0, 0, width, height)
  428. context.drawImage(image, 0, 0, width, height);
  429. success({tempFilePath, isEmpty})
  430. context.clearRect(0,0, width, height)
  431. context.drawImage(image, 0, 0, width, height);
  432. }
  433. })
  434. }
  435. } else {
  436. success({tempFilePath: res, isEmpty})
  437. }
  438. },
  439. fail
  440. })
  441. // #endif
  442. }catch(err){
  443. console.warn(err)
  444. fail(err)
  445. }
  446. } else {
  447. toDataURL(this.canvasId, this).then(res => {
  448. if(backgroundColor) {
  449. const image = createImage()
  450. image.src = res
  451. image.onload = () => {
  452. context.fillStyle = backgroundColor
  453. context.fillRect(0, 0, width, height)
  454. context.drawImage(image, 0, 0, width, height);
  455. context.draw && context.draw(true, () => {
  456. toDataURL(this.canvasId, this).then(res => {
  457. success({tempFilePath: res, isEmpty})
  458. context.clearRect(0,0, width, height)
  459. context.drawImage(image, 0, 0, width, height);
  460. context.draw && context.draw(true)
  461. })
  462. });
  463. }
  464. } else {
  465. success({tempFilePath: res, isEmpty})
  466. }
  467. }).catch(err => {
  468. console.warn(err)
  469. fail(err)
  470. })
  471. }
  472. // #endif
  473. },
  474. // #ifndef APP-PLUS
  475. getContext() {
  476. const {pixelRatio} = uni.getSystemInfoSync()
  477. return new Promise(resolve => {
  478. if(this.useCanvas2d) {
  479. uni.createSelectorQuery().in(this)
  480. .select(`#${this.canvasId}`)
  481. .fields({
  482. node: true,
  483. size: true,
  484. rect: true,
  485. })
  486. .exec(res => {
  487. if(res) {
  488. const {width, height, node, left, top, right} = res[0]
  489. const context = node.getContext('2d')
  490. node.width = width * pixelRatio;
  491. node.height = height * pixelRatio;
  492. resolve({ left, top, right, width, height, context, canvas: node, pixelRatio})
  493. }
  494. })
  495. } else {
  496. uni.createSelectorQuery().in(this)
  497. .select(`#${this.canvasId}`)
  498. .boundingClientRect()
  499. .exec(res => {
  500. if(res) {
  501. const {width, height, left, top, right} = res[0]
  502. const context = uniContext(uni.createCanvasContext(this.canvasId, this))
  503. const canvas = {
  504. createImage,
  505. toDataURL: () => toDataURL(this.canvasId, this),
  506. requestAnimationFrame
  507. }
  508. resolve({ left, top, right, width, height, context, pixelRatio:1, canvas})
  509. }
  510. })
  511. }
  512. })
  513. },
  514. touchStart(e) {
  515. if(!this.canvasEl) return
  516. this.isStart = true
  517. this.canvasEl.dispatchEvent('touchstart', wrapEvent(e))
  518. },
  519. touchMove(e) {
  520. if(!this.canvasEl || !this.isStart && this.canvasEl) return
  521. this.canvasEl.dispatchEvent('touchmove', wrapEvent(e))
  522. },
  523. touchEnd(e) {
  524. if(!this.canvasEl) return
  525. this.isStart = false
  526. this.canvasEl.dispatchEvent('touchend', wrapEvent(e))
  527. },
  528. // #endif
  529. }
  530. }
  531. </script>
  532. <style lang="stylus">
  533. .lime-signature,.lime-signature__canvas
  534. // #ifndef APP-NVUE
  535. width: 100%;
  536. height: 100%
  537. // #endif
  538. // #ifdef APP-NVUE
  539. flex: 1;
  540. // #endif
  541. </style>