import Two from "two.js"; const colorList = ['#f5222d', '#d4380d', '#d46b08', '#d48806', '#d4b106', '#7cb305', '#389e0d', '#08979c', '#096dd9', '#531dab'] import backImage from '@/assets/BackImage/framebg.jpg' import wheelImage from '@/assets/RoundaboutImage/2-09.png'; import cageImage from '@/assets/RoundaboutImage/2-13.png'; import pedestalImage from '@/assets/RoundaboutImage/2-14.png' import countImage from '@/assets/RoundaboutImage/计数.png' import lightImage from '@/assets/RoundaboutImage/round.png' import ropeImage1 from '@/assets/RoundaboutImage/2-15.png' import ropeImage2 from '@/assets/RoundaboutImage/2-16.png' import ropeImage3 from '@/assets/RoundaboutImage/2-17.png' import ropeImage4 from '@/assets/RoundaboutImage/2-18.png' import ropeImage5 from '@/assets/RoundaboutImage/2-19.png' import ropeImage6 from '@/assets/RoundaboutImage/2-20.png' import ropeImage7 from '@/assets/RoundaboutImage/2-21.png' import ropeImage8 from '@/assets/RoundaboutImage/2-22.png' import ropeImage9 from '@/assets/RoundaboutImage/2-23.png' const designWidth = 640; // 设计稿宽度 const desginRadius = 246; // 设计稿轮盘半径 const pedestalDistance = 424; // 底座与轮盘圆心距离 const primaryColor = '#F19805'; // 设计稿主题色 const scale = window.screen.width / designWidth; const ropeImageList = [ ropeImage1, ropeImage2, ropeImage3, ropeImage4, ropeImage5, ropeImage6, ropeImage7, ropeImage8, ropeImage9, ]; export default function game({ el, center, onError, onSuccess, onBingo }) { // 是否游戏结束 let isFinished = false; // 是否失败 // eslint-disable-next-line no-unused-vars let isError = false; // 每个区域的旋转角度, 8 个轿厢, 分 16 等份 const cageNum = 8; const perAngle = Math.PI / cageNum // 2 * Math.PI / ( cageNum * 2 ) 分成 轿厢 * 2 的分数, 最终效果是每隔一个,显示一个轿厢 const offsetAngle = perAngle / 2 // 大转盘半径 const raduis = desginRadius * scale; // 轿厢宽度 = 大轮盘的周长 / 间隔份数 const cageRadius = Math.PI * 2 * raduis / (cageNum * 2) // 转盘中心坐标 // const center; // 轿厢 - 子弹 const bullets = [] // 子弹是否在射击状态, 如果是, 不能进行其他操作 let isShooting = false // 子弹飞行速度, 因为是向上飞, y 值是不端减小的过程 const speed = -5; // 目标轿厢列表, 该列表主要用来判断子弹是否击中目标 const cageList = []; // 轿厢的初始旋转弧度 let rotateAngle = 0 // 轿厢的旋转速度 - 单位弧度 const rotateSpeed = 0.01 // 目标轿厢与子弹轿厢的映射字典 const mntMap = {} // 初始化 let two = new Two({ fullscreen: true, autostart: true }).appendTo(el); // 绘制背景 makeBg({two, center}); // 绘制转盘 const wheelArc = two.makeCircle(center.x, center.y, 0); const lightArc = two.makeCircle(center.x, center.y, 0); const drawRoundAbout = () => { const { x, y } = center // 绘制图片转盘 const wheelTexture = two.makeTexture(wheelImage); wheelArc.noStroke(); wheelArc.fill = wheelTexture; wheelTexture.bind('load', () => { wheelArc.radius = wheelTexture.image.naturalWidth / 2; wheelArc.scale = scale; }) // 绘制光圈 const lightTexture = two.makeTexture(lightImage); lightArc.noStroke(); lightArc.fill = lightTexture; lightTexture.bind('load', () => { lightArc.radius = lightTexture.image.naturalWidth / 2; lightArc.scale = scale; }) lightArc.bind('update', () => { console.log('-----------lightArc----------') }) // 绘制辅助扇形区域 const arcList = Array(cageNum).fill().map((_, inx) => { const startAngle = 2 * inx * perAngle - offsetAngle; // 实际效果图是圆形有固定大小的, 不能直接从 0° 开始, 需要从 0 - offset 开始 const endAngle = startAngle + perAngle const arc = two.makeArcSegment(x, y, 0, raduis, startAngle, endAngle) // arc.fill = colorList[inx] // 取消这个注释, 有助于梳理逻辑 arc.noFill() arc.noStroke() // 绘制目标轿厢 // 1、计算扇形边上的中心坐标 const cx = x + Math.cos(startAngle + perAngle / 2) * raduis const cy = y + Math.sin(startAngle + perAngle / 2) * raduis // 2、轿厢 const rect = two.makeCircle(cx, cy, cageRadius / 2); rect.id = `mnt-${inx}` // rect.fill = colorList[inx] // 取消这个注释, 有助于梳理逻辑 rect.noFill() rect.stroke = primaryColor rect.linewidth = 2 rect.dashes = [4] rect.__$angle = startAngle // 轿厢的初始弧度 cageList.push(rect) return arc }) return arcList } const roundAbout = drawRoundAbout() // 成功之后的特效 const successEffect = () => { } // 旋转 const rotate = () => { rotateAngle += rotateSpeed wheelArc.rotation = rotateAngle roundAbout.forEach((x, inx) => { x.rotation = rotateAngle // 计算旋转后的轿厢坐标 const cage = cageList[inx] const angle = cage.__$angle + rotateAngle + perAngle / 2 const cx = center.x + Math.cos(angle) * raduis const cy = center.y + Math.sin(angle) * raduis cage.position = new Two.Vector(cx, cy) // 如果有对应的挂载物 if (mntMap[cage.id]) { mntMap[cage.id].position = cage.position } }) } let ropeStart = null; let ropeStop = null; // 绘制底座 const drawPedestal = (x, y) => { // const pedestalTexture = two.makeTexture(pedestalImage); const pedestalBox = two.makeRectangle(x, y, 0, 0); pedestalBox.noStroke(); pedestalBox.fill = pedestalTexture; pedestalTexture.bind('load', () => { const { naturalWidth, naturalHeight } = pedestalTexture.image pedestalBox.width = naturalWidth pedestalBox.height = naturalHeight pedestalBox.scale = scale }) // const ropeSequence = two.makeImageSequence(ropeImageList, x, y, 40); // 40 是不断调式出来的频率, 需要跟 speed 匹配 ropeSequence.scale = new Two.Vector(scale, scale); ropeStart = () => ropeSequence.play(); ropeStop = () => ropeSequence.stop(); return pedestalBox; } const pedestalBox = drawPedestal(center.x, center.y + pedestalDistance * scale) // 绘制子弹 const drawBullets = (x, y) => { const cageTexture = two.makeTexture(cageImage); const list = Array(cageNum).fill().map((_, inx) => { const arc = two.makeCircle(x, y, cageRadius / 2) arc.id = `bullet-${inx}` // arc.fill = 'black' arc.fill = cageTexture arc.noStroke() arc.visible = inx === 0; // 第一个显示, 其余默认不显示 return arc }) cageTexture.bind('load', () => { const r = cageTexture.image.naturalWidth / 2; list.forEach(it => { it.radius = r; it.scale = scale; }); }) bullets.push(...list) } drawBullets(center.x, center.y + pedestalDistance * scale) // 待发射子弹 const clip = bullets.slice() // 绘制计数器 const drawCounter = (x, y) => { const counterTexture = two.makeTexture(countImage); const counterBox = two.makeRectangle(x, y, 0, 0); counterBox.noStroke(); counterBox.fill = counterTexture; counterTexture.bind('load', () => { const { naturalWidth, naturalHeight } = counterTexture.image counterBox.width = naturalWidth counterBox.height = naturalHeight counterBox.scale = scale }) // 初始值是弹夹中子弹数量 const counter = two.makeText(clip.length, x, y, { fill: primaryColor, weight: 700 }) return counter } const counter = drawCounter(pedestalBox.position.x + (224 * scale), pedestalBox.position.y); // 224 是设计稿上底座与计数器之间距离 // 挂载到轮盘 const mountToCage = (targ, cage) => { targ.position = cage.position // 写入映射表 mntMap[cage.id] = targ } // 当前子弹 let currentBullet = null; // 射击 const shooting = () => { const { top, height } = currentBullet.getBoundingClientRect() currentBullet.position = new Two.Vector(currentBullet.position.x, top + height / 2 + speed) // 是否击中 const hitted = isHit(currentBullet) if (hitted === 'error') { ropeStop(); const t = setTimeout(() => { isError = true isFinished = true onError() clearTimeout(t) }, 0) return } // 击中之后, 需要挂载轿厢到对应的位置 if (hitted) { mountToCage(currentBullet, hitted) // currentBullet = null; currentBullet = clip.shift(); if (currentBullet) { currentBullet.visible = true counter.value = clip.length + 1 } else { isFinished = true counter.value = 0 } isShooting = false; ropeStop(); const t = setTimeout(() => { if (isFinished) { onSuccess() } else { onBingo() } clearTimeout(t) }, 0) } } // 是否击中 // 如果击中, 则返回目标轿厢 const isHit = (bullet) => { const rect2 = bullet.getBoundingClientRect(); const x = rect2.left + rect2.width / 2 const y = rect2.top + rect2.height / 2 if (y < (center.y + raduis)) { return 'error'; } const cage = cageList.filter((it) => { const rect1 = it.getBoundingClientRect(); // 如果当前子弹的中心点位于目标轿厢矩形范围内 // 则代表击中 return x >= rect1.left && x <= rect1.right && y >= rect1.top && y <= rect1.bottom; })[0] if (!cage) return false; // 未找到对应的目标轿厢 if (mntMap[cage.id]) return false; // 已经挂载过轿厢 return cage } // 重复绘制内容 two.bind('update', () => { if (!isFinished) { rotate() if (isShooting) { shooting() } } }) // 绑定 dom click 事件 触发子弹发射 el.addEventListener('click', () => { if (!isFinished && !isShooting) { isShooting = true // 启动绳子效果 ropeStart() if (!currentBullet) { currentBullet = clip.shift() currentBullet.visible = true } } }) return { start: () => { isFinished = false; }, stop: () => { isFinished = true; }, destroy: () => { two.unbind('update'); two.pause(); el.removeChild(two.renderer.domElement); two = null; } } } function makeBg({two, center}) { const bgTexture = two.makeTexture(backImage); const bgBox = two.makeRectangle(center.x, center.y, 0, 0); bgBox.noStroke(); bgBox.fill = bgTexture; bgTexture.bind('load', () => { bgBox.width = bgTexture.image.naturalWidth; bgBox.height = bgTexture.image.naturalHeight; bgBox.scale = scale; }) }