game.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. import Two from "two.js";
  2. const colorList = ['#f5222d', '#d4380d', '#d46b08', '#d48806', '#d4b106', '#7cb305', '#389e0d', '#08979c', '#096dd9', '#531dab']
  3. import backImage from '@/assets/BackImage/framebg.jpg'
  4. import wheelImage from '@/assets/RoundaboutImage/2-09.png';
  5. import cageImage from '@/assets/RoundaboutImage/2-13.png';
  6. import pedestalImage from '@/assets/RoundaboutImage/2-14.png'
  7. import countImage from '@/assets/RoundaboutImage/计数.png'
  8. import lightImage from '@/assets/RoundaboutImage/round.png'
  9. import ropeImage1 from '@/assets/RoundaboutImage/2-15.png'
  10. import ropeImage2 from '@/assets/RoundaboutImage/2-16.png'
  11. import ropeImage3 from '@/assets/RoundaboutImage/2-17.png'
  12. import ropeImage4 from '@/assets/RoundaboutImage/2-18.png'
  13. import ropeImage5 from '@/assets/RoundaboutImage/2-19.png'
  14. import ropeImage6 from '@/assets/RoundaboutImage/2-20.png'
  15. import ropeImage7 from '@/assets/RoundaboutImage/2-21.png'
  16. import ropeImage8 from '@/assets/RoundaboutImage/2-22.png'
  17. import ropeImage9 from '@/assets/RoundaboutImage/2-23.png'
  18. const designWidth = 640; // 设计稿宽度
  19. const desginRadius = 246; // 设计稿轮盘半径
  20. const pedestalDistance = 424; // 底座与轮盘圆心距离
  21. const primaryColor = '#F19805'; // 设计稿主题色
  22. const scale = window.screen.width / designWidth;
  23. const ropeImageList = [
  24. ropeImage1,
  25. ropeImage2,
  26. ropeImage3,
  27. ropeImage4,
  28. ropeImage5,
  29. ropeImage6,
  30. ropeImage7,
  31. ropeImage8,
  32. ropeImage9,
  33. ];
  34. export default function game({ el, center, onError, onSuccess, onBingo }) {
  35. // 是否游戏结束
  36. let isFinished = false;
  37. // 是否失败
  38. // eslint-disable-next-line no-unused-vars
  39. let isError = false;
  40. // 每个区域的旋转角度, 8 个轿厢, 分 16 等份
  41. const cageNum = 8;
  42. const perAngle = Math.PI / cageNum // 2 * Math.PI / ( cageNum * 2 ) 分成 轿厢 * 2 的分数, 最终效果是每隔一个,显示一个轿厢
  43. const offsetAngle = perAngle / 2
  44. // 大转盘半径
  45. const raduis = desginRadius * scale;
  46. // 轿厢宽度 = 大轮盘的周长 / 间隔份数
  47. const cageRadius = Math.PI * 2 * raduis / (cageNum * 2)
  48. // 转盘中心坐标
  49. // const center;
  50. // 轿厢 - 子弹
  51. const bullets = []
  52. // 子弹是否在射击状态, 如果是, 不能进行其他操作
  53. let isShooting = false
  54. // 子弹飞行速度, 因为是向上飞, y 值是不端减小的过程
  55. const speed = -5;
  56. // 目标轿厢列表, 该列表主要用来判断子弹是否击中目标
  57. const cageList = [];
  58. // 轿厢的初始旋转弧度
  59. let rotateAngle = 0
  60. // 轿厢的旋转速度 - 单位弧度
  61. const rotateSpeed = 0.01
  62. // 目标轿厢与子弹轿厢的映射字典
  63. const mntMap = {}
  64. // 初始化
  65. let two = new Two({
  66. fullscreen: true,
  67. autostart: true
  68. }).appendTo(el);
  69. // 绘制背景
  70. makeBg({two, center});
  71. // 绘制转盘
  72. const wheelArc = two.makeCircle(center.x, center.y, 0);
  73. const lightArc = two.makeCircle(center.x, center.y, 0);
  74. const drawRoundAbout = () => {
  75. const { x, y } = center
  76. // 绘制图片转盘
  77. const wheelTexture = two.makeTexture(wheelImage);
  78. wheelArc.noStroke();
  79. wheelArc.fill = wheelTexture;
  80. wheelTexture.bind('load', () => {
  81. wheelArc.radius = wheelTexture.image.naturalWidth / 2;
  82. wheelArc.scale = scale;
  83. })
  84. // 绘制光圈
  85. const lightTexture = two.makeTexture(lightImage);
  86. lightArc.noStroke();
  87. lightArc.fill = lightTexture;
  88. lightTexture.bind('load', () => {
  89. lightArc.radius = lightTexture.image.naturalWidth / 2;
  90. lightArc.scale = scale;
  91. })
  92. lightArc.bind('update', () => {
  93. console.log('-----------lightArc----------')
  94. })
  95. // 绘制辅助扇形区域
  96. const arcList = Array(cageNum).fill().map((_, inx) => {
  97. const startAngle = 2 * inx * perAngle - offsetAngle; // 实际效果图是圆形有固定大小的, 不能直接从 0° 开始, 需要从 0 - offset 开始
  98. const endAngle = startAngle + perAngle
  99. const arc = two.makeArcSegment(x, y, 0, raduis, startAngle, endAngle)
  100. // arc.fill = colorList[inx] // 取消这个注释, 有助于梳理逻辑
  101. arc.noFill()
  102. arc.noStroke()
  103. // 绘制目标轿厢
  104. // 1、计算扇形边上的中心坐标
  105. const cx = x + Math.cos(startAngle + perAngle / 2) * raduis
  106. const cy = y + Math.sin(startAngle + perAngle / 2) * raduis
  107. // 2、轿厢
  108. const rect = two.makeCircle(cx, cy, cageRadius / 2);
  109. rect.id = `mnt-${inx}`
  110. // rect.fill = colorList[inx] // 取消这个注释, 有助于梳理逻辑
  111. rect.noFill()
  112. rect.stroke = primaryColor
  113. rect.linewidth = 2
  114. rect.dashes = [4]
  115. rect.__$angle = startAngle // 轿厢的初始弧度
  116. cageList.push(rect)
  117. return arc
  118. })
  119. return arcList
  120. }
  121. const roundAbout = drawRoundAbout()
  122. // 成功之后的特效
  123. const successEffect = () => {
  124. }
  125. // 旋转
  126. const rotate = () => {
  127. rotateAngle += rotateSpeed
  128. wheelArc.rotation = rotateAngle
  129. roundAbout.forEach((x, inx) => {
  130. x.rotation = rotateAngle
  131. // 计算旋转后的轿厢坐标
  132. const cage = cageList[inx]
  133. const angle = cage.__$angle + rotateAngle + perAngle / 2
  134. const cx = center.x + Math.cos(angle) * raduis
  135. const cy = center.y + Math.sin(angle) * raduis
  136. cage.position = new Two.Vector(cx, cy)
  137. // 如果有对应的挂载物
  138. if (mntMap[cage.id]) {
  139. mntMap[cage.id].position = cage.position
  140. }
  141. })
  142. }
  143. let ropeStart = null;
  144. let ropeStop = null;
  145. // 绘制底座
  146. const drawPedestal = (x, y) => {
  147. //
  148. const pedestalTexture = two.makeTexture(pedestalImage);
  149. const pedestalBox = two.makeRectangle(x, y, 0, 0);
  150. pedestalBox.noStroke();
  151. pedestalBox.fill = pedestalTexture;
  152. pedestalTexture.bind('load', () => {
  153. const { naturalWidth, naturalHeight } = pedestalTexture.image
  154. pedestalBox.width = naturalWidth
  155. pedestalBox.height = naturalHeight
  156. pedestalBox.scale = scale
  157. })
  158. //
  159. const ropeSequence = two.makeImageSequence(ropeImageList, x, y, 40); // 40 是不断调式出来的频率, 需要跟 speed 匹配
  160. ropeSequence.scale = new Two.Vector(scale, scale);
  161. ropeStart = () => ropeSequence.play();
  162. ropeStop = () => ropeSequence.stop();
  163. return pedestalBox;
  164. }
  165. const pedestalBox = drawPedestal(center.x, center.y + pedestalDistance * scale)
  166. // 绘制子弹
  167. const drawBullets = (x, y) => {
  168. const cageTexture = two.makeTexture(cageImage);
  169. const list = Array(cageNum).fill().map((_, inx) => {
  170. const arc = two.makeCircle(x, y, cageRadius / 2)
  171. arc.id = `bullet-${inx}`
  172. // arc.fill = 'black'
  173. arc.fill = cageTexture
  174. arc.noStroke()
  175. arc.visible = inx === 0; // 第一个显示, 其余默认不显示
  176. return arc
  177. })
  178. cageTexture.bind('load', () => {
  179. const r = cageTexture.image.naturalWidth / 2;
  180. list.forEach(it => {
  181. it.radius = r;
  182. it.scale = scale;
  183. });
  184. })
  185. bullets.push(...list)
  186. }
  187. drawBullets(center.x, center.y + pedestalDistance * scale)
  188. // 待发射子弹
  189. const clip = bullets.slice()
  190. // 绘制计数器
  191. const drawCounter = (x, y) => {
  192. const counterTexture = two.makeTexture(countImage);
  193. const counterBox = two.makeRectangle(x, y, 0, 0);
  194. counterBox.noStroke();
  195. counterBox.fill = counterTexture;
  196. counterTexture.bind('load', () => {
  197. const { naturalWidth, naturalHeight } = counterTexture.image
  198. counterBox.width = naturalWidth
  199. counterBox.height = naturalHeight
  200. counterBox.scale = scale
  201. })
  202. // 初始值是弹夹中子弹数量
  203. const counter = two.makeText(clip.length, x, y, { fill: primaryColor, weight: 700 })
  204. return counter
  205. }
  206. const counter = drawCounter(pedestalBox.position.x + (224 * scale), pedestalBox.position.y); // 224 是设计稿上底座与计数器之间距离
  207. // 挂载到轮盘
  208. const mountToCage = (targ, cage) => {
  209. targ.position = cage.position
  210. // 写入映射表
  211. mntMap[cage.id] = targ
  212. }
  213. // 当前子弹
  214. let currentBullet = null;
  215. // 射击
  216. const shooting = () => {
  217. const { top, height } = currentBullet.getBoundingClientRect()
  218. currentBullet.position = new Two.Vector(currentBullet.position.x, top + height / 2 + speed)
  219. // 是否击中
  220. const hitted = isHit(currentBullet)
  221. if (hitted === 'error') {
  222. ropeStop();
  223. const t = setTimeout(() => {
  224. isError = true
  225. isFinished = true
  226. onError()
  227. clearTimeout(t)
  228. }, 0)
  229. return
  230. }
  231. // 击中之后, 需要挂载轿厢到对应的位置
  232. if (hitted) {
  233. mountToCage(currentBullet, hitted)
  234. // currentBullet = null;
  235. currentBullet = clip.shift();
  236. if (currentBullet) {
  237. currentBullet.visible = true
  238. counter.value = clip.length + 1
  239. } else {
  240. isFinished = true
  241. counter.value = 0
  242. }
  243. isShooting = false;
  244. ropeStop();
  245. const t = setTimeout(() => {
  246. if (isFinished) {
  247. onSuccess()
  248. } else {
  249. onBingo()
  250. }
  251. clearTimeout(t)
  252. }, 0)
  253. }
  254. }
  255. // 是否击中
  256. // 如果击中, 则返回目标轿厢
  257. const isHit = (bullet) => {
  258. const rect2 = bullet.getBoundingClientRect();
  259. const x = rect2.left + rect2.width / 2
  260. const y = rect2.top + rect2.height / 2
  261. if (y < (center.y + raduis)) {
  262. return 'error';
  263. }
  264. const cage = cageList.filter((it) => {
  265. const rect1 = it.getBoundingClientRect();
  266. // 如果当前子弹的中心点位于目标轿厢矩形范围内
  267. // 则代表击中
  268. return x >= rect1.left && x <= rect1.right &&
  269. y >= rect1.top && y <= rect1.bottom;
  270. })[0]
  271. if (!cage) return false; // 未找到对应的目标轿厢
  272. if (mntMap[cage.id]) return false; // 已经挂载过轿厢
  273. return cage
  274. }
  275. // 重复绘制内容
  276. two.bind('update', () => {
  277. if (!isFinished) {
  278. rotate()
  279. if (isShooting) {
  280. shooting()
  281. }
  282. }
  283. })
  284. // 绑定 dom click 事件 触发子弹发射
  285. el.addEventListener('click', () => {
  286. if (!isFinished && !isShooting) {
  287. isShooting = true
  288. // 启动绳子效果
  289. ropeStart()
  290. if (!currentBullet) {
  291. currentBullet = clip.shift()
  292. currentBullet.visible = true
  293. }
  294. }
  295. })
  296. return {
  297. start: () => {
  298. isFinished = false;
  299. },
  300. stop: () => {
  301. isFinished = true;
  302. },
  303. destroy: () => {
  304. two.unbind('update');
  305. two.pause();
  306. el.removeChild(two.renderer.domElement);
  307. two = null;
  308. }
  309. }
  310. }
  311. function makeBg({two, center}) {
  312. const bgTexture = two.makeTexture(backImage);
  313. const bgBox = two.makeRectangle(center.x, center.y, 0, 0);
  314. bgBox.noStroke();
  315. bgBox.fill = bgTexture;
  316. bgTexture.bind('load', () => {
  317. bgBox.width = bgTexture.image.naturalWidth;
  318. bgBox.height = bgTexture.image.naturalHeight;
  319. bgBox.scale = scale;
  320. })
  321. }