diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..6784004 --- /dev/null +++ b/src/config.js @@ -0,0 +1,12 @@ +export const CONTAINER_WIDTH = 800; +export const CONTAINER_HEIGHT = 600; + +export const PADDLE_WIDTH = 50; +export const PADDLE_HEIGHT = 10; + +export const BALL_RADIUS = 10; +export const BALL_SPEED = 3; +export const BALL_INITIAL_ANGLE = 180; + +export const BRICK_WIDTH = 40; +export const BRICK_HEIGHT = 10; diff --git a/src/lib/calculateCollision/calculateCollision.js b/src/lib/calculateCollision/calculateCollision.js new file mode 100644 index 0000000..1ed41b9 --- /dev/null +++ b/src/lib/calculateCollision/calculateCollision.js @@ -0,0 +1,51 @@ +/** + * Вычисляет пересеклись ли два объекта по ААBB формуле в системе координат где ось X идет справа налево, ось Y идет сверху вних + * @param {Object} firstObject - первый объект + * @param {number} firstObject.left - Min X координата первого объекта + * @param {number} firstObject.right - Max X координата первого объекта + * @param {number} firstObject.top - Min Y координата первого объекта (ось смотрит вниз) + * @param {number} firstObject.bottom - Max Y координата первого объекта + * @param {Object} secondObject - второй объект + * @param {number} secondObject.left - Min X координата второго объекта + * @param {number} secondObject.right - Max X координата второго объекта + * @param {number} secondObject.top - Min Y координата второго объекта (ось смотрит вниз) + * @param {number} secondObject.bottom - Max Y координата второго объекта + * @returns {boolean} + */ +export function calculateCollision(firstObject, secondObject) { + try { + const coordinates = [ + firstObject.left, + firstObject.right, + firstObject.top, + firstObject.bottom, + secondObject.left, + secondObject.right, + secondObject.top, + secondObject.bottom, + ]; + + if (coordinates.some((element) => typeof element !== 'number')) { + throw new Error('Координаты должны являться числовыми значениями'); + } + + if ( + firstObject.left > firstObject.right || + firstObject.top > firstObject.bottom || + secondObject.left > secondObject.right || + secondObject.top > secondObject.bottom + ) { + throw new Error('Координаты должны быть корректными'); + } + + return ( + firstObject.left <= secondObject.right && + firstObject.right >= secondObject.left && + firstObject.top <= secondObject.bottom && + firstObject.bottom >= secondObject.top + ); + } catch (err) { + console.error(err); + return null; + } +} diff --git a/src/lib/calculateCollision/calculateCollision.test.js b/src/lib/calculateCollision/calculateCollision.test.js new file mode 100644 index 0000000..c2581cf --- /dev/null +++ b/src/lib/calculateCollision/calculateCollision.test.js @@ -0,0 +1,129 @@ +import { calculateCollision } from './calculateCollision'; + +describe('calculateCollision', () => { + it('Корректно обрабатывает невозможные кейсы (левая координата больше правой)', () => { + const firstObject = { + left: 0, + right: 20, + top: 0, + bottom: 20, + }; + + const secondObject = { + left: 10, + right: 0, + top: 0, + bottom: 10, + }; + + expect(calculateCollision(firstObject, secondObject)).toBeNull(); + }); + + it('Корректно обрабатывает неверный формат данных', () => { + const firstObject = { + left: 'wrong', + right: 20, + top: 0, + bottom: 20, + }; + + const secondObject = { + left: 0, + right: 10, + top: 0, + bottom: 10, + }; + + expect(calculateCollision(firstObject, secondObject)).toBeNull(); + }); + + it('Корректно обрабатывает отсутствие пересечения по X', () => { + const firstObject = { + left: 0, + right: 20, + top: 0, + bottom: 20, + }; + + const secondObject = { + left: 25, + right: 30, + top: 0, + bottom: 10, + }; + + expect(calculateCollision(firstObject, secondObject)).toBe(false); + }); + + it('Корректно обрабатывает отсутствие пересечения по Y', () => { + const firstObject = { + left: 0, + right: 20, + top: 0, + bottom: 20, + }; + + const secondObject = { + left: 0, + right: 30, + top: 25, + bottom: 30, + }; + + expect(calculateCollision(firstObject, secondObject)).toBe(false); + }); + + it('Корректно обрабатывает отсутствие пересечения по X и Y', () => { + const firstObject = { + left: 0, + right: 20, + top: 0, + bottom: 20, + }; + + const secondObject = { + left: 25, + right: 30, + top: 25, + bottom: 30, + }; + + expect(calculateCollision(firstObject, secondObject)).toBe(false); + }); + + it('Корректно обрабатывает пересечение', () => { + const firstObject = { + left: 0, + right: 20, + top: 0, + bottom: 20, + }; + + const secondObject = { + left: 10, + right: 30, + top: 10, + bottom: 30, + }; + + expect(calculateCollision(firstObject, secondObject)).toBe(true); + }); + + it('Корректно обрабатывает вхождение', () => { + const firstObject = { + left: 0, + right: 20, + top: 0, + bottom: 20, + }; + + const secondObject = { + left: 10, + right: 15, + top: 10, + bottom: 15, + }; + + expect(calculateCollision(firstObject, secondObject)).toBe(true); + }); +}); diff --git a/src/lib/calculateDirection/calculateDirection.js b/src/lib/calculateDirection/calculateDirection.js new file mode 100644 index 0000000..5ebf113 --- /dev/null +++ b/src/lib/calculateDirection/calculateDirection.js @@ -0,0 +1,52 @@ +import { calculateCollision } from '../calculateCollision/calculateCollision'; + +/** + * Вычисляет направление наибольшего пересечения по осям и возвращает tuple множителей для изменения координат + * @param {Object} firstObject - первый объект + * @param {number} firstObject.left - Min X координата первого объекта + * @param {number} firstObject.right - Max X координата первого объекта + * @param {number} firstObject.top - Min Y координата первого объекта (ось смотрит вниз) + * @param {number} firstObject.bottom - Max Y координата первого объекта + * @param {Object} secondObject - второй объект + * @param {number} secondObject.left - Min X координата второго объекта + * @param {number} secondObject.right - Max X координата второго объекта + * @param {number} secondObject.top - Min Y координата второго объекта (ось смотрит вниз) + * @param {number} secondObject.bottom - Max Y координата второго объекта + * @returns {Array} tuple формата [1, -1] с множителями для осей X и Y. Каждый может принимать значение либо 1, либо -1 + */ +export function calculateDirection(firstObject, secondObject) { + try { + // Запускаем для проверки формата аргументов + const isCollided = calculateCollision(firstObject, secondObject); + + if (isCollided === null) { + return null; + } + + if (!isCollided) { + return [1, 1]; + } + + // Смотрим по какой оси значение пересечения объектов больше и выбираем множитель по + const valueX = Math.min(firstObject.right, secondObject.right) - Math.max(firstObject.left, secondObject.left); + const valueY = Math.min(firstObject.bottom, secondObject.bottom) - Math.max(firstObject.top, secondObject.top); + + switch (true) { + // TODO: добавить эпсилон для сравнения + case valueX === valueY: + return [-1, -1]; + + case valueX > valueY: + return [1, -1]; + + case valueX < valueY: + return [-1, 1]; + + default: + return [1, 1]; + } + } catch (err) { + console.error(err); + return null; + } +} diff --git a/src/lib/calculateDirection/calculateDirection.test.js b/src/lib/calculateDirection/calculateDirection.test.js new file mode 100644 index 0000000..879c1f0 --- /dev/null +++ b/src/lib/calculateDirection/calculateDirection.test.js @@ -0,0 +1,111 @@ +import { calculateDirection } from './calculateDirection'; + +describe('calculateDirection', () => { + it('Корректно обрабатывает невозможные кейсы (левая координата больше правой)', () => { + const firstObject = { + left: 0, + right: 20, + top: 0, + bottom: 20, + }; + + const secondObject = { + left: 10, + right: 0, + top: 0, + bottom: 10, + }; + + expect(calculateDirection(firstObject, secondObject)).toBeNull(); + }); + + it('Корректно обрабатывает неверный формат данных', () => { + const firstObject = { + left: 'wrong', + right: 20, + top: 0, + bottom: 20, + }; + + const secondObject = { + left: 0, + right: 10, + top: 0, + bottom: 10, + }; + + expect(calculateDirection(firstObject, secondObject)).toBeNull(); + }); + + it('Возвращает корректные множители для кейса с отсутствием коллизии', () => { + const firstObject = { + left: 0, + right: 20, + top: 0, + bottom: 20, + }; + + const secondObject = { + left: 30, + right: 40, + top: 30, + bottom: 40, + }; + + expect(calculateDirection(firstObject, secondObject)).toEqual([1, 1]); + }); + + it('Возвращает корректные множители для коллизии по оси Y', () => { + const firstObject = { + left: 0, + right: 20, + top: 0, + bottom: 20, + }; + + const secondObject = { + left: 5, + right: 20, + top: 15, + bottom: 25, + }; + + expect(calculateDirection(firstObject, secondObject)).toEqual([1, -1]); + }); + + it('Возвращает корректные множители для коллизии по оси X', () => { + const firstObject = { + left: 0, + right: 20, + top: 0, + bottom: 20, + }; + + const secondObject = { + left: 15, + right: 20, + top: 5, + bottom: 25, + }; + + expect(calculateDirection(firstObject, secondObject)).toEqual([-1, 1]); + }); + + it('Возвращает корректные множители для коллизии по осям X и Y', () => { + const firstObject = { + left: 0, + right: 20, + top: 0, + bottom: 20, + }; + + const secondObject = { + left: 15, + right: 25, + top: 15, + bottom: 25, + }; + + expect(calculateDirection(firstObject, secondObject)).toEqual([-1, -1]); + }); +}); diff --git a/src/main.js b/src/main.js index 20b064f..9754c58 100644 --- a/src/main.js +++ b/src/main.js @@ -1,45 +1,129 @@ import './style.css'; -import { Application, Assets, Container, Sprite } from 'pixi.js'; +import { Application, Assets, Container, Graphics, Sprite } from 'pixi.js'; +import { + BALL_INITIAL_ANGLE, + BALL_RADIUS, + BALL_SPEED, + BRICK_HEIGHT, + BRICK_WIDTH, + CONTAINER_HEIGHT, + CONTAINER_WIDTH, + PADDLE_HEIGHT, + PADDLE_WIDTH, +} from './config'; +import { calculateCollision } from './lib/calculateCollision/calculateCollision'; +import { calculateDirection } from './lib/calculateDirection/calculateDirection'; (async () => { // Create a new application const app = new Application(); // Initialize the application - await app.init({ background: '#1099bb', resizeTo: window }); + await app.init({ background: '#1099bb', width: CONTAINER_WIDTH, height: CONTAINER_HEIGHT }); // Append the application canvas to the document body document.body.appendChild(app.canvas); // Create and add a container to the stage - const container = new Container(); + const container = new Container({ + eventMode: 'static', + hitArea: app.screen, + }); + + container.x = 0; + container.y = 0; app.stage.addChild(container); - // Load the bunny texture - const texture = await Assets.load('https://pixijs.com/assets/bunny.png'); + const paddle = new Graphics().rect(0, CONTAINER_HEIGHT - PADDLE_HEIGHT, PADDLE_WIDTH, PADDLE_HEIGHT).fill('#fff000'); + container.addChild(paddle); - // Create a 5x5 grid of bunnies in the container - for (let i = 0; i < 25; i++) { - const bunny = new Sprite(texture); + container.on('pointermove', (event) => { + const localPosition = container.toLocal(event.global); - bunny.x = (i % 5) * 40; - bunny.y = Math.floor(i / 5) * 40; - container.addChild(bunny); - } + if (localPosition.x < CONTAINER_WIDTH - PADDLE_WIDTH) { + paddle.x = localPosition.x; + } + }); - // Move the container to the center - container.x = app.screen.width / 2; - container.y = app.screen.height / 2; + const bricksRow = Array.from({ length: Math.floor(CONTAINER_WIDTH / BRICK_WIDTH) }).map((_, index) => { + const brick = new Graphics().rect(0, 0, BRICK_WIDTH, BRICK_HEIGHT).fill('#000fff'); + brick.x = index * BRICK_WIDTH; + brick.y = 1; + container.addChild(brick); + return brick; + }); - // Center the bunny sprites in local container coordinates - container.pivot.x = container.width / 2; - container.pivot.y = container.height / 2; + const ball = new Graphics().circle(0, 0, BALL_RADIUS).fill('#ffffff'); + ball.x = 100; + ball.y = 100; + container.addChild(ball); + + const leftBoundary = BALL_RADIUS; + const rightBoundary = CONTAINER_WIDTH - BALL_RADIUS; + const topBoundary = BALL_RADIUS; + const bottomBoundary = CONTAINER_HEIGHT - BALL_RADIUS; + const paddleTop = CONTAINER_HEIGHT - PADDLE_HEIGHT; + const bricksBottom = BRICK_HEIGHT; + + let horizontalSpeed = BALL_SPEED * Math.cos(BALL_INITIAL_ANGLE); + let verticalSpeed = -1 * BALL_SPEED * Math.sin(BALL_INITIAL_ANGLE); - // Listen for animate update app.ticker.add((time) => { - // Continuously rotate the container! - // * use delta to create frame-independent transform * - container.rotation -= 0.01 * time.deltaTime; + // Базоввое взаимодействие мяча и кирпича + for (let i = bricksRow.length - 1; i >= 0; i--) { + const currentBrick = bricksRow[i]; + const brickLeft = currentBrick.x; + const brickRight = currentBrick.x + BRICK_WIDTH; + const brickTop = currentBrick.y; + const brickBottom = currentBrick.y + BRICK_HEIGHT; + + const ballLeft = ball.x - BALL_RADIUS; + const ballRight = ball.x + BALL_RADIUS; + const ballTop = ball.y - BALL_RADIUS; + const ballBottom = ball.y + BALL_RADIUS; + + const ballObject = { left: ballLeft, right: ballRight, top: ballTop, bottom: ballBottom }; + const brickObject = { left: brickLeft, right: brickRight, top: brickTop, bottom: brickBottom }; + + const isCollided = calculateCollision(ballObject, brickObject); + const directions = calculateDirection(ballObject, brickObject); + + if (isCollided && directions !== null) { + horizontalSpeed *= directions[0]; + verticalSpeed *= directions[1]; + + container.removeChild(currentBrick); + currentBrick.destroy(); + bricksRow.splice(i, 1); + + break; + } + } + + // Не даем мячу выйти за границы стен слева / справав и меняем направление + if (ball.x <= leftBoundary || ball.x >= rightBoundary) { + ball.x = ball.x <= leftBoundary ? leftBoundary : rightBoundary; + horizontalSpeed *= -1; + } + + // Не даем мячу выйти за границы стен сверху / снизу и меняем направление + if (ball.y <= topBoundary || ball.y >= bottomBoundary) { + ball.y = ball.y <= topBoundary ? topBoundary : bottomBoundary; + verticalSpeed *= -1; + } + + // Базоввое взаимодействие мяча и ракетки + if ( + verticalSpeed > 0 && + ball.y + BALL_RADIUS >= paddleTop && + ball.x >= paddle.x && + ball.x <= paddle.x + PADDLE_WIDTH + ) { + verticalSpeed *= -1; + } + + ball.x += horizontalSpeed * time.deltaTime; + ball.y += verticalSpeed * time.deltaTime; }); })(); diff --git a/vitest.config.js b/vitest.config.js index d7d6032..f63332c 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -4,5 +4,6 @@ export default defineConfig({ test: { environment: 'jsdom', include: ['src/**/*.test.js'], + globals: true, }, });