ilyachalov (ilyachalov) wrote,
ilyachalov
ilyachalov

Category:

JavaScript: гоняю мяч по полю, CSS-анимация

Начало: CSS: абсолютное позиционирование и содержащий блок.

Решил задачу «Передвиньте мяч по полю» к подразделу 2.1 «Введение в браузерные события» второй части учебника по JavaScript.

Задача очень интересная и не слишком сложная. Самое важное при решении этой задачи — понимать, где взять размеры и координаты HTML-элементов (и их частей) и в случае координат — относительно какой точки отсчета они считаются.

Разные методы и свойства могут выдавать координаты относительно, к примеру, левого верхнего угла 1) HTML-страницы (документа), 2) области просмотра браузера, 3) HTML-элемента, 4) клиентской части HTML-элемента.

Итак, у нас есть футбольное поле, представленное на заданной HTML-странице HTML-элементом div с идентификатором field, заданным с помощью свойства id тега div (см. предыдущий пост).

Пользователь может кликать мышкой по этому футбольному полю. Чтобы пользователю было видно, где на HTML-странице можно нажимать кнопку мыши, чтобы получить отклик, добавим в стиль футбольного поля CSS-свойство cursor:
cursor: pointer;
Теперь при наведении курсора мышью на футбольное поле курсор будет менять вид с обычной стрелки на руку с указующим перстом. Такое поведение курсора идентично его обычному поведению при наведении на ссылку на HTML-странице, отображаемой в браузере.

Теперь можно заметить любопытную особенность: рука с указующим перстом появляется не только над зеленой частью футбольного поля, но и над толстой черной границей вокруг футбольного поля. Считается, что в состав HTML-элемента входят его граница (border), внутренние отступы (padding) и его содержимое. Внешние отступы (margin) в состав HTML-элемента не входят, хоть значения этих отступов могут быть определены в стиле HTML-элемента.

Клиентской частью HTML-элемента считаются внутренние отступы (padding) и его содержимое. Граница (border) и полосы прокрутки (вертикальная и горизонтальная, если они есть) в клиентскую часть HTML-элемента не входят.

Для ясности я решил сделать четыре картинки с нашим футбольным полем, на которых красной штриховкой покажу, где и какая область находится. Для каждой области координаты отсчитываются относительно левого верхнего угла этой области.

Картинка 1. Область просмотра в браузере (по-английски «viewport»):



Картинка 2. HTML-элемент div, представляющий футбольное поле:



Картинка 3. Клиентская часть HTML-элемента div, представляющего футбольное поле:



Картинка 4. Часть клиентской части HTML-элемента div, представляющего футбольное поле. Это та часть футбольного поля, в которой может находиться центр футбольного мяча по условиям задачи. Условия задачи в данном случае такие: мяч не должен улетать за границы футбольного поля и не должен залезать на границу футбольного поля.



На картинке 4 расстояние между заштрихованной красной штриховкой областью и толстой черной границей футбольного поля составляет по вертикали половинку высоты мяча, а по горизонтали — половинку ширины мяча. Впрочем, если мяч круглый, то его высота и ширина равны его диаметру и равны между собой.

Приступим к решению задачи. Для начала повесим на событие click (щелчок основной кнопкой мыши) для HTML-элемента, представляющего футбольное поле, нашу функцию moveBall, которая будет обрабатывать указанное событие (будет передвигать мяч в место, в которое пользователь щелкнул мышью):
1.
field.onclick = moveBall;

function moveBall(event) {
   //...код функции...
}

В параметр event при клике мышью браузер передаст объект с данными о произошедшем событии, в частности — с координатами места, в которое пользователь щелкнул мышью. Извлечем из этого объекта нужные координаты и передвинем мяч в новое место:
2.
field.onclick = moveBall;

function moveBall(event) {
    // получим координаты клика мышью (относительно области просмотра)
    // это координаты нового места для мяча
    let x = event.clientX,
        y = event.clientY;

    //...вычисление правильных координат...

    // передвинем мяч на новое место
    ball.style.left = x + "px";
    ball.style.top = y + "px";
}

Мы, конечно, можем оставить код в таком виде и он будет работать без формальных ошибок. Однако, в браузере мяч будет перемещаться не туда, куда рассчитывает пользователь.

Дело в том, что у нас стили настроены таким образом (см. предыдущий пост), что координаты left и top отсчитываются относительно левого верхнего угла клиентской части HTML-элемента div, представляющего футбольное поле (картинка 3 выше), а из объекта event мы получаем координаты clientX и clientY, которые отсчитываются относительно левого верхнего угла области просмотра браузера (картинка 1 выше).

Меняем код:
3.
field.onclick = moveBall;

