basketball.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. <template>
  2. <div class="game-container">
  3. <canvas id="canvas" @mousedown="handleMouseDown" @mouseup="handleMouseUp" @touchstart="handleTouchStart"
  4. @touchend="handleTouchEnd"></canvas>
  5. </div>
  6. </template>
  7. <script setup name="Basketball" lang="ts">
  8. import { onMounted, ref, reactive, onBeforeUnmount } from 'vue';
  9. import { initSpeech, speckText, playMusic, controlMusic, speckCancel, chineseNumber } from '@/utils/speech';
  10. import { useWebSocket } from '@/utils/bodyposeWs';
  11. const { proxy } = getCurrentInstance() as any;
  12. const router = useRouter();
  13. const { bodyposeWs, startDevice, checkBodypose, openBodypose, terminateBodypose, suspendBodypose, resumeBodypose, getBodyposeState, closeWS } = useWebSocket();
  14. const data = reactive<any>({
  15. bodyposeData: {},//姿态信息
  16. bodyposeState: false,//姿态识别窗口状态
  17. parameter: {},//参数
  18. deviceInfo: {},//设备信息
  19. againNum: 0,//再次启动次数
  20. againTimer: null,//定时状态
  21. wsState: false,//WS状态
  22. clientObj: {},//浏览器对象
  23. myThrow: 0,//0放下1举投
  24. });
  25. const { bodyposeData, bodyposeState, parameter, deviceInfo, againNum, againTimer, wsState, clientObj, myThrow } = toRefs(data);
  26. /**
  27. * 输出犯规
  28. */
  29. watch(
  30. () => myThrow.value,
  31. (newData,oldData) => {
  32. console.log("ppp",oldData,newData)
  33. if (newData == 1) {
  34. console.log("投篮")
  35. handleMouseDown();
  36. setTimeout(()=>{
  37. handleMouseUp();
  38. },100)
  39. }else{
  40. }
  41. },
  42. { immediate: true }
  43. );
  44. /**
  45. * 初始化
  46. */
  47. const getInit = async () => {
  48. console.log("触发姿态识别")
  49. let deviceid = localStorage.getItem('deviceid') || '';
  50. if (!deviceid) {
  51. proxy?.$modal.msgError(`请重新登录绑定设备号后使用`);
  52. return false;
  53. }
  54. bodyposeState.value = true;
  55. if (wsState.value) {
  56. proxy?.$modal.msgWarning(`操作过快,请稍后重试`);
  57. setTimeout(() => {
  58. bodyposeState.value = false;
  59. }, 1000)
  60. return false;
  61. }
  62. speckText("正在姿态识别");
  63. bodyposeWs((e: any) => {
  64. //console.log("bodyposeWS", e)
  65. if (e?.wksid) {
  66. wsState.value = true;
  67. //获取设备信息
  68. startDevice({ deviceid: deviceid });
  69. console.log("获取设备信息")
  70. }
  71. if (e?.type == 'fe_device_init_result') {
  72. //接收设备信息并发送请求
  73. if (e?.device_info) {
  74. deviceInfo.value = e.device_info;
  75. getCheckBodypose();
  76. console.log("返回设备信息,检查是否支持姿态识别")
  77. } else {
  78. proxy?.$modal.msgError(`设备信息缺失,请重新登录绑定设备号后使用`);
  79. }
  80. }
  81. if (e?.cmd == 'check_bodyposecontroller_available') {
  82. let handcontroller_id = deviceInfo.value.handcontroller_id;
  83. if (e?.code == 0) {
  84. //查看姿态识别状态,如果不处于关闭就先关闭再重新启动(可能会APP退出然后工作站还在运行的可能性)
  85. getBodyposeState(handcontroller_id);
  86. againNum.value = 0;
  87. againTimer.value = null;
  88. clearTimeout(againTimer.value);
  89. console.log("查看姿态识别状态")
  90. } else {
  91. //尝试多次查询姿态识别状态
  92. if (againNum.value <= 2) {
  93. againTimer.value = setTimeout(() => {
  94. getCheckBodypose();
  95. }, 500)
  96. againNum.value++;
  97. } else {
  98. let msg = "";
  99. if (e.code == 102402) {
  100. msg = `多次连接失败请重试,姿态识别模块被占用`;
  101. } else {
  102. msg = `多次连接失败请重试,姿态识别模块不可用,code:${e.code}`;
  103. }
  104. proxy?.$modal.msgWarning(msg);
  105. againNum.value = 0;
  106. againTimer.value = null;
  107. clearTimeout(againTimer.value);
  108. getCloseBodypose();//直接关闭
  109. }
  110. }
  111. }
  112. if (e?.cmd == 'get_bodyposecontroller_state') {
  113. let handcontroller_id = deviceInfo.value.handcontroller_id;
  114. //state说明: 0:关闭,3:空闲,36:工作中
  115. if ([3, 36].includes(e.state)) {
  116. getCloseBodypose();
  117. proxy?.$modal.msgWarning(`请重新姿态识别`);
  118. } else {
  119. openBodypose(handcontroller_id);
  120. }
  121. }
  122. if (e?.type == 'bodyposecontroller_result') {
  123. let arr = e.data.result.keypoints;
  124. let result = [];
  125. for (let i = 0; i < arr.length; i += 3) {
  126. result.push(arr.slice(i, i + 2));
  127. }
  128. //console.log("result", result)
  129. bodyposeData.value = result;
  130. getCanvas(result);
  131. }
  132. if (e?.cmd == 'terminate_bodyposecontroller') {
  133. if (e?.code == 0) {
  134. closeWS();
  135. bodyposeState.value = false;
  136. }
  137. }
  138. if (e?.type == 'disconnect') {
  139. wsState.value = false;
  140. }
  141. });
  142. };
  143. /**
  144. * 查询姿态识别状态
  145. */
  146. const getCheckBodypose = () => {
  147. let handcontroller_id = deviceInfo.value.handcontroller_id;
  148. //检查是否支持姿态识别
  149. checkBodypose(handcontroller_id);
  150. };
  151. /**
  152. * 关闭姿态识别
  153. */
  154. const getCloseBodypose = () => {
  155. let handcontroller_id = deviceInfo.value.handcontroller_id;
  156. terminateBodypose(handcontroller_id);
  157. bodyposeState.value = false;
  158. speckCancel(); //停止播报
  159. setTimeout(() => {
  160. if (wsState.value) {
  161. closeWS();
  162. }
  163. }, 3000)
  164. };
  165. const getCanvas = (data) => {
  166. let leftA = data[6][1];
  167. let rightA = data[5][1];
  168. let leftB = data[10][1];
  169. let rightB = data[9][1];
  170. if (leftB > leftA || rightB > rightA) {
  171. myThrow.value = 1;
  172. } else {
  173. myThrow.value = 0;
  174. }
  175. };
  176. // 游戏主类的响应式状态
  177. const gameState = reactive({
  178. version: '0.1',
  179. balls: [],
  180. hoops: [],
  181. texts: [],
  182. res: {},
  183. score: 0,
  184. started: false,
  185. gameOver: false,
  186. ballX: 320 / 2,
  187. ballY: 880,
  188. ballVel: 300,
  189. ballAngleVel: 100,
  190. ballAngle: 0,
  191. ballsShot: 1,
  192. ballCharge: 0,
  193. time: 60,
  194. toNextSecond: 1,
  195. sound: false,
  196. state: 'menu',
  197. click: false,
  198. canvas: null,
  199. ctx: null,
  200. animationFrameId: null,
  201. then: 0
  202. });
  203. // 篮筐类
  204. class Hoop {
  205. constructor(x, y) {
  206. this.x = x;
  207. this.y = y;
  208. this.points = [
  209. { x: x + 7, y: y + 18 },
  210. { x: x + 141, y: y + 18 }
  211. ];
  212. }
  213. drawBack(ctx, game) {
  214. drawImage(
  215. ctx,
  216. game.res['/static/images/basketball/hoop.png'],
  217. this.x,
  218. this.y,
  219. 0, 0, 148, 22, 0, 0, 0
  220. );
  221. }
  222. drawFront(ctx, game) {
  223. drawImage(
  224. ctx,
  225. game.res['/static/images/basketball/hoop.png'],
  226. this.x,
  227. this.y + 22,
  228. 0, 22, 148, 178 - 22, 0, 0, 0
  229. );
  230. }
  231. }
  232. // 篮球类
  233. class Ball {
  234. constructor(x, y) {
  235. this.x = x;
  236. this.y = y;
  237. this.vx = 0;
  238. this.vy = 0;
  239. this.speed = 100;
  240. this.canBounce = true;
  241. this.angle = 270;
  242. this.gravity = 0;
  243. this.falling = false;
  244. this.bounces = 0;
  245. this.scored = false;
  246. this.drawAngle = 0;
  247. this.angleVel = 100;
  248. this.solid = false;
  249. this.z = 1;
  250. }
  251. setAngle(angle) {
  252. this.angle = angle;
  253. this.vx = this.speed * Math.cos(this.angle * Math.PI / 180);
  254. this.vy = this.speed * Math.sin(this.angle * Math.PI / 180);
  255. this.gravity = 0;
  256. }
  257. shoot(power) {
  258. this.speed = power + Math.floor(Math.random() * 40);
  259. this.setAngle(270);
  260. }
  261. update(delta) {
  262. this.y += this.gravity * delta;
  263. this.gravity += 1500 * delta;
  264. this.x += this.vx * delta;
  265. this.y += this.vy * delta;
  266. if (this.vx > 500) this.vx = 500;
  267. if (this.vy > 500) this.vy = 500;
  268. if (this.y < 300) {
  269. this.solid = true;
  270. }
  271. if (this.gravity > this.speed) {
  272. this.falling = true;
  273. }
  274. if (this.x + 47 > 640) {
  275. this.vx = this.vx * -1;
  276. this.x = 640 - 47;
  277. }
  278. if (this.x - 47 < 0) {
  279. this.vx = this.vx * -1;
  280. this.x = 47;
  281. }
  282. this.drawAngle += this.angleVel * delta;
  283. }
  284. draw(ctx, game) {
  285. drawImage(
  286. ctx,
  287. game.res['/static/images/basketball/ball.png'],
  288. Math.floor(this.x - (93 / 2)),
  289. Math.floor(this.y - (93 / 2)),
  290. 0, 0, 93, 93, 93 / 2, 93 / 2,
  291. this.drawAngle
  292. );
  293. }
  294. }
  295. // 弹出文字类
  296. class PopText {
  297. constructor(string, x, y) {
  298. this.string = string;
  299. this.x = x;
  300. this.y = y;
  301. this.vy = -500;
  302. this.opacity = 1;
  303. }
  304. update(delta) {
  305. this.y += this.vy * delta;
  306. this.vy += 1000 * delta;
  307. if (this.vy > 0 && this.opacity > 0) {
  308. this.opacity -= 2 * delta;
  309. }
  310. if (this.opacity <= 0) {
  311. this.opacity = 0;
  312. }
  313. }
  314. draw(ctx, game) {
  315. ctx.globalAlpha = this.opacity;
  316. game.drawText(ctx, this.string, this.x + 15, this.y);
  317. ctx.globalAlpha = 1;
  318. }
  319. }
  320. // 工具函数:绘制旋转图像
  321. function drawImage(ctx, image, x, y, sx, sy, w, h, rx, ry, a) {
  322. ctx.save();
  323. ctx.translate(x + rx, y + ry);
  324. ctx.rotate(a * Math.PI / 180);
  325. ctx.drawImage(image, sx, sy, w, h, -rx, -ry, w, h);
  326. ctx.restore();
  327. }
  328. // 事件处理函数
  329. const handleMouseDown = () => {
  330. gameState.click = true;
  331. };
  332. const handleMouseUp = () => {
  333. gameState.click = false;
  334. };
  335. const handleTouchStart = () => {
  336. gameState.click = true;
  337. };
  338. const handleTouchEnd = () => {
  339. gameState.click = false;
  340. };
  341. // 游戏方法
  342. const setupCanvas = () => {
  343. gameState.canvas = document.getElementById('canvas');
  344. gameState.canvas.width = 640;
  345. gameState.canvas.height = 960;
  346. gameState.ctx = gameState.canvas.getContext('2d');
  347. };
  348. const resizeToWindow = () => {
  349. const w = gameState.canvas.width / gameState.canvas.height;
  350. const h = window.innerHeight;
  351. const ratio = h * w;
  352. gameState.canvas.style.width = Math.floor(ratio) + 'px';
  353. gameState.canvas.style.height = Math.floor(h) + 'px';
  354. };
  355. const drawLoadingScreen = () => {
  356. const ctx = gameState.ctx;
  357. ctx.fillStyle = 'black';
  358. ctx.fillRect(0, 0, 960, 640);
  359. ctx.textAlign = 'center';
  360. drawText(ctx, 'Loading...', 640 / 2, 960 / 2, 40);
  361. ctx.textAlign = 'left';
  362. };
  363. const getResources = () => {
  364. const images = [
  365. '/static/images/basketball/background.png',
  366. '/static/images/basketball/title.png',
  367. '/static/images/basketball/ball.png',
  368. '/static/images/basketball/hoop.png'
  369. ];
  370. const sounds = [
  371. '/static/images/basketball/bounce_1.wav'
  372. ];
  373. return gameState.sound ? images.concat(sounds) : images;
  374. };
  375. const drawText = (ctx, string, x, y, size = 30) => {
  376. ctx.font = size + 'px Contrail One';
  377. ctx.lineWidth = 5;
  378. ctx.strokeStyle = 'white';
  379. ctx.strokeText(string, x, y);
  380. ctx.fillStyle = '#0098BF';
  381. ctx.fillText(string, x, y);
  382. };
  383. const playSound = (name) => {
  384. if (gameState.sound && gameState.res[name]) {
  385. gameState.res[name].currentTime = 0;
  386. gameState.res[name].play().catch(e => console.log('Sound play error:', e));
  387. }
  388. };
  389. const loadResources = () => {
  390. drawLoadingScreen();
  391. const resources = getResources();
  392. let loaded = 0;
  393. return new Promise((resolve) => {
  394. resources.forEach(resource => {
  395. const type = resource.split('.').pop();
  396. if (type === 'png') {
  397. const image = new Image();
  398. image.src = resource;
  399. image.addEventListener('load', () => {
  400. loaded++;
  401. gameState.res[resource] = image;
  402. if (loaded === resources.length) resolve();
  403. });
  404. } else if (['wav', 'mp3'].includes(type)) {
  405. const sound = new Audio();
  406. sound.src = resource;
  407. sound.addEventListener('canplaythrough', () => {
  408. loaded++;
  409. gameState.res[resource] = sound;
  410. if (loaded === resources.length) resolve();
  411. });
  412. }
  413. });
  414. });
  415. };
  416. const gameLoop = (timestamp) => {
  417. if (!gameState.then) gameState.then = timestamp;
  418. const delta = (timestamp - gameState.then) / 1000;
  419. // 更新游戏状态
  420. update(delta);
  421. // 绘制游戏
  422. draw();
  423. gameState.then = timestamp;
  424. gameState.animationFrameId = requestAnimationFrame(gameLoop);
  425. };
  426. const update = (delta) => {
  427. if (gameState.state === 'menu') {
  428. if (gameState.click) {
  429. gameState.state = 'play';
  430. gameState.click = false;
  431. }
  432. return;
  433. }
  434. if (gameState.state === 'play') {
  435. // 更新篮球横向移动
  436. gameState.ballX += gameState.ballVel * delta;
  437. if (gameState.ballX > 640 - 93) {
  438. gameState.ballVel = -gameState.ballVel;
  439. gameState.ballX = 640 - 93;
  440. }
  441. if (gameState.ballX < 0) {
  442. gameState.ballVel = -gameState.ballVel;
  443. gameState.ballX = 0;
  444. }
  445. // 更新所有篮球
  446. for (let i = gameState.balls.length - 1; i >= 0; i--) {
  447. const ball = gameState.balls[i];
  448. if (ball.falling) {
  449. // 检测与篮筐的碰撞
  450. gameState.hoops.forEach(hoop => {
  451. const cx = hoop.x + (148 / 2);
  452. const cy = hoop.y + 40;
  453. const dx = cx - ball.x;
  454. const dy = cy - ball.y;
  455. const mag = Math.sqrt(dx * dx + dy * dy);
  456. if (mag < 47 + 5 && !ball.scored) {
  457. ball.setAngle(90);
  458. gameState.score += 100;
  459. gameState.texts.push(new PopText('+ 100', hoop.x, hoop.y));
  460. ball.scored = true;
  461. }
  462. if (!ball.scored) {
  463. hoop.points.forEach(point => {
  464. const dx = point.x - ball.x;
  465. const dy = point.y - ball.y;
  466. const mag = Math.sqrt(dx * dx + dy * dy);
  467. const angle = Math.atan2(point.y - ball.y, point.x - ball.x);
  468. if (mag > 47 + 7 && !ball.canBounce) {
  469. ball.canBounce = true;
  470. }
  471. if (mag < 47 + 5 && ball.canBounce) {
  472. playSound('/static/images/basketball/bounce_1.wav');
  473. ball.bounces++;
  474. ball.setAngle((angle * 180 / Math.PI) + 180 + Math.floor(Math.random() * 5) - Math.floor(Math.random() * 5));
  475. ball.bounces = Math.min(ball.bounces, 3);
  476. const deg = angle * 180 / Math.PI;
  477. if (deg > 0 && deg < 180) {
  478. ball.gravity = 750 + (ball.bounces * 50);
  479. }
  480. ball.angleVel = -ball.angleVel;
  481. ball.canBounce = false;
  482. }
  483. });
  484. }
  485. });
  486. }
  487. ball.update(delta);
  488. // 移除超出屏幕的球
  489. if (ball.y > 960) {
  490. gameState.ballX = ball.x;
  491. gameState.balls.splice(i, 1);
  492. }
  493. }
  494. // 更新计时器
  495. if (gameState.time > 0) {
  496. gameState.toNextSecond -= delta;
  497. if (gameState.toNextSecond <= 0) {
  498. gameState.time--;
  499. gameState.toNextSecond = 1;
  500. }
  501. } else {
  502. gameState.state = 'over';
  503. }
  504. // 处理投篮逻辑
  505. if (gameState.click && gameState.ballY <= 950) {
  506. if (gameState.balls.length < 1) {
  507. const ball = new Ball(gameState.ballX + (93 / 2), gameState.ballY);
  508. ball.drawAngle = gameState.ballAngle;
  509. ball.shoot(1480);
  510. gameState.balls.push(ball);
  511. gameState.ballY = 961;
  512. }
  513. }
  514. // 重置篮球位置
  515. if (gameState.balls.length < 1 && gameState.ballY > 880) {
  516. gameState.ballY -= 100 * delta;
  517. }
  518. if (!gameState.click) {
  519. gameState.ballsShot = 0;
  520. }
  521. // 更新弹出文字
  522. gameState.texts.forEach((text, i) => {
  523. text.update(delta);
  524. if (text.opacity <= 0) {
  525. gameState.texts.splice(i, 1);
  526. }
  527. });
  528. // 更新篮球角度
  529. gameState.ballAngle += 100 * delta;
  530. }
  531. // 游戏结束状态处理
  532. if (gameState.state === 'over' && gameState.click) {
  533. gameState.gameOver = false;
  534. gameState.started = false;
  535. gameState.score = 0;
  536. gameState.time = 60;
  537. gameState.balls = [];
  538. gameState.state = 'menu';
  539. gameState.click = false;
  540. }
  541. };
  542. const draw = () => {
  543. const ctx = gameState.ctx;
  544. if (!ctx) return;
  545. // 绘制背景
  546. if (gameState.res['/static/images/basketball/background.png']) {
  547. ctx.drawImage(gameState.res['/static/images/basketball/background.png'], 0, 0);
  548. }
  549. // 绘制菜单状态
  550. if (gameState.state === 'menu') {
  551. if (gameState.res['/static/images/basketball/title.png']) {
  552. ctx.drawImage(
  553. gameState.res['/static/images/basketball/title.png'],
  554. 640 / 2 - (492 / 2),
  555. 100
  556. );
  557. }
  558. ctx.textAlign = 'center';
  559. drawText(ctx, '请做投篮动作开始游戏', 640 / 2, 520, 40);
  560. drawText(ctx, '(?) Tap to throw ball. Try to make as many hoops before the time runs out!', 640 / 2, 940, 20);
  561. ctx.textAlign = 'left';
  562. }
  563. // 绘制游戏状态
  564. if (gameState.state === 'play') {
  565. // 绘制篮筐背景
  566. gameState.hoops.forEach(hoop => hoop.drawBack(ctx, gameState));
  567. // 绘制正在下落的球
  568. gameState.balls.forEach(ball => {
  569. if (ball.falling) ball.draw(ctx, gameState);
  570. });
  571. // 绘制篮筐前景
  572. gameState.hoops.forEach(hoop => hoop.drawFront(ctx, gameState));
  573. // 绘制未下落的球
  574. gameState.balls.forEach(ball => {
  575. if (!ball.falling) ball.draw(ctx, gameState);
  576. });
  577. // 绘制当前可投掷的球
  578. if (gameState.balls.length < 1 && gameState.res['/static/images/basketball/ball.png']) {
  579. drawImage(
  580. ctx,
  581. gameState.res['/static/images/basketball/ball.png'],
  582. gameState.ballX,
  583. gameState.ballY,
  584. 0, 0, 93, 93,
  585. 45, 45,
  586. gameState.ballAngle
  587. );
  588. }
  589. // 绘制分数和时间
  590. drawText(ctx, 'Score: ' + gameState.score, 45, 70, 50);
  591. drawText(ctx, 'Time: ' + gameState.time, 435, 70, 50);
  592. // 绘制弹出文字
  593. gameState.texts.forEach(text => text.draw(ctx, { drawText }));
  594. }
  595. // 绘制游戏结束界面
  596. if (gameState.state === 'over') {
  597. ctx.textAlign = 'center';
  598. drawText(ctx, 'Game Over', 640 / 2, 200, 80);
  599. drawText(ctx, 'Score: ' + gameState.score, 640 / 2, 400, 50);
  600. drawText(ctx, 'Click to Continue', 640 / 2, 800, 50);
  601. ctx.textAlign = 'center';
  602. }
  603. };
  604. // 初始化游戏
  605. const initGame = async () => {
  606. setupCanvas();
  607. resizeToWindow();
  608. window.addEventListener('resize', resizeToWindow);
  609. await loadResources();
  610. // 添加篮筐
  611. gameState.hoops = [
  612. new Hoop(110, 520),
  613. new Hoop(640 - 148 - 110, 520),
  614. new Hoop(640 / 2 - (148 / 2), 260)
  615. ];
  616. // 开始游戏循环
  617. gameState.animationFrameId = requestAnimationFrame(gameLoop);
  618. };
  619. // 生命周期钩子
  620. onMounted(() => {
  621. initGame();
  622. });
  623. onBeforeMount(() => {
  624. getInit();
  625. });
  626. onBeforeUnmount(() => {
  627. closeWS();
  628. if (gameState.animationFrameId) {
  629. cancelAnimationFrame(gameState.animationFrameId);
  630. }
  631. window.removeEventListener('resize', resizeToWindow);
  632. });
  633. </script>
  634. <style scoped>
  635. .game-container {
  636. width: 100vw;
  637. height: 100vh;
  638. display: flex;
  639. justify-content: center;
  640. align-items: center;
  641. background-color: #000;
  642. overflow: hidden;
  643. }
  644. #canvas {
  645. display: block;
  646. touch-action: none;
  647. user-select: none;
  648. }
  649. </style>