index.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. /*
  2. *
  3. * ┏┓   ┏┓
  4. * ┏┛┻━━━┛┻┓
  5. * ┃       ┃
  6. * ┃   ━   ┃
  7. * ┃ >   < ┃
  8. * ┃       ┃
  9. * ┃... ⌒ ... ┃
  10. * ┃       ┃
  11. * ┗━┓   ┏━┛
  12. * ┃   ┃
  13. * ┃   ┃
  14. * ┃   ┃
  15. * ┃   ┃ 神兽保佑
  16. * ┃   ┃ 代码无bug
  17. * ┃   ┃
  18. * ┃   ┗━━━┓
  19. * ┃       ┣┓
  20. * ┃       ┏┛
  21. * ┗┓┓┏━┳┓┏┛
  22. * ┃┫┫ ┃┫┫
  23. * ┗┻┛ ┗┻┛
  24. * @Author: 木木
  25. * @LastEditors: 木木
  26. * @Date: 2022-06-02 09:46:06
  27. * @LastEditTime: 2022-06-02 10:41:42
  28. * @Description:调用摄像头识别二维码
  29. */
  30. /**
  31. * @description: 初始化
  32. * @param {*} options {
  33. * 使用后置还是前置摄像头
  34. * exact: 'environment' | 'user'
  35. * 获取摄像头视频像素 false 正常, true高清
  36. * definition: boolean
  37. * 是否持续监听
  38. * continue: boolean
  39. * 成功事件
  40. * onSuccess: ():string => {}
  41. * }
  42. * @param {*} onSuccess ():string=>{} 成功事件
  43. * @return {*} MumuQrcode
  44. */
  45. function MumuQrcode(options = {}, onSuccess) {
  46. // if (origin.indexOf('https') === -1) throw '请在 https 环境中调用摄像头。'
  47. if (origin.indexOf('https') === -1){
  48. alert('请在 https 环境中调用摄像头。')
  49. window.history.go(-1)
  50. return this
  51. }
  52. this.exact = options.exact || 'environment'
  53. this.definition = options.definition || false
  54. this.continue = options.continue || false
  55. this.onSuccess = onSuccess || null
  56. this.__init()
  57. return this
  58. }
  59. MumuQrcode.prototype.__init = function () {
  60. var js = document.createElement('script')
  61. js.type = 'text/javascript'
  62. js.src = './mumu-qrcode/jsQR.js'
  63. document.body.append(js)
  64. js.addEventListener('load', () => {
  65. this.start()
  66. })
  67. }
  68. MumuQrcode.prototype.start = function () {
  69. this.windowWidth = document.documentElement.clientWidth || document.body.clientWidth
  70. this.windowHeight = document.documentElement.clientHeight || document.body.clientHeight
  71. this.video = null
  72. this.canvas = null
  73. this.canvas2d = null
  74. /** 闪光灯相关 */
  75. this.stream = null
  76. this.track = null
  77. this.isUseTrack = false
  78. this.trackStatus = false
  79. this.video = document.createElement('video')
  80. this.video.setAttribute('playsinline', 'true')
  81. this.video.setAttribute('webkit-playsinline', 'true')
  82. this.video.width = this.windowWidth
  83. this.video.height = this.windowHeight
  84. this.canvas = document.createElement('canvas')
  85. this.canvas.width = this.windowWidth
  86. this.canvas.height = this.windowHeight
  87. this.canvas2d = this.canvas.getContext('2d')
  88. const mumuQrcode = document.getElementById('mumuQrcode')
  89. this.mumuQrcode = mumuQrcode
  90. mumuQrcode.style =
  91. 'position: absolute; top: 0;left: 0; background-color: #333;width:' +
  92. this.windowWidth +
  93. 'px;height:' +
  94. this.windowHeight +
  95. 'px'
  96. mumuQrcode.append(this.canvas)
  97. mumuQrcode.append(this.video)
  98. const box = document.createElement('div')
  99. box.className = 'box'
  100. const line = document.createElement('div')
  101. line.className = 'line'
  102. const angle = document.createElement('div')
  103. angle.className = 'angle'
  104. box.append(line)
  105. box.append(angle)
  106. mumuQrcode.append(box)
  107. /** 闪关灯操作 */
  108. const box2 = document.createElement('div')
  109. box2.className = 'box2'
  110. box2.innerText = '打开闪光灯'
  111. this.box2Dom = box2
  112. box2.addEventListener('click', () => {
  113. if (this.trackStatus) {
  114. box2.innerText = '打开闪光灯'
  115. } else {
  116. box2.innerText = '关闭闪光灯'
  117. }
  118. this.trackStatus = !this.trackStatus
  119. this.openTrack()
  120. })
  121. this.addStyle()
  122. this.openScan()
  123. }
  124. MumuQrcode.prototype.openScan = function () {
  125. var width = this.transtion(this.windowHeight)
  126. var height = this.transtion(this.windowWidth)
  127. var videoParam = {
  128. audio: false,
  129. video: {
  130. facingMode: { exact: this.exact },
  131. width,
  132. height
  133. }
  134. }
  135. navigator.mediaDevices
  136. .getUserMedia(videoParam)
  137. .then((stream) => {
  138. this.video.srcObject = stream
  139. this.video.play()
  140. this.tick()
  141. this.stream = stream
  142. this.track = stream.getVideoTracks()[0]
  143. setTimeout(() => {
  144. const t = this.track.getCapabilities()
  145. this.isUseTorch = t.torch || null
  146. if (this.isUseTorch) return this.mumuQrcode.append(this.box2Dom)
  147. }, 500)
  148. })
  149. .catch((error) => {
  150. alert('设备不支持,请检查是否允许摄像头权限')
  151. // console.log('设备不支持,请检查是否允许摄像头权限', error)
  152. // throw error
  153. })
  154. }
  155. MumuQrcode.prototype.tick = function () {
  156. if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) {
  157. this.canvas2d.drawImage(this.video, 0, 0, this.windowWidth, this.windowHeight)
  158. const imageData = this.canvas2d.getImageData(0, 0, this.windowWidth, this.windowHeight)
  159. const codeRes = jsQR(imageData.data, imageData.width, imageData.height, {
  160. inversionAttempts: 'dontInvert'
  161. })
  162. if (codeRes) {
  163. this.drawLine(codeRes.location.topLeftCorner, codeRes.location.topRightCorner, '#FF3B58')
  164. this.drawLine(codeRes.location.topRightCorner, codeRes.location.bottomRightCorner, '#FF3B58')
  165. this.drawLine(
  166. codeRes.location.bottomRightCorner,
  167. codeRes.location.bottomLeftCorner,
  168. '#FF3B58'
  169. )
  170. this.drawLine(codeRes.location.bottomLeftCorner, codeRes.location.topLeftCorner, '#FF3B58')
  171. if (codeRes.data) {
  172. this.getData(codeRes.data)
  173. }
  174. }
  175. }
  176. requestAnimationFrame(this.tick.bind(this))
  177. }
  178. MumuQrcode.prototype.drawLine = function (begin, end, color) {
  179. this.canvas2d.beginPath()
  180. this.canvas2d.moveTo(begin.x, begin.y)
  181. this.canvas2d.lineTo(end.x, end.y)
  182. this.canvas2d.lineWidth = 4
  183. this.canvas2d.strokeStyle = color
  184. this.canvas2d.stroke()
  185. }
  186. MumuQrcode.prototype.getData = function (code) {
  187. this.onSuccess && this.onSuccess(code)
  188. if (this.continue) return
  189. if (!this.stream) return
  190. this.stream.getTracks().forEach((track) => {
  191. track.stop()
  192. })
  193. }
  194. MumuQrcode.prototype.openTrack = function () {
  195. if (!this.stream) return
  196. const ww = {
  197. advanced: [{ torch: this.trackStatus }]
  198. }
  199. this.track.applyConstraints(ww)
  200. }
  201. MumuQrcode.prototype.transtion = function (number) {
  202. return this.definition ? number * 1.6 : number
  203. }
  204. MumuQrcode.prototype.addStyle = function () {
  205. const style = document.createElement('style')
  206. style.innerHTML = `
  207. .box {
  208. width: 4rem;
  209. height: 4rem;
  210. position: absolute;
  211. left: 50%;
  212. top: 50%;
  213. transform: translate(-50%, -50%);
  214. overflow: hidden;
  215. border: 0.1rem solid rgba(0, 255, 51, 0.2);
  216. z-index: 10;
  217. }
  218. .box .line {
  219. height: calc(100% - 2px);
  220. width: 100%;
  221. background: linear-gradient(180deg, rgba(0, 255, 51, 0) 43%, #00ff33 211%);
  222. border-bottom: 3px solid #00ff33;
  223. transform: translateY(-100%);
  224. animation: radar-beam 2s infinite alternate;
  225. animation-timing-function: cubic-bezier(0.53, 0, 0.43, 0.99);
  226. animation-delay: 1.4s;
  227. }
  228. .box:after,
  229. .box:before,
  230. .angle:after,
  231. .angle:before {
  232. content: '';
  233. display: block;
  234. position: absolute;
  235. width: 0.2rem;
  236. height: 0.2rem;
  237. z-index: 12;
  238. border: 0.1rem solid transparent;
  239. }
  240. .box:after,
  241. .box:before {
  242. top: 0;
  243. border-top-color: #00ff33;
  244. }
  245. .angle:after,
  246. .angle:before {
  247. bottom: 0;
  248. border-bottom-color: #00ff33;
  249. }
  250. .box:before,
  251. .angle:before {
  252. left: 0;
  253. border-left-color: #00ff33;
  254. }
  255. .box:after,
  256. .angle:after {
  257. right: 0;
  258. border-right-color: #00ff33;
  259. }
  260. @keyframes radar-beam {
  261. 0% {
  262. transform: translateY(-100%);
  263. }
  264. 100% {
  265. transform: translateY(0);
  266. }
  267. }
  268. .box2 {
  269. width: 2rem;
  270. height: 1rem;
  271. position: absolute;
  272. left: 50%;
  273. bottom: 1.2rem;
  274. transform: translate(-50%);
  275. z-index: 20;
  276. font-size: 0.28rem;
  277. display: flex;
  278. align-items: center;
  279. justify-content: center;
  280. color: aliceblue;
  281. background-color: rgba(000, 000, 000, 0.35);
  282. }
  283. `
  284. document.body.append(style)
  285. }