function moveBall(event) {
    // получим координаты клика мышью (относительно области просмотра)
    // это координаты нового места для мяча
    let x = event.clientX,
        y = event.clientY;

    // получим координаты поля (относительно области просмотра)
    let coordField = field.getBoundingClientRect();
    
    // преобразуем координаты нового места для мяча
    //   из координат относительно области просмотра
    //   в координаты относительно клиентской части поля
    x = x - coordField.x - field.clientLeft;
    y = y - coordField.y - field.clientTop;

    //...дополнительные вычисления...

    // передвинем мяч на новое место
    ball.style.left = x + "px";
    ball.style.top = y + "px";
}

Здесь вычитанием координат coordField.x и coordField.y мы преобразуем координаты относительно области просмотра в координаты относительно HTML-элемента div, представляющего футбольное поле. А дополнительным вычитанием значений field.clientLeft и field.clientTop (это толщина границы футбольного поля в сумме с полосой прокрутки, если она есть и примыкает к верхней или левой границе футбольного поля) мы преобразуем координаты относительно HTML-элемента div, представляющего футбольное поле, в координаты относительно клиентской части этого HTML-элемента (см. картинки выше для ясности).

Теперь код работает уже лучше, но пока еще не выполняются условия задачи: мяч не должен вылезать на или за границу футбольного поля.

Чтобы учесть эти условия, вычислим координаты (относительно левого верхнего угла клиентской части HTML-элемента, представляющего футбольное поле) той части клиентской части HTML-элемента div, представляющего футбольное поле, в которой центру мяча разрешено появляться (картинка 4 выше). Эта область представляет собой прямоугольник, для определения которого достаточно знать координаты левого верхнего угла этого прямоугольника и координаты правого нижнего угла этого прямоугольника:
    // координаты части клиентской части поля (относительно клиентской части поля),
    // на которой центру мяча разрешено появляться (чтобы он не вылез
    // на или за границу поля)
    let coordFieldX_left  = ball.offsetWidth / 2,
        coordFieldY_left  = ball.offsetHeight / 2,
        coordFieldX_right = field.clientWidth - ball.offsetWidth / 2,
        coordFieldY_right = field.clientHeight - ball.offsetHeight / 2;

А теперь проверим имеющиеся у нас координаты нового места мяча на попадание в этот разрешенный прямоугольник и, если координаты нового места мяча не попадают в него, то координаты нового места мяча будем коорректировать так, чтобы они попали в разрешенный прямоугольник:
    // скорректируем координаты нового места для мяча, если при этих координатах
    // мяч вылазит на или за границу поля
    if (x < coordFieldX_left)  x = coordFieldX_left;
    if (y < coordFieldY_left)  y = coordFieldY_left;
    if (x > coordFieldX_right) x = coordFieldX_right;
    if (y > coordFieldY_right) y = coordFieldY_right;

Ну и после всего этого, напоследок, нужно не забыть, что при перемещении мяча в новое место мы перемещаем HTML-элемент img, представляющий мяч. При этом в новое место попадает не центр HTML-элемента img, а его левый верхний угол. Так как нам нужно, чтобы в новое место для мяча попадал именно центр мяча, то необходимо произвести коррекцию:
    // скорректируем координаты так, чтобы в место клика мышью попал центр мяча,
    // а не левый верхний угол HTML-элемента, представляющего мяч
    x -= ball.offsetWidth / 2;
    y -= ball.offsetHeight / 2;

Окончательный вариант решения:
4.
field.onclick = moveBall;

function moveBall(event) {
    // получим координаты клика мышью (относительно области просмотра)
    // это координаты нового места для мяча
    let x = event.clientX,
        y = event.clientY;

    // получим координаты поля (относительно области просмотра)
    let coordField = field.getBoundingClientRect();
    
    // преобразуем координаты нового места для мяча
    //   из координат относительно области просмотра
    //   в координаты относительно клиентской части поля
    x = x - coordField.x - field.clientLeft;
    y = y - coordField.y - field.clientTop;

    // координаты части клиентской части поля (относительно клиентской части поля),
    // на которой центру мяча разрешено появляться (чтобы он не вылез
    // на или за границу поля)
    let coordFieldX_left  = ball.offsetWidth / 2,
        coordFieldY_left  = ball.offsetHeight / 2,
        coordFieldX_right = field.clientWidth - ball.offsetWidth / 2,
        coordFieldY_right = field.clientHeight - ball.offsetHeight / 2;

    // скорректируем координаты нового места для мяча, если при этих координатах
    // мяч вылазит на или за границу поля
    if (x < coordFieldX_left)  x = coordFieldX_left;
    if (y < coordFieldY_left)  y = coordFieldY_left;
    if (x > coordFieldX_right) x = coordFieldX_right;
    if (y > coordFieldY_right) y = coordFieldY_right;

    // скорректируем координаты так, чтобы в место клика мышью попал центр мяча,
    // а не левый верхний угол HTML-элемента, представляющего мяч
    x -= ball.offsetWidth / 2;
    y -= ball.offsetHeight / 2;

    // передвинем мяч на новое место
    ball.style.left = x + "px";
    ball.style.top = y + "px";
}

Отмечу, что авторы задачи в своем решении добавили в стиль мяча ширину и высоту (это ширина и высота HTML-элемента img, представляющего мяч). Это сделано не просто так. Причина этого уже разбиралась в задачах ранее. Если нигде не будут указаны размеры HTML-элемента img, представляющего мяч, то при первой загрузке (Ctrl+F5) заданной HTML-страницы браузер не будет знать размеров мяча, что может привести к неверным вычислениям. При последующих загрузках (F5) заданной HTML-страницы картинка с мячом уже будет сохранена в кэше браузера и браузер сможет взять размеры мяча оттуда, таким образом вычисления в нашей функции будут выполнены правильно. Таким образом, указание ширины и высоты HTML-элемента в стиле мяча обеспечивает правильную работу нашей функции и при первой загрузке (Ctrl+F5) заданной HTML-страницы:
  #ball {                      /* стиль мяча */
    position: absolute;        /* позиционируем мяч относительно поля */
    width: 40px;
    height: 40px;
  }

Но наше решение универсально и будет работать при любых размерах HTML-элемента, представляющего мяч. Например, я указал ширину и высоту мяча 20px в стиле мяча и всё работает так, как требуется.

CSS-анимация

В демонстрационном примере решения от авторов задачи (можно посмотреть тут) мяч не прыгает резко с одного места на другое, как в нашем решении задачи, а перемещается плавно. Такое перемещение называют «анимацией» и она может обеспечиваться несколькими способами. Например:

С помощью CSS-свойства animation и CSS-оператора @keyframes:
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations

С помощью CSS-свойства transition:
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions

Авторы задачи для анимации используют CSS-свойство transition. Слово «transition» переводится с английского на русский язык как «переход». Имеется в виду переход HTML-элемента из одного состояния в другое. Состояние HTML-элемента определяется значениями его CSS-свойств. То есть в данном случае анимация автоматически выполняется браузером при изменении CSS-свойств HTML-элемента.

Наша функция, передвигающая мяч, изменяет CSS-свойства left и top HTML-элемента img, представляющего мяч. При этом браузер может автоматически обеспечить анимацию движения мяча, если описать CSS-свойство transition. К примеру, вот так:
  #ball {                      /* стиль мяча */
    position: absolute;        /* позиционируем мяч относительно поля */
    width: 40px;
    height: 40px;
    transition: all 1s;        /* включить анимацию */
  }

Значение all означает, что анимация будет применена браузером при изменении любого из CSS-свойств HTML-элемента, представляющего мяч. Значение 1s определяет продолжительность анимации, то есть время, в течение которого мяч будет плавно перемещаться из первоначального места в новое место (в данном случае — это 1 секунда).

С анимацией в данном случае есть одна тонкость. В описанной выше редакции стиля мяча при использовании нашей функции, передвигающей мяч, первое передвижение мяча не будет анимировано (по крайней мере, в моём браузере «Microsoft Edge» на движке «Chromium» это так). А последующие передвижения мяча уже будут анимированы.

Это происходит потому, что в данном случае значения CSS-свойств left и top первоначально не указаны. По правилам языка CSS в данном случае (см. предыдущий пост) по умолчанию эти CSS-свойства равны значению auto. Как я понимаю, браузер не знает, как обеспечить анимацию при переходе от значения auto в числовое значение, и поэтому вообще ее не выполняет.

Чтобы исправить эту недоработку, укажем первоначальные значения CSS-свойств left и top в стиле мяча:
  #ball {                      /* стиль мяча */
    position: absolute;        /* позиционируем мяч относительно поля */
    left: 0;
    top: 0;
    width: 40px;
    height: 40px;
    transition: all 1s;        /* включить анимацию */
  }
Это окончательная редакция стиля мяча для данной задачи.

CSS-анимация — это, как я понимаю, одна из причин смерти технологии «Flash», которая ранее использовалась для анимации в веб-приложениях. Поддержка технологии «Flash» была прекращена 31 декабря прошлого 2020 года (подробнее см. тут). В принципе, CSS-анимацию можно использовать для создания простых игр и мультфильмов в вебе.
Tags: Образование, Программирование
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments