jigsaw.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. (function (window) {
  2. const l = 42, // 滑块边长
  3. r = 9, // 滑块半径
  4. w = 310, // canvas宽度
  5. h = 155, // canvas高度
  6. PI = Math.PI
  7. const L = l + r * 2 + 3 // 滑块实际边长
  8. function getRandomNumberByRange (start, end) {
  9. return Math.round(Math.random() * (end - start) + start)
  10. }
  11. function createCanvas (width, height) {
  12. const canvas = createElement('canvas')
  13. canvas.width = width
  14. canvas.height = height
  15. return canvas
  16. }
  17. function createImg (onload) {
  18. const img = createElement('img')
  19. img.crossOrigin = "Anonymous"
  20. img.onload = onload
  21. img.onerror = () => {
  22. img.src = getRandomImg()
  23. }
  24. img.src = getRandomImg()
  25. return img
  26. }
  27. function createElement (tagName, className) {
  28. const elment = document.createElement(tagName)
  29. elment.className = className
  30. return elment
  31. }
  32. function addClass (tag, className) {
  33. tag.classList.add(className)
  34. }
  35. function removeClass (tag, className) {
  36. tag.classList.remove(className)
  37. }
  38. function getRandomImg () {
  39. return 'https://picsum.photos/300/150/?image=' + getRandomNumberByRange(0, 1084)
  40. }
  41. function draw (ctx, x, y, operation) {
  42. ctx.beginPath()
  43. ctx.moveTo(x, y)
  44. ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI)
  45. ctx.lineTo(x + l, y)
  46. ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI)
  47. ctx.lineTo(x + l, y + l)
  48. ctx.lineTo(x, y + l)
  49. ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true)
  50. ctx.lineTo(x, y)
  51. ctx.lineWidth = 2
  52. ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
  53. ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'
  54. ctx.stroke()
  55. ctx[operation]()
  56. ctx.globalCompositeOperation = 'overlay'
  57. }
  58. function sum (x, y) {
  59. return x + y
  60. }
  61. function square (x) {
  62. return x * x
  63. }
  64. class jigsaw {
  65. constructor ({ el, onSuccess, onFail, onRefresh }) {
  66. el.style.position = el.style.position || 'relative'
  67. this.el = el
  68. this.onSuccess = onSuccess
  69. this.onFail = onFail
  70. this.onRefresh = onRefresh
  71. this.validateSuc = false;
  72. }
  73. init () {
  74. this.initDOM()
  75. this.initImg()
  76. this.bindEvents()
  77. }
  78. initDOM () {
  79. const canvas = createCanvas(w, h) // 画布
  80. const block = canvas.cloneNode(true) // 滑块
  81. const sliderContainer = createElement('div', 'sliderContainer')
  82. const refreshIcon = createElement('div', 'refreshIcon')
  83. const sliderMask = createElement('div', 'sliderMask')
  84. const slider = createElement('div', 'slider')
  85. const sliderIcon = createElement('span', 'sliderIcon')
  86. const text = createElement('span', 'sliderText')
  87. const picContainer = createElement('div', 'picContainer')
  88. picContainer.id = "picContainer"
  89. picContainer.style.display = "none"
  90. block.className = 'block'
  91. text.innerHTML = '向右滑动填充拼图'
  92. const el = this.el
  93. picContainer.appendChild(canvas)
  94. picContainer.appendChild(refreshIcon)
  95. picContainer.appendChild(block)
  96. el.appendChild(picContainer)
  97. slider.appendChild(sliderIcon)
  98. sliderMask.appendChild(slider)
  99. sliderContainer.appendChild(sliderMask)
  100. sliderContainer.appendChild(text)
  101. el.appendChild(sliderContainer)
  102. Object.assign(this, {
  103. canvas,
  104. block,
  105. sliderContainer,
  106. refreshIcon,
  107. slider,
  108. sliderMask,
  109. sliderIcon,
  110. text,
  111. canvasCtx: canvas.getContext('2d'),
  112. blockCtx: block.getContext('2d')
  113. })
  114. }
  115. initImg () {
  116. const img = createImg(() => {
  117. this.draw()
  118. this.canvasCtx.drawImage(img, 0, 0, w, h)
  119. this.blockCtx.drawImage(img, 0, 0, w, h)
  120. const y = this.y - r * 2 - 1
  121. const ImageData = this.blockCtx.getImageData(this.x - 3, y, L, L)
  122. this.block.width = L
  123. this.blockCtx.putImageData(ImageData, 0, y)
  124. })
  125. this.img = img
  126. }
  127. draw () {
  128. // 随机创建滑块的位置
  129. this.x = getRandomNumberByRange(L + 10, w - (L + 10))
  130. this.y = getRandomNumberByRange(10 + r * 2, h - (L + 10))
  131. draw(this.canvasCtx, this.x, this.y, 'fill')
  132. draw(this.blockCtx, this.x, this.y, 'clip')
  133. }
  134. clean () {
  135. this.canvasCtx.clearRect(0, 0, w, h)
  136. this.blockCtx.clearRect(0, 0, w, h)
  137. this.block.width = w
  138. }
  139. bindEvents () {
  140. this.el.onselectstart = () => false
  141. this.refreshIcon.onclick = () => {
  142. this.reset()
  143. typeof this.onRefresh === 'function' && this.onRefresh()
  144. }
  145. let originX, originY, trail = [], isMouseDown = false
  146. const handleDragStart = function (e) {
  147. originX = e.clientX || e.touches[0].clientX
  148. originY = e.clientY || e.touches[0].clientY
  149. isMouseDown = true
  150. }
  151. const handleDragMove = (e) => {
  152. if (!isMouseDown) return false
  153. const eventX = e.clientX || e.touches[0].clientX
  154. const eventY = e.clientY || e.touches[0].clientY
  155. const moveX = eventX - originX
  156. const moveY = eventY - originY
  157. if (moveX < 0 || moveX + 38 >= w) return false
  158. this.slider.style.left = moveX + 'px'
  159. const blockLeft = (w - 40 - 20) / (w - 40) * moveX
  160. this.block.style.left = blockLeft + 'px'
  161. addClass(this.sliderContainer, 'sliderContainer_active')
  162. this.sliderMask.style.width = moveX + 'px'
  163. trail.push(moveY)
  164. }
  165. const handleDragEnd = (e) => {
  166. if (!isMouseDown) return false
  167. isMouseDown = false
  168. const eventX = e.clientX || e.changedTouches[0].clientX
  169. if (eventX == originX) return false
  170. removeClass(this.sliderContainer, 'sliderContainer_active')
  171. this.trail = trail
  172. const { spliced, verified } = this.verify()
  173. if (spliced) {
  174. if (verified) {
  175. addClass(this.sliderContainer, 'sliderContainer_success')
  176. this.validateSuc = true;
  177. $("#picContainer").hide(300);
  178. typeof this.onSuccess === 'function' && this.onSuccess()
  179. } else {
  180. addClass(this.sliderContainer, 'sliderContainer_fail')
  181. this.text.innerHTML = '再试一次'
  182. this.reset()
  183. }
  184. } else {
  185. addClass(this.sliderContainer, 'sliderContainer_fail')
  186. typeof this.onFail === 'function' && this.onFail()
  187. setTimeout(() => {
  188. this.reset()
  189. }, 1000)
  190. }
  191. }
  192. this.slider.addEventListener('mousedown', handleDragStart)
  193. this.slider.addEventListener('touchstart', handleDragStart)
  194. document.addEventListener('mousemove', handleDragMove)
  195. document.addEventListener('touchmove', handleDragMove)
  196. document.addEventListener('mouseup', handleDragEnd)
  197. document.addEventListener('touchend', handleDragEnd)
  198. //处理悬浮事件
  199. $(".sliderContainer").hover(()=> {
  200. if(!this.validateSuc)
  201. $("#picContainer").show(300)
  202. }, function() {
  203. // $("#picContainer").hide(300)
  204. });
  205. $(document).on('click','',function(event){
  206. if(event.target.className != "picContainer" &&
  207. event.target.className != "sliderContainer" &&
  208. event.target.className != "sliderIcon" &&
  209. event.target.className != "sliderText" &&
  210. event.target.className != "slider" &&
  211. event.target.tagName != "CANVAS")
  212. {
  213. $("#picContainer").hide(300)
  214. }
  215. })
  216. }
  217. verify () {
  218. const arr = this.trail // 拖动时y轴的移动距离
  219. const average = arr.reduce(sum) / arr.length
  220. const deviations = arr.map(x => x - average)
  221. const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length)
  222. const left = parseInt(this.block.style.left)
  223. return {
  224. spliced: Math.abs(left - this.x) < 10,
  225. verified: stddev !== 0, // 简单验证下拖动轨迹,为零时表示Y轴上下没有波动,可能非人为操作
  226. }
  227. }
  228. reset () {
  229. this.sliderContainer.className = 'sliderContainer'
  230. this.slider.style.left = 0
  231. this.block.style.left = 0
  232. this.sliderMask.style.width = 0
  233. this.clean()
  234. this.img.src = getRandomImg()
  235. }
  236. }
  237. window.jigsaw = {
  238. init: function (opts) {
  239. return new jigsaw(opts).init()
  240. }
  241. }
  242. }(window))