July 14th, 2021

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-анимацию можно использовать для создания простых игр и мультфильмов в вебе.

JavaScript: плющим мяч, язык SVG

Начало:
1. CSS: абсолютное позиционирование и содержащий блок
2. JavaScript: гоняю мяч по полю, CSS-анимация

В предыдущем посте при решении задачи «Передвиньте мяч по полю» к подразделу 2.1 «Введение в браузерные события» второй части учебника по JavaScript я написал, что наше решение универсально и будет работать при любых размерах HTML-элемента, представляющего мяч.

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

Если менять эти размеры так, чтобы ширина и высота мяча оставались равными друг другу, то есть чтобы мяч оставался круглым, то всё работает так, как следует. (Но, наверное, не стоит задавать слишком маленькие маленькие размеры, вроде 1px, тогда мяч будет плохо видно, либо слишком большие размеры, вроде 250px, тогда мяч будет виден (вернее, часть мяча будет видна), но его невозможно будет двигать, потому что размеры мяча превысят размеры поля.)

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



Хорошо, а что произойдет, если мяч не будет круглым? Зададим, например, ширину HTML-элемента, представляющего мяч, в 35px, а его высоту оставим равной 20px. Я временно задал в стиле мяча границу HTML-элемента с красным цветом указанием border: 1px solid red;, чтобы было понятно, где находится граница этого элемента. Вот что получилось у меня в браузере:



Как видно на картинке, HTML-элемент img, представляющий мяч, растянулся на заданную ширину (высота осталась прежней), но изображение мяча не растянулось. При этом наша функция moveBall для передвижения мяча по щелчку мыши работает так, как требуется по условиям задачи, потому что она, как уже говорилось выше, универсальна. Но из-за того, что мяч не растянулся на заданную ширину, создается неприятное впечатление, будто справа и слева от мяча есть невидимое пространство (если убрать из стиля мяча красную границу), не дающее мячу приблизиться впритык к правой и левой сторонам футбольного поля (с верхней и нижней сторонами поля всё в порядке).

В чём проблема? Дело в том, что изображение мяча для HTML-элемента img, представляющего мяч на заданной в задаче HTML-странице, берется из файла формата SVG:
<img src="https://ru.js.cx/clipart/ball.svg" id="ball">

В отличие от растровых форматов изображения, вроде JPG, GIF или PNG, файл формата SVG представляет из себя не бинарный, а текстовый файл. Аббревиатура «SVG» расшифровывается как «Scalable Vector Graphics», что по-русски значит «масштабируемая векторная графика». Файл формата SVG написан на языке разметки SVG, визуально похожем на язык HTML. В файле SVG изображение задается с помощью тегов, содержащих информацию о ключевых точках изображения и другую информацию. При масштабировании изображения в формате SVG, как следует из названия этого формата, изображение не теряет качества (не замыливается и не пикселизируется), как при масштабировании изображений в растровых форматах.

Если в нашем случае изображение мяча преобразовать из формата SVG в растровый формат, то при изменении размеров HTML-элемента img будет соответственно меняться и размер содержащегося в этом HTML-элементе растрового изображения. Проблем не будет.

В случае же изображения в формате SVG его поведение при машстабировании содержащего его HTML-элемента img регулируется из самого этого файла SVG.

Я открыл файл ball.svg в текстовом редакторе и увидел, что корневой тег представляет из себя следующее:
<svg width="40px" height="40px" viewBox="0 0 40 40" ...>
  <!-- ...другие теги... -->
</svg>
На месте многоточия в теге svg в файле есть и другие свойства, но я не стал их оттуда выписывать, так как для темы данного поста они не имеют значения.

Так как я не разбираюсь в языке SVG, то почитал в интернете и узнал, что в данном случае (если в теге svg присутствует свойство viewBox) поведение изображения при масштабировании содержащего его HTML-элемента можно регулировать с помощью свойства preserveAspectRatio в теге svg. Вот так:
<svg width="40px" height="40px" viewBox="0 0 40 40" preserveAspectRatio="none" ...>
  <!-- ...другие теги... -->
</svg>

Тут подробнее:
https://css-tricks.com/scale-svg/

Я подредактировал файл ball.svg так, как было указано выше, и запустил заданную в задаче HTML-страницу с нашим скриптом для передвижения мяча. Теперь всё работает так, как требуется:



Теперь мяч можно плющить хоть по горизонтали, хоть по вертикали и менять размеры, как угодно.

Красная династия в Китае и гибель старшего брата

По ссылке от Гоблина прочел интересную статью:

https://carnegie.ru/commentary/84936

Статья от 12.07.2021 года называется «Красная династия с цифровым стержнем: какую модель строит Компартия Китая». Автор — Александр Габуев, руководитель программы «Россия в Азиатско-Тихоокеанском регионе» Московского Центра Карнеги.

Удивительное дело: Гоблин ссылается на статью на сайте «Московского Центра Карнеги», аналитического центра, являющегося подразделением «Фонда Карнеги за международный мир», который, в свою очередь, является некоммерческой организацией, финансируемой из Вашингтона. Президент (теперь уже — бывший) этого фонда Уильям Дж. Бёрнс (с 2015 года по 18 марта 2021 года) только что (с 19 марта этого года) назначен директором ЦРУ.

Интересно, почему в публикациях на этом сайте не указано, что она имеет статус иностранного агента? Если она его не имеет, то как это может быть?

Еще удивительнее то, что сама статья оказалась очень интересной и мне понравилась. Тут почти отсутствуют обычные для западников нападки на коммунизм, СССР и современный Китай, всё очень взвешенно и аргументированно (много ссылок на статьи разных авторов на других сайтах, в том числе китайских, англоязычных и русских). Автор статьи Александр Габуев — молодец, его действительно можно назвать экспертом в его области, так мне кажется.

Со ссылки из указанной статьи перешел на еще одну статью, которую тоже прочел с интересом:

https://globalaffairs.ru/articles/gibel-starshego-brata/

Статья довольно старая, но, как мне кажется, не потерявшая актуальности. Дата ее публикации — 28.10.2012 года. Автор — тот же, Александр Габуев.

Статья называется «Гибель старшего брата». В ней рассказывается о том, как китайские товарищи изучали (и изучают до сих пор) опыт саморазрушения КПСС и падения СССР, чтобы уберечь свою КПК и свой Китай от тех же ошибок. Очень познавательно.