JavaScript: бесконечная страница

Решил задачу «Бесконечная страница» к подразделу 3.6 «Прокрутка» второй части учебника по JavaScript.

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

В теле заданной HTML-страницы (в песочнице) содержится лишь HTML-элемент h1 (заголовок), содержащий текст «Прокрути меня». (Еще там содержится HTML-элемент script, но, так как он не отображается в браузере, в данном случае его присутствие не имеет значения.)

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

Что делать? Я решил в своем скрипте сначала увеличить высоту HTML-страницы, добавив в нее столько строк с текущими датой и временем, сколько потребуется, чтобы высота HTML-страницы превысила высоту области просмотра браузера. При этом должна автоматически появиться вертикальная полоса прокрутки HTML-страницы. Пишем код:
// высота HTML-страницы
let htmlHeight = document.documentElement.offsetHeight;
// высота клиентской части области просмотра браузера
let viewportHeight = document.documentElement.clientHeight;

// добавляем текущие дату и время в тело HTML-страницы,
// пока ее высота не станет больше высоты области просмотра браузера
while (htmlHeight <= viewportHeight) {
    let p = document.createElement("p"); // создаем параграф с текущими датой и временем
    p.innerHTML = new Date();            // и добавляем его в тело HTML-страницы
    document.body.append(p);
                                         // заново определяем высоту HTML-страницы
    htmlHeight = document.documentElement.offsetHeight;
}

(При определении высоты HTML-страницы в скрипте на языке JavaScript следует внимательно следить за стилями HTML-элементов, описанными на языке CSS. Некоторые указания в стилях на языке CSS могут повлиять на работу свойств объектов в JavaScript. Например, если описать для HTML-элемента html стиль html {height: 100%;}, то высота HTML-элемента html всегда будет равна высоте области просмотра браузера, независимо от содержания HTML-элемента html. То есть свойство offsetHeight для этого HTML-элемента постоянно будет равно свойству clientHeight, даже если в браузере мы наблюдаем, что содержимое HTML-элемента html (например, какой-то текст в теле HTML-страницы) занимает меньше высоты области просмотра браузера или, наоборот, больше высоты области просмотра браузера. Это будет показано на практике в следующем посте.)

У меня этот код сработал так, как я и планировал. Первоначально у меня в браузере (развернутом на весь экран компьютера) высота заданной HTML-страницы (с одним заголовком «Прокрути меня») составляет 80 пикселей, а высота клиентской части (то есть за вычетом места, которое занимает горизонтальная полоса прокрутки, если она присутствует) области просмотра браузера составляет 891 пиксель. Описанный выше скрипт добавляет в тело заданной HTML-страницы 24 параграфа с текущими датой и временем, после чего высота HTML-страницы становится равной 896 пикселям. Так как в результате высота HTML-страницы (896 пикселей) стала больше, чем высота клиентской части области просмотра браузера (891 пиксель), то автоматически появилась вертикальная полоса прокрутки.

Теперь нужно отловить момент, когда пользователь прокрутит полученную HTML-страницу вниз до конца. Для этого повесим функцию-обработчик на событие scroll на окне браузера window. Дополним вышеописанный код. Я не будут переписывать заново описанную выше часть, чтобы не увеличивать понапрасну размер поста, просто буду иметь в виду, что нижеописанный код добавляется к вышеописанному.
window.addEventListener("scroll", function() {
    //...
});

По мере прокрутки HTML-страницы вниз браузер будет генерировать множество событий scroll. При этом будет увеличиваться сдвиг HTML-страницы pageYOffset. Как определить, что HTML-страница сдвинута вниз до самого конца? Сдвиг HTML-страницы pageYOffset должен стать равен максимально возможному сдвигу HTML-страницы при текущей высоте HTML-страницы и текущей высоте клиентской части области просмотра браузера. Этот максимальный сдвиг я обозначил идентификатором maxYOffset. Дополним код:
window.addEventListener("scroll", function() {

    // максимально возможный на данный момент сдвиг HTML-страницы по вертикали
    let maxYOffset = document.documentElement.offsetHeight -
                     document.documentElement.clientHeight;
    
    // если достигнут максимально возможный сдвиг,
    if (pageYOffset == maxYOffset) {
        let p = document.createElement("p"); // создаем параграф с текущими датой и временем
        p.innerHTML = new Date();            // и добавляем его в тело HTML-страницы
        document.body.append(p);
    }

});

Вот и всё. Этот скрипт у меня работает: при прокрутке HTML-страницы вниз бесконечно добавляются параграфы с текущими датой и временем.

С распадом СССР левая идея не умерла

По этому поводу можно написать несколько книг, но я к этому не готов, просто хочу чиркнуть пару строк.

Что я подразумеваю под «левой идеей»? По этому поводу можно почитать статью «Левые» в википедии. Левую идею можно найти практически во всех современных политических партиях и течениях, разница в степени левизны.

Лично я не являюсь безусловным сторонником какой-то одной политической идеи, в том числе левой или правой. Я считаю, что все имеющиеся идеи следует рассматривать и совмещать, конструируя для каждой страны свою смесь (в принципе, на практике так и происходит).

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

Родившиеся при СССР (я в том числе) пока еще «перемёрли» далеко не все, но уже сейчас становится видно, что левая идея не умерла, она стала востребованной у новых поколений.

Как оказалось, проблема (если это считать проблемой) была вовсе не в «совках» (википедия), как нас называют недоброжелатели. «Совки» жили при советской власти и могут со знанием дела указать как на ее плюсы, так и на ее минусы. Другое дело — сегодняшние молодые (новые) левые. Многие из них склонны идеализировать левую идею (СССР при этом считается чем-то плохим, так их научили в школе, противоречия они тут не видят).

Сейчас сегодняшние «западники» (назовем их так по аналогии с западниками XIX века) внезапно как-будто бы проснулись, увидели молодых левых и ужаснулись (например, ужаснулись при появлении движения «BLM» в США или тому, что в Европе власть стали брать «Зелёные», но апплодируют партии Навального, не замечая, что Навальный довольно сильно склонен к левой повестке).

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

JavaScript: одновременное нажатие клавиш

Разбираю своё решение задачи «Отследить одновременное нажатие» к подразделу 3.4 «Клавиатура: keydown и keyup» второй части учебника по JavaScript.

Требуется написать функцию с названием runOnKeys. В постановке задачи сказано, что она должна запускать другую функцию (переданную ей в первом параметре func) при одновременном нажатии на клавиатуре нескольких клавиш (предполагается, что их количество может быть любым) с кодами, переданными ей в параметрах, начиная со второго. Приведен пример вызова этой функции:
runOnKeys(
  () => alert("Привет!"),
  "KeyQ",
  "KeyW"
);

Видимо, авторы задачи запутались в формулировке постановки задачи. На самом деле, функция runOnKeys будет запускаться на HTML-странице, но она не будет запускать переданную ей в первом параметре функцию func. Я это понял, когда обдумывал способы решения этой задачи.

Запуск функции runOnKeys должен придавать HTML-странице способность выполнять функцию func при одновременном нажатии заданных клавиш на клавиатуре. Собственно, запуск функции func будет производить пользователь, одновременно нажав на заданные клавиши на клавиатуре.

Как функция runOnKeys придаст HTML-странице (document) эту способность? Она должна повесить соответствующую функцию-обработчик на какие-то события, которые произойдут на HTML-странице. Так как одновременное нажатие на несколько клавиш на клавиатуре вызовет событие keydown, значит, функцию-обработчик следует вешать именно на это событие. Пишем код:
function runOnKeys() {

    // вешаем функцию-обработчик на событие "keydown" на HTML-странице
    document.addEventListener("keydown", function (event) {
        //...
    });

}

По идее, чтобы начать обработку одновременного нажатия нескольких клавиш на клавиатуре, нам нужно знать, когда последовательность нажатия нескольких клавиш закончится. Мы можем начать обработку после нажатия последней клавиши в последовательности. Но в функции-обработчике, повешенной на событие keydown, мы не можем это отследить, потому что эта функция-обработчик обрабатывает каждый раз только нажатие одной из клавиш в последовательности, она не имеет информации о том, последняя ли это клавиша в последовательности или нет.

Когда мы сможем понять, что последовательность одновременного нажатия клавиш окончилась? Когда пользователь начнет отпускать клавиши, то есть при генерации первого события keyup после одновременного нажатия клавиш. Значит, нам нужно отслеживать и отпускание клавиш, то есть повесить функцию-обработчик на событие keyup. Дополним код:
function runOnKeys() {

    // вешаем функцию-обработчик на событие "keydown" на HTML-странице
    document.addEventListener("keydown", function (event) {
        //...
    });

    // вешаем функцию-обработчик на событие "keyup" на HTML-странице
    document.addEventListener("keyup", function (event) {
        //...
    });

}

Мы можем при событии keydown запоминать нажатые клавиши, а при событии keyup обработать их. Для запоминания одновременно нажатых клавиш я решил использовать массив. Дополним код:
function runOnKeys() {

    let arrChars = [];                    // массив одновременно нажатых клавиш

    document.addEventListener("keydown", function (event) {
        arrChars.push(event.code);        // запоминаем код нажатой и пока еще не отпущенной клавиши
    });

    document.addEventListener("keyup", function (event) {
        if (arrChars.length == 0) return; // нечего обрабатывать, завершаем функцию

        // ...

        arrChars.length = 0;              // очистим массив одновременно нажатых клавиш
    });

}

Событий keyup после отпускания одновременно нажатых клавиш будет сгенерировано столько же, сколько было отпущенных клавиш. Обработку одновременного нажатия клавиш мы запустим при первом событии keyup, после чего очистим массив одновременно нажатых клавиш. Последующие события keyup после первого проигнорируем с помощью инструкции if (arrChars.length == 0) return;.

Я потестировал полученный код в браузере, отслеживая состояние массива одновременно нажатых клавиш с помощью инструкции console.log(arrChars);, и заметил, что при достаточно продолжительном нажатии клавиш генерируется повторное событие keydown, из-за чего в массиве одновременно нажатых клавиш некоторые клавиши многократно дублируются (этот момент был отмечен и в обсуждаемом учебнике). Эти повторы можно отбросить. Дополним код (я отметил изменения красным цветом):
function runOnKeys() {

    let arrChars = [];                    // массив одновременно нажатых клавиш

    document.addEventListener("keydown", function (event) {
        if (event.repeat) return;         // повторы не обрабатываем
        arrChars.push(event.code);        // запоминаем код нажатой и пока еще не отпущенной клавиши
    });

    document.addEventListener("keyup", function (event) {
        if (arrChars.length == 0) return; // нечего обрабатывать, завершаем функцию

        // ...

        arrChars.length = 0;              // очистим массив одновременно нажатых клавиш
    });

}

Осталось написать обработку одновременного нажатия клавиш. От нас требуется при одновременном нажатии заданных клавиш запустить заданный код. До сих пор я не рассматривал параметры нашей функции runOnKeys. Первым параметром с названием func, очевидно, будет заданный код. Чтобы прописать для функции runOnKeys множество параметров, начиная со второго, задающих определенную последовательность одновременно нажатых клавиш, мне пришлось вернуться к первой части обсуждаемого учебника и повторить подраздел 6.2 «Остаточные параметры и оператор расширения». Множество параметров с точно не определенным их количеством можно задать с помощью так называемых «остаточных параметров», используя многоточие. Дополним код (я отметил изменения красным цветом):
function runOnKeys(func, ...args) {

    let arrChars = [];                    // массив одновременно нажатых клавиш

    document.addEventListener("keydown", function (event) {
        if (event.repeat) return;         // повторы не обрабатываем
        arrChars.push(event.code);        // запоминаем код нажатой и пока еще не отпущенной клавиши
    });

    document.addEventListener("keyup", function (event) {
        if (arrChars.length == 0) return; // нечего обрабатывать, завершаем функцию

        // ...

        arrChars.length = 0;              // очистим массив одновременно нажатых клавиш
    });

}

Теперь нам нужно проверить, есть ли заданные клавиши среди одновременно нажатых. И, если заданные клавиши есть среди одновременно нажатых, запустим заданный код func. Дополним код (я отметил изменения красным цветом):
function runOnKeys(func, ...args) {

    let arrChars = [];                    // массив одновременно нажатых клавиш

    document.addEventListener("keydown", function (event) {
        if (event.repeat) return;         // повторы не обрабатываем
        arrChars.push(event.code);        // запоминаем код нажатой и пока еще не отпущенной клавиши
    });

    document.addEventListener("keyup", function (event) {
        if (arrChars.length == 0) return; // нечего обрабатывать, завершаем функцию

        let runFunc = true;
        for (let arg of args) {           // нажаты ли одновременно отслеживаемые клавиши
            if (!arrChars.includes(arg)) {
                runFunc = false;
                break;
            }
        }
        if (runFunc) func();              // если нажаты, запускаем заданный код

        arrChars.length = 0;              // очистим массив одновременно нажатых клавиш
    });

}

args — это массив кодов отслеживаемых клавиш. Я перебираю их в цикле и проверяю, есть ли эти коды в массиве одновременно нажатых клавиш arrChars с помощью метода includes. Если установлено, что какая-либо из отслеживаемых клавиш отсутствует в массиве одновременно нажатых клавиш, цикл прерывается, а этот факт запоминается в переменной runFunc. Заданный код func запускается (или не запускается) в зависимости от значения переменной runFunc.

Вот, собственно, и всё. Я протестировал этот код у себя в браузере, он работает. Авторы задачи решили ее несколько по-другому, хотя общие принципы те же. Вместо массива они используют множество значений Set, а обработку заданного кода умудрились вставить в функцию-обработчик события keydown. Очень интересное решение.

Мифы о большом взрыве, Олег Верходанов

Никогда не думал, что научная лекция может собрать на ютубе миллион просмотров. И вот она, эта лекция. Ничего не понятно, но очень интересно, просмотрел на одном дыхании. Лектор — виртуоз. Рекомендую к просмотру.

https://www.youtube.com/watch?v=EiUy8aJJKOQ

Четко, с юмором, со списком интересных книг по физике. Походя упоминается известный сериал.

Продолжительность видеоролика: 49 минут 46 секунд. Название: «Мифы о Большом взрыве: как из «ничего» получилось «всё»? Олег Верходанов. Ученые против мифов 12-7». Видеоролик опубликован на ютубе примерно год назад. Название канала: «АНТРОПОГЕНЕЗ РУ».

Лекция проходила в рамках научно-просветительского форума «Ученые против мифов». Это было уже 12-е по счету мероприятие такого рода (первый форум прошел в 2016 году), организованное в частности одним из создателей портала «Антропогенез.Ру», Александром Соколовым (на видео это мужчина с бородой, ведущий форума).

12-й форум проходил в Москве два дня: с 29 февраля по 1 марта 2020 года.

Жаль, но докладчик, Олег Васильевич Верходанов, вскоре после этого (05.04.2020 года) умер от сердечного приступа. Ему было всего лишь 55 лет:
https://ru.wikipedia.org/wiki/Верходанов,_Олег_Васильевич

Новая версия файла heroes.png

Начало: GIMP, формат PNG, утилита pngcrush.

1. Оригинальная версия файла heroes.png из задачи «Расставить супергероев по полю» к подразделу 3.3 «Drag'n'Drop с событиями мыши» второй части учебника по JavaScript:



Размеры изображения (ширина, высота): 380 × 260 пикселей.
Размер файла изображения: 56 699 байт (55,3 килобайта).

2. Я сделал новую версию этого файла:



Размеры изображения (ширина, высота): те же, что и у оригинала.
Размер файла изображения: 46 170 байт (45,0 килобайта).

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

Во-вторых, фигурку Человека-паука опустил вниз на один пиксель (на демонстрационной странице авторов задачи у этой фигурки срезался один пиксель с головы сверху, это можно увидеть, если увеличить демонстрационную страницу авторов задачи в браузере), фигурку Железного человека поднял вверх на несколько пикселей до уровня Человека-паука (у Железного человека на демонстрационной странице авторов задачи из-за этой недоработки художника отрезается часть тени снизу), фигурку Капитана Америки сдвинул вправо на несколько пикселей и дорисовал срезанную художником часть тени слева от этой фигурки.

В-третьих, подправил (кое-где дорисовал) тени Капитана Америки, Железного человека, Человека-паука и Циклопа (немного сомневаюсь, что правильно его идентифицировал). Нужно учитывать, что тени нарисованы пикселями с частичной прозрачностью.

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

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

При таком условии не получается исправить самый главный «косяк» художника и верстальщика. Авторы задачи отвели для каждой фигурки размеры (ширина, высота) 130 × 128 пикселей. Фигурки Халка и Винни-Пуха почти полностью занимают эти размеры, поэтому с ними проблем нет. А вот остальные четыре фигурки для таких размеров слишком узкие (по высоте всё в порядке), поэтому у них с боков остается определенное пространство, которое на демонстрационной странице авторов задачи не позволяет поднести четыре проблемные фигурки вплотную к боковым сторонам области просмотра браузера.

Чтобы решить эту проблему, верстальщику для каждой из четырех узких фигурок в коде стилей CSS стоит выделить область гораздо меньше 128 пикселей по ширине. В этом случае код CSS-стилей тестовой HTML-страницы придется изменять.

GIMP, формат PNG, утилита pngcrush

В одном из предыдущих постов я разбирал постановку задачи в учебнике по JavaScript. Авторы задачи создали тестовую HTML-страницу, на которой использовалась картинка в формате PNG размером 380 × 260 пикселей. Я описал несколько недостатков и ошибок, допущенных художником. После решения той задачи я решил почистить картинку, избавить ее от некоторых из перечисленных ошибок и недостатков. Вот эта картинка:



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

Уже довольно давно для обработки изображений я пользуюсь растровым графическим редактором «GIMP»:

https://ru.wikipedia.org/wiki/GIMP
https://www.gimp.org

Сначала (с 1995 года) эта программа разрабатывалась для операционной системы «Linux», но потом появилась и версия для «Microsoft Windows», которая так и называется: «GIMP for Windows». Я использую именно ее. Программа бесплатная, у нее имеется перевод интерфейса на русский язык. По возможностям, конечно, уступает флагману жанра, программе «Adobe Photoshop». Скачать программу можно с ее официального сайта, ссылка указана выше.

У меня на компьютере стояла версия 2.8.10 программы «GIMP for Windows». Это довольно старая версия, вышедшая где-то в 2013 году (то есть 8 лет назад). Но она меня полностью устраивала, и я не испытывал потребности в ее обновлении.

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

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

С этими задачами я более-менее справился и сохранил итоговое изображение в формате PNG. И тут оказалось, что на выходе у меня получился файл примерно раза в полтора больше исходного. Исходный файл имеет размер в 55,3 килобайт, а у меня получился файл около 86 килобайт.

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

* * *

Я решил немного поэкспериментировать с графическим редактором. И в первую очередь просто открыл исходную картинку, а потом сохранил ее в другом файле, никак не изменяя. И вот оно: размер файла картинки увеличился в полтора раза. Значит, виновником увеличения был не я (не мои действия, изменяющие картинку), а сам графический редактор.

Тут, наверное, следует немного пояснить, как в графическом редакторе происходит работа с картинкой. Картинка хранится в файле в формате PNG. В первую очередь это значит, что картинка хранится в файле в упакованном (сжатом) виде. Формат PNG предназначен для использования на веб-сайтах, для уменьшения размера файла картинка упаковывается по определенным алгоритмам так, чтобы размер файла был как можно меньше. Для отображения картинки браузер распаковывает ее из файла.

Точно так же графический редактор, когда открывает картинку в формате PNG из файла, распаковывает ее. Затем картинка импортируется во внутренний формат графического редактора. В этом формате картинку удобнее обрабатывать: в нем можно сохранять свои действия над картинкой, чтобы их можно было «откатить». Также у внутреннего формата есть еще множество других возможностей. Внутренний формат графического редактора «GIMP» называется «XCF», в нем можно сохранять картинку на промежуточных этапах работы над нею. Вот статья об этом формате в википедии:

https://ru.wikipedia.org/wiki/XCF

Чтобы сохранить окончательный результат обработки обратно в файл формата PNG, картинку следует не «сохранять», а «экспортировать» (в меню редактора «GIMP» это разные пункты).

* * *

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

Текущая версия программы «GIMP for Windows» — 2.10.54 от 28.03.2021 года.

Но обновление не помогло. Несколько увеличились возможности импорта и экспорта картинок.

Например, при импорте картинки из файла формата PNG графический редактор выдает вопрос о преобразовании цветового пространства (цветового профиля) картинки (этот вопрос может и не выдаваться, его можно отключить в настройках редактора). В связи с этим в википедии есть несколько подходящих статей:

https://ru.wikipedia.org/wiki/Цветовое_пространство
https://ru.wikipedia.org/wiki/Цветовая_модель
https://ru.wikipedia.org/wiki/ICC-профиль

В оригинальной картинке содержится цветовой профиль «sRGB IEC61966-2.1», на который, как там написано, есть авторские права у американской компании «Hewlett-Packard». Редактор «GIMP» предлагает преобразовать указанный цифровой профиль в цифровой профиль «GIMP built-in sRGB», который передан в общественное достояние (по-английски «Public Domain»). Однако, всё это мало влияет на итоговый размер файла картинки в формате PNG (я пробовал использовать оба этих цифровых профиля).

В новой версии графического редактора обновлен диалог экспорта картинки в файл формата PNG.

Например, добавился раскрывающийся список, в котором по умолчанию выбрано значение «Автовыбор формата пикселей», а если этот список раскрыть, то в нем есть 8 пунктов, например «8 бит на канал RGB», «8 бит на канал GRAY» и так далее. Да, с помощью этого списка можно регулировать размер итогового файла формата PNG, но эти пункты (я их проверил) меняют изображение так, что разница становится видна невооруженным глазом, а мне этого не нужно (я хотел, чтобы качество картинки осталось примерно таким же, как и на исходной картинке). Например, если из этих пунктов выбрать вариант без буквы «A» на конце, то с картинки исчезнет прозрачность в нужных местах (будет удален так называемый «альфа-канал», как я понимаю). Ни один из этих пунктов так и не дал уменьшения файла картинки до нужного размера с одновременным сохранением нужного качества.

Еще в новой версии редактора в этом диалоге внизу добавилось пять флагов: «Сохранить данные Exif», «Сохранить данные XMP» и так далее. Это дополнительные данные к картинке формата PNG и они, как я понимаю, не влияют на само изображение. Я отключил все эти пять флагов, но в моем случае это не дало уменьшения итогового файла картинки.

* * *

Еще я пробовал уменьшить размер итогового файла картинки, переведя ее в так называемый «индексированный режим» (пункт меню «Изображение – Режим – Индексированный...»). Да, таким образом можно очень сильно уменьшить размер итогового файла, но у меня, например, при этом исчезли полупрозрачные тени героев. Полная прозрачность, где она была, осталась, а частично прозрачные пиксели полностью исчезли.

Как я понимаю, при переводе в индексированный режим уменьшается количество используемых цветов в изображении, таким образом и достигается уменьшение размера итогового файла. В оригинальном изображении в моем случае содержится 498 разных цветов (количество цветов в изображении можно определить с помощью пункта меню «Цвет – Инфо – Анализ изображения»). По умолчанию при переходе в индексированный режим количество цветов сокращается до максимум 256. Понятно, что качество картинки при этом ухудшится. Мне такое не подходит. Не знаю, можно ли в индексированном режиме использовать палитру из 498 цветов? Я не смог такого добиться.

* * *

В конце концов, мне помогла серия из 4 статьей от 2007-2008 годов автора, имя которого в именительном падеже звучит как «Сергей Чикуенок» (не знаю, как склонять его фамилию):

https://www.artlebedev.ru/technogrette/img/png-1/
https://www.artlebedev.ru/technogrette/img/png-2/
https://www.artlebedev.ru/technogrette/img/png-3/
https://www.artlebedev.ru/technogrette/img/png-4/

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

Путей, по которым может пойти работа алгоритма сжатия, настолько много, что проверить их все не представляется возможным. Например, в статье Сергея утверждается, что таких способов 5 в степени числа пикселей, содержащихся по высоте картинки. То есть, к примеру, для нашей картинки их 5 в степени 260. Это очень большое число.

Я не знаю, как с этим справляется редактор «GIMP», но по моему случаю видно, что не очень хорошо. Сергей рекомендует пользоваться оптимизирующими утилитами, которые перебирают несколько путей сжатия, выбранных эвристически. (Тут мне в голову пришли шахматные движки, которые уже давно играют лучше шахматистов-людей. Эти движки, ведь, тоже не могут рассчитать все возможные пути развития шахматной партии, потому что их очень много. И эти движки тоже пользуются различными эвристическими уловками, чтобы оценить перспективность путей развития партии и выбрать лучший, не просчитывая пути до конца.)

Я попробовал использовать одну из таких утилит, указанную в списке Сергея первой. Это оказалась утилита с названием «pngcrush» из набора утилит «PNG and MNG tools». Статья в википедии:

https://ru.wikipedia.org/wiki/Pngcrush
https://pmt.sourceforge.io/pngcrush/index.html (страница утилиты)

Утилита бесплатная. Скомпилированные версии утилиты можно скачать отсюда:

https://sourceforge.net/projects/pmt/files/pngcrush-executables/

Я скачал версию 1.8.11 от 16.01.2017 года.

Утилита запускается из командной строки операционной системы «Microsoft Windows» или из программы «Windows PowerShell» той же операционной системы. Я запускал из «Windows PowerShell».

Как сказано на странице утилиты и на странице википедии, посвященной этой утилите, главная цель этой утилиты — уменьшение размера данных секции IDAT в файле формата PNG. Как я понимаю, в этой секции и хранятся, собственно, сжатые данные об изображении (по-английски «image data», сокращенно «IDAT»). Возможные секции (по-английски «chunk») формата PNG описаны в спецификации формата PNG:

https://www.w3.org/TR/PNG/

Команда для запуска утилиты в программе «Windows PowerShell» у меня в компьютере:
PS C:\Илья\PNG crush> .\pngcrush_1_8_11_w64 heroes_gimp.png heroes_pngcrush.png

Здесь PS C:\Илья\PNG crush> — это так называемое «приглашение ко вводу команды», оно содержит адрес местоположения, в котором сейчас находится пользователь (название каталога, в который я поместил утилиту). Символы .\ требуются, чтобы запустить исполняемый файл из текущего каталога (в командной строке они не были нужны). pngcrush_1_8_11_w64 — это название исполняемого файла утилиты «pngcrush».

Я передал утилите два параметра: название входящего файла heroes_gimp.png (этот файл утилита будет анализировать, он должен присутствовать в каталоге) и название исходящего файла heroes_pngcrush.png (утилита создаст файл с таким именем и поместит в него файл в формате PNG уменьшенного размера).

Что у меня получилось? Итоги:

heroes.png — исходный файл, размер: 56 699 байт (55,3 килобайта);
heroes_gimp.png — файл heroes.png после импорта в GIMP и экспорта обратно в файл, размер: 86 404 байта (84,3 килобайта);
heroes_pngcrush.png — результат обработки файла heroes_gimp.png утилитой «pngcrush», размер: 43 697 байт (42,6 килобайта).

Утилита «pngcrush» почти в два раза уменьшила размер переданного ей файла. Естественно, без потери качества изображения, так как меняется только способ сжатия в пределах одного и того же алгоритма сжатия (а формат PNG — это изначально формат сохранения изображения со сжатием без потери качества изображения).

JavaScript: расставить героев на поле, решение задачи

Начало: JavaScript: расставить героев на поле, постановка задачи.

Разбираю моё решение задачи «Расставить супергероев по полю» к подразделу 3.3 «Drag'n'Drop с событиями мыши» второй части учебника по JavaScript.

Сначала, как и советуют авторы задачи, повесим одну функцию-обработчик на событие mousedown на документе (будем использовать делегирование обработки этого события от перетаскиваемых объектов к документу):
document.addEventListener("mousedown", onMouseDown);
function onMouseDown(event) {
    let tar = event.target;
    if (!tar.classList.contains("draggable")) return;

    // ...
}

event.target — это HTML-элемент, над которым пользователь нажал кнопку мыши. Если этот HTML-элемент не содержит в своем списке классов CSS-класс draggable, то такой HTML-элемент мы в данной функции-обработчике обрабатывать не будем, а завершим работу функции (инструкция return;). То есть в данной функции будем обрабатывать только перетаскиваемые объекты (в данном случае — 6 объектов-героев и мяч).

Сразу избавимся от действий браузера по умолчанию, которые в данном случае могут помешать работе скрипта. Пока пользователь еще не может перетаскивать героев и мяч на тестовой HTML-странице. Но, если он, например, наведет курсор мыши на одного из шести героев, нажмет основную кнопку мыши, а затем, не отпуская кнопку мыши, станет перетаскивать курсор мыши вверх HTML-страницы, над текстом, который там находится, то текст начнет выделяться. Выделение текста — это действие браузера по умолчанию, которое запускается после события мыши mousedown.

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

Отключим эти два действия браузера по умолчанию (изменения в коде я выделил красным цветом):
document.addEventListener("mousedown", onMouseDown);
function onMouseDown(event) {
    let tar = event.target;
    if (!tar.classList.contains("draggable")) return;
    event.preventDefault();

    // ...
}

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

Реализуем дальнейший механизм процесса перетаскивания («Drag’n’Drop») объектов-героев или мяча так, как мы это уже делали в задаче про слайдер (и близко к тому, как это было показано в подразделе 3.3 «Drag'n'Drop с событиями мыши» второй части обсуждаемого учебника). Дополним код:
document.addEventListener("mousedown", onMouseDown);
function onMouseDown(event) {     // захват объекта
    let tar = event.target;
    if (!tar.classList.contains("draggable")) return;
    event.preventDefault();

    // ...

    function onMouseMove(event) { // перетаскивание объекта
        // ...
    }

    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);

    function onMouseUp() {        // отпускание объекта
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("mouseup", onMouseUp);
    };
}

Теперь реализуем движение объекта при перетаскивании по аналогии с тем, как это было сделано в задаче про слайдер (и близко к тому, как это было показано в подразделе 3.3 «Drag'n'Drop с событиями мыши» второй части обсуждаемого учебника). Дополним код (изменения я отметил красным цветом):
Скрипт 1.
document.addEventListener("mousedown", onMouseDown);
function onMouseDown(event) {     // захват объекта
    let tar = event.target;
    if (!tar.classList.contains("draggable")) return;
    event.preventDefault();

    let tarRect = tar.getBoundingClientRect();
    let shiftX = event.clientX - tarRect.x;
    let shiftY = event.clientY - tarRect.y;

    function onMouseMove(event) { // перетаскивание объекта
        let x, y;
        x = event.pageX - shiftX;
        y = event.pageY - shiftY;

        tar.style.position = "absolute";
        tar.style.left = x + "px";
        tar.style.top = y + "px";
    }

    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);

    function onMouseUp() {        // отпускание объекта
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("mouseup", onMouseUp);
    };
}

Вместо бегунка слайдера теперь — перетаскиваемый объект tar. И мы передвигаем объект не только по горизонтали, как в случае с бегунком слайдера, а и по вертикали тоже (то есть могут меняться и координата x, и координата y).

Стало не нужно учитывать сдвиг слайдера, на котором двигался бегунок, так как наш перетаскиваемый объект может двигаться по всей области просмотра браузера без ограничений. Это немного упростило формулу для вычисления координат x и y.

Позиционирование бегунка слайдера было относительным (он позиционировался относительно верхнего левого угла клиентской части своего контейнера, то есть слайдера). Для перетаскиваемых объектов я решил выбрать абсолютное позиционирование (координаты перетаскиваемого объекта вычисляются относительно левого верхнего угла тестовой HTML-страницы, поэтому берем event.pageX и event.pageY).

И тут выяснилась одна интересная деталь, которую я упустил при чтении подраздела 3.3 «Drag'n'Drop с событиями мыши» второй части обсуждаемого учебника (там она была затронута). В задаче со слайдером я использовал для вычисления сдвига курсора мыши относительно левого верхнего угла перетаскиваемого объекта такой код:
    let thumbRect = thumb.getBoundingClientRect();
    let shiftX = event.pageX - thumbRect.x;

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

Для прокручиваемой HTML-страницы правильно было бы учесть возможное смещение HTML-страницы по вертикали или горизонтали. Например, так:
    let thumbRect = thumb.getBoundingClientRect();
    let shiftX = event.pageX - pageXOffset - thumbRect.x;
Ну а это то же самое, что и
    let thumbRect = thumb.getBoundingClientRect();
    let shiftX = event.clientX - thumbRect.x;
Что для нашей теперешней задачи с героями, мячом и футбольным полем превращается в
    let tarRect = tar.getBoundingClientRect();
    let shiftX = event.clientX - tarRect.x;
    let shiftY = event.clientY - tarRect.y;

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

Отмечу тут интересную особенность: при начале перемещения любого из шести перетаскиваемых объектов-героев он исключается из нормального потока HTML-страницы (потому что мы включаем для него в скрипте абсолютное позиционирование), его место освобождается, но другие объекты-герои, идущие в коде HTML-страницы за ним, сразу же автоматически сдвигаются на его место. Как я понимаю, это происходит из-за того, что в стиле этих перетаскиваемых объектов есть указание float: left;.

Но мы не можем остановиться на скрипте 1, так как задача ставит еще две дополнительные подзадачи: во-первых, скрипт не должен позволять перетащить объект так, чтобы его часть вышла за границы области просмотра браузера (скрипт 1 это позволяет); во-вторых, при перетаскивании объекта, если край объекта достиг края области просмотра браузера, окно браузера должно прокрутиться в нужную сторону, чтобы позволить дальнейшее перетаскивание объекта в желаемую пользователем сторону (скрипт 1 этого не умеет).

Реализуем первую подзадачу (ограничение перетаскивания объектов границей области просмотра браузера). Если бы не было возможности прокрутки HTML-страницы по вертикали и горизонтали, то для решения этой подзадачи следовало бы контролировать координаты перетаскиваемых объектов (координаты левых верхних углов этих объектов) так, чтобы они не выходили за границы следующих диапазонов:

0 ⩽ x ⩽ (ширина области просмотра браузера – ширина перетаскиваемого объекта)
0 ⩽ y ⩽ (высота области просмотра браузера – высота перетаскиваемого объекта)

или в терминах нашего кода на языке JavaScript:

0 ⩽ xdocument.documentElement.clientWidth - tar.offsetWidth
0 ⩽ ydocument.documentElement.clientHeight - tar.offsetHeight

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

pageXOffsetxdocument.documentElement.clientWidth + pageXOffset - tar.offsetWidth
pageYOffsetydocument.documentElement.clientHeight + pageYOffset - tar.offsetHeight

Дополним код (я отметил изменения красным цветом):
Скрипт 2.
document.addEventListener("mousedown", onMouseDown);
function onMouseDown(event) {     // захват объекта
    let tar = event.target;
    if (!tar.classList.contains("draggable")) return;
    event.preventDefault();

    let tarRect = tar.getBoundingClientRect();
    let shiftX = event.clientX - tarRect.x;
    let shiftY = event.clientY - tarRect.y;

    function onMouseMove(event) { // перетаскивание объекта
        let x, y;
        x = event.pageX - shiftX;
        y = event.pageY - shiftY;

        if (x > document.documentElement.clientWidth + pageXOffset - tar.offsetWidth)
            x = document.documentElement.clientWidth + pageXOffset - tar.offsetWidth;
        if (x < pageXOffset) x = pageXOffset;

        if (y > document.documentElement.clientHeight + pageYOffset - tar.offsetHeight)
            y = document.documentElement.clientHeight + pageYOffset - tar.offsetHeight;
        if (y < pageYOffset) y = pageYOffset;

        tar.style.position = "absolute";
        tar.style.left = x + "px";
        tar.style.top = y + "px";
    }

    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);

    function onMouseUp() {        // отпускание объекта
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("mouseup", onMouseUp);
    };
}

Этот скрипт позволяет пользователю перетаскивать объектов-героев и мяч, но не дает перетащить их за границу области просмотра браузера. Хотя можно, к примеру, уменьшить окно браузера так, чтобы появились полосы прокрутки, затем перетащить объект к краю области просмотра браузера, после чего с помощью полосы прокрутки прокрутить HTML-страницу так, что объект таки окажется за границей области просмотра. Но такая возможность не противоречит условиям задачи, потому что по условиям задачи мы должны ограничивать именно перетаскивание объекта, а не какие-либо другие действия пользователя (прокрутка HTML-страницы).

Реализуем последнюю подзадачу: обеспечение прокрутки HTML-страницы, если пользователь подносит перетаскиваемый объект к краю области просмотра браузера. Вообще в задаче поставлено условие обеспечить эту возможность только по вертикали, но мы сделаем и по горизонтали тоже (к чему нам полумеры?).

Как это реализовать? При каждом движении курсора мыши (событие мыши mousemove) с захваченным объектом, если объект коснулся края области просмотра браузера, будем прокручивать HTML-страницу с помощью соответствующего метода (к примеру, возьмем метод scrollBy) на то расстояние, на которое объект залез бы за границу области просмотра браузера при этом отдельном движении курсора мыши.

Делать это будем до каждого из соответствующих ограничений диапазонов, которые мы прописали в скрипте 2. Зачем нужно оставить ограничения диапазонов? Если их убрать, то у пользователя появится возможность прокручивать HTML-страницу в любом направлении бесконечно. Нам этого, думаю, не нужно (по крайней мере, эти ограничения оставлены в решении авторов задачи, я это заметил, наблюдая работу их решения на демонстрационной странице).

Почему ограничения диапазонов координат будут работать правильно, ведь мы их создали для области просмотра браузера? Дело в том, что перед проверкой каждого из ограничений мы сделаем соответствующую прокрутку HTML-страницы, как это было описано выше. Из-за этого границы диапазонов изменятся и позволят двигать объект в нужном направлении.

Как тогда ограничивается бесконечная прокрутка в любом направлении? Метод scrollBy срабатывает только до тех пор, пока в требуемом направлении за границей области просмотра еще имеется скрытый кусок HTML-страницы. Как только в требуемом направлении HTML-страница будет прокручена до своего конца, метод scrollBy перестанет срабатывать. (В случае без ограничения диапазона передвижение объекта за границу области просмотра и одновременно за границу HTML-страницы дает место для срабатывания метода scrollBy. По крайней мере, так это работает в моем браузере.)

Дополняем код (я отметил красным цветом изменения):
Скрипт 3.
document.addEventListener("mousedown", onMouseDown);
function onMouseDown(event) {     // захват объекта
    let tar = event.target;
    if (!tar.classList.contains("draggable")) return;
    event.preventDefault();

    let tarRect = tar.getBoundingClientRect();
    let shiftX = event.clientX - tarRect.x;
    let shiftY = event.clientY - tarRect.y;

    function onMouseMove(event) { // перетаскивание объекта
        let x, y;
        x = event.pageX - shiftX;
        y = event.pageY - shiftY;

        if (x > document.documentElement.clientWidth + pageXOffset - tar.offsetWidth)
            scrollBy(x - (document.documentElement.clientWidth + pageXOffset - tar.offsetWidth), 0);
        if (x > document.documentElement.clientWidth + pageXOffset - tar.offsetWidth)
            x = document.documentElement.clientWidth + pageXOffset - tar.offsetWidth;

        if (x < pageXOffset) scrollBy(x - pageXOffset, 0);
        if (x < pageXOffset) x = pageXOffset;

        if (y > document.documentElement.clientHeight + pageYOffset - tar.offsetHeight)
            scrollBy(0, y - (document.documentElement.clientHeight + pageYOffset - tar.offsetHeight));
        if (y > document.documentElement.clientHeight + pageYOffset - tar.offsetHeight)
            y = document.documentElement.clientHeight + pageYOffset - tar.offsetHeight;

        if (y < pageYOffset) scrollBy(0, y - pageYOffset);
        if (y < pageYOffset) y = pageYOffset;

        tar.style.position = "absolute";
        tar.style.left = x + "px";
        tar.style.top = y + "px";
    }

    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);

    function onMouseUp() {        // отпускание объекта
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("mouseup", onMouseUp);
    };
}

Это окончательный вариант скрипта.

JavaScript: расставить героев на поле, постановка задачи

Решил задачу «Расставить супергероев по полю» к подразделу 3.3 «Drag'n'Drop с событиями мыши» второй части учебника по JavaScript.

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

Дана тестовая HTML-страница со схемой футбольного поля. Рядом с этим полем расположено 7 объектов, которые пользователь может разместить на футбольном поле. Пять из этих объектов представляют собой разных супергероев из американских комиксов, шестой объект — Винни-Пух из советского мультфильма 1969 года, а седьмой объект — знакомый нам по предыдущим задачам мяч.

Если ширины HTML-страницы хватает, то объекты будут расположены справа от футбольного поля. В противном случае они появятся снизу от футбольного поля.

Изначально пользователь может лишь навести курсор мыши на эти объекты, в результате чего курсор мыши изменит свой вид со стрелки на руку с указующим пальцем.

Задача состоит в том, чтобы с помощью скрипта на языке JavaScript обеспечить пользователю возможность перетаскивать (с помощью механизма «Drag’n’Drop»: то есть пользователь нажимает кнопку мыши на выбранном объекте, затем, не отпуская нажатой кнопки мыши, перетаскивает объект в выбранное место и отпускает кнопку мыши, оставляя объект на новом месте) заданные на HTML-страницы объекты туда, куда он пожелает.

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

Выше футбольного поля на тестовой HTML-странице есть текст с заголовком, в котором программисту, который будет писать скрипт, даны дополнительные инструкции. Либо это можно считать памяткой с главными условиями, которые должны быть учтены при работе скрипта.

Вот как выглядит тестовая HTML-страница изначально в моём браузере (картинка):



Тонкая серая линия сверху и слева на картинке — это край окна браузера. Я оставил ее на картинке, чтобы можно было понять, где в области просмотра браузера относительно края области просмотра находится содержимое тестовой HTML-страницы.

* * *

Рассмотрим, как заданная тестовая HTML-страница сделана с точки зрения языков HTML и CSS. В принципе, у меня это всегда первый этап при решении задач в обсуждаемом учебнике.

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

Как в коде HTML тут представлено футбольное поле? Оно представлено HTML-элементом div с идентификатором field. Для этого элемента в файле soccer.css описан стиль #field. В этом стиле заданы ширина и высота HTML-элемента (800 × 600 пикселей), а также загружается картинка того же размера в формате PNG, которая будет фоном данного HTML-элемента div. На этой картинке и изображена схема футбольного поля, которую в итоге видит пользователь в браузере. Надписи на схеме являются частью картинки футбольного поля.

В стиле #field стоит обратить внимание на указание float: left;, которое смещает футбольное поле к левой стороне контейнера (в данном случае контейнером является тело тестовой HTML-страницы), позволяя тексту и строковым HTML-элементам обтекать себя справа. Именно поэтому 7 объектов, которые пользователь сможет перетаскивать, располагаются справа от футбольного поля (если позволяет ширина тестовой HTML-страницы) или снизу от футбольного поля (если ширина тестовой HTML-страницы настолько маленькая, что объектам не остается места справа от футбольного поля).

Как в коде HTML тут представлены 7 перетаскиваемых (по-английски «draggable») объектов? Мяч и 6 объектов-героев представлены по-разному.

Мяч, так же, как и в предыдущих задачах из обсуждаемого учебника, представлен HTML-элементом img, в который загружается файл в формате SVG (напомню, это не картинка, а текстовый файл на языке разметки SVG, похожем на язык разметки HTML; с помощью формата SVG браузер может отображать изображения масштабируемой векторной графики). Размеры мяча заданы в файле SVG — 40 × 40 пикселей.

6 объектов-героев представлены шестью HTML-элементами div с идентификаторами от hero1 до hero6. Для этих шести HTML-элементов в файле soccer.css описаны CSS-класс .hero и шесть индивидуальных стилей от #hero1 до #hero6. Размер (ширина и высота) содержимого каждого HTML-элемента div из шести задан в 130 × 128 пикселей. Здесь же (в стиле) загружается картинка формата PNG, которая будет фоном каждого из шести HTML-элементов div.

Как может одна картинка давать разный фон для каждого из шести HTML-элементов div? Рисунки всех шести героев содержатся на одной картинке размером (ширина и высота) в 380 × 260 пикселей. С помощью CSS-свойства background-position в индивидуальных стилях шести HTML-элементов div, изображающих героев, указывается расстояние в пикселях, на которое браузер сдвинет фоновое изображение относительно левого верхнего угла клиентской части конкретного HTML-элемента div. Используется два числа: сдвиг по горизонтали, сдвиг по вертикали. Если сдвиг фонового изображения происходит за пределы клиентской части HTML-элемента div влево или вверх, то расстояние соответствующего сдвига указывается отрицательным числом.

Фрагмент файла soccer.css:
#hero1 {
  background-position: 0 0;
}

#hero2 {
  background-position: 0 -128px;
}

#hero3 {
  background-position: -120px 0;
}

#hero4 {
  background-position: -125px -128px;
}

#hero5 {
  background-position: -248px -128px;
}

#hero6 {
  background-position: -244px 0;
}

Чтобы ситуация стала еще более ясной, я взял картинку с героями и выделил на ней области, которые оказываются фоном для каждого из шести HTML-элементов div, изображающих героев:



Вставляя эту картинку в данный пост, я задал ей в стиле синюю границу толщиной в 1 пиксель. Надеюсь, ЖЖ ее отображает. У меня в браузере она видна. Я ее добавил, чтобы было видно, где заканчивается картинка с героями (это здесь, в посте, не видно, потому что у картинки в оригинале прозрачный фон). Сама эта синяя граница не входит в картинку.

Области фона каждого из шести HTML-элементов div я выделил красной границей. Эти красные границы являются частью каждого из шести прямоугольников фона размером 130 × 128 пикселей (этот размер, как уже было сказано ранее, указан в стиле .hero). Еще я добавил красные цифры на картинке — это номера индивидуальных стилей объектов-героев от #hero1 до #hero6.

(Тут, кстати, нужно отметить, что на тестовую HTML-страницу герои выводятся по порядку в строку, а строка в браузере может иметь переносы на следующую строку, если она не помещается в область просмотра браузера по ширине. А на картинке выше герои нумеруются по другому принципу. Эта разница может запутать программиста. Чтобы увидеть эту разницу, можно сравнить картинку из начала поста и данную картинку с героями.)

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

Видно, как верстальщик в паре с художником здесь накосячили.

Например, у изображений героев 1, 2, 3 и 4 с боков есть довольно большое пустое пространство. Это скажется, когда мы решим задачу. По условиям задачи, при поднесении объекта-героя к левому или правому краю области просмотра браузера, герой не должен вылезать за границу области просмотра. В итоге мы не сможем поднести изображение героя вплотную к границе области просмотра браузера из-за пустого пространства на самой картинке.

У героя 1 художник отрезал часть тени слева. У героя 2 верстальщик отрезал часть тени снизу. У героев 2 и 4 над головой остается ощутимо разное расстояние до верхней границы области фона.

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

В стиле .hero следует обратить внимание на указание float: left;. Если бы у нас в теле тестовой HTML-страницы был бы только один HTML-элемент с таким указанием, то он был бы прижат к левой стороне контейнера (в данном случае контейнером, как уже было указано выше, является тело тестовой HTML-страницы). Но у нас таких HTML-элементов восемь (футбольное поле и семь перетаскиваемых объектов). В такой ситуации к левой стороне контейнера оказалось прижато футбольное поле, потому что оно в коде HTML-страницы идет первым. Следующий HTML-элемент с таким указанием обтекает предыдущий HTML-элемент с таким указанием и так далее. (Если это указание убрать из указанного стиля, объекты-герои окажутся под футбольным полем, они его как бы не заметят.)

Все перетаскиваемые объекты (их семь штук: герои и мяч) помечены CSS-классом .draggable. Это можно будет использовать, если в скрипте понадобится обратиться сразу ко всем этим объектам (по общему CSS-классу легко сделать выборку).

И, наконец, последним в теле тестовой HTML-страницы (если не учитывать HTML-элемент script) идет еще один HTML-элемент div. Зачем он нужен? Дело в том, что в его стиле есть указание clear:both. Такую конструкцию применяют, если нужно очистить (по-английски «clear») имеющуюся у браузера информацию о ранее установленном в стилях предыдущих HTML-элементов выравнивании и обтекании (в данном случае нужно очистить действие CSS-свойства float: left;).

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

На этом разбор постановки задачи заканчиваю. Разбор решения — в следующем посте.

JavaScript: mousedown, preventDefault, iframe

Начало: JavaScript: событие mouseup за пределами HTML-элемента.

Причиной разбора в этом посте послужило непонятное для меня поведение браузера (у меня — «Microsoft Edge» на движке «Chromium») при решении задачи «Слайдер» к подразделу 3.3 «Drag'n'Drop с событиями мыши» второй части учебника по JavaScript. Я посветил разбору этой задачи отдельный пост (см. окончание этого поста).

После решения задачи со слайдером я решил сначала разобраться, каким должно быть поведение браузера в той ситуации согласно стандарта (спецификации «UI Events»). Этому был посвящен предыдущий пост.

Для тестов я использовал такой скрипт на языке JavaScript (см. предыдущий пост):
let elem;

// elem = window;                     // окно браузера
// elem = document;                   // HTML-страница (документ)
// elem = document.documentElement;   // HTML-элемент <html>
// elem = document.body;              // HTML-элемент <body>
elem = document.querySelector("div"); // HTML-элемент <div>

elem.addEventListener("mousedown", onMouse);
elem.addEventListener("mouseup", onMouse);

function onMouse(event) {
    console.log(event.type + ", " + event.target);
}

К вышеупомянутой задаче про слайдер авторы задачи в своем решении использовали в начале функции-обработчика события mousedown следующий код:
event.preventDefault(); // предотвратить запуск выделения (действие браузера)

Действительно, в большинстве браузеров с события mousedown могут начинаться различные действия браузера по умолчанию. Например, при выделении кусочка текста на загруженной в браузер HTML-странице пользователь может поместить курсор мыши в начало кусочка текста, который он хочет выделить, затем нажать основную кнопку мыши (генерируется событие mousedown) и, не отпуская кнопку мыши, перетащить курсор мыши к концу кусочка текста, который он хочет выделить, после этого отпустить кнопку мыши. Браузер это умеет по умолчанию, автору HTML-страницы не требуется писать скрипт для обеспечения пользователю возможности выделения кусочка текста (и не только текста, а и других объектов на HTML-странице).

В данном же случае мы не хотим, чтобы браузер начал действие по умолчанию (выделение кусочка текста на HTML-странице) по событию мыши mousedown, поэтому и применяется метод event.preventDefault, который отменяет действие браузера по умолчанию.

Это прекрасно работает, пока в дело не вступает HTML-элемент iframe. Вообще, в вышеупомянутой задаче про слайдер HTML-элемент iframe отсутствует. Я решил эту задачу и не испытал никаких проблем. Однако, авторы обсуждаемого учебника по JavaScript придумали показывать работу своего решения задачи про слайдер, вставив HTML-страницу с решением задачи на HTML-страницу с постановкой задачи с помощью HTML-элемента iframe (они это проделали не только в этой задаче, но и в других). Тут-то в дело и вступил этот HTML-элемент.

Итак, сначала я изменил вышеприведенный тестовый скрипт, вставив в функцию-обработчик onMouse вызов метода event.preventDefault для события mousedown:
function onMouse(event) {
    if (event.type == "mousedown") {
        event.preventDefault();
    }
    console.log(event.type + ", " + event.target);
}

Поведение браузера осталось таким же, какое было описано в предыдущем посте.

После этого я смоделировал ситуацию в обсуждаемом учебнике по JavaScript, сложившуюся на странице постановки задачи со слайдером.

У меня уже имелась тестовая HTML-страница test.html, в теле которой содержался один HTML-элемент div. Я создал еще одну тестовую HTML-страницу, которую назвал test1.html. В ее тело я вставил HTML-элемент iframe с первоначальной тестовой HTML-страницей:
<iframe style="height: 200px; width: 100%" src="test.html"></iframe>
Дальнейшие тесты я уже проводил на этой новой HTML-странице test1.html.

В случае отсутствия запуска метода event.preventDefault поведение браузера (даже и с наличием HTML-элемента iframe) остается таким же, какое было описано в предыдущем посте.

В случае же присутствия запуска метода event.preventDefault (при наличии HTML-элемента iframe) поведение браузера поменялось.

Теперь, даже если для события мыши mouseup повесить (в скрипте, вызванном со встроенной HTML-страницы test.html) функцию-обработчик на объект window (окно браузера) или на объект document (документ, загруженный в браузер) или на объект document.documentElement (HTML-элемент html), то это событие не будет отловлено при выходе мыши с неотпущенной основной кнопкой за пределы HTML-элемента iframe (подробности см. в предыдущем посте).

Это поведение можно увидеть и на странице вышеупомянутой задачи со слайдером, поэкспериментировав с демонстрационным примером. Схватив бегунок слайдера мышкой, можно (не отпуская кнопку мыши) вывести курсор мыши за пределы встроенного кадра (HTML-элемента iframe) и отпустить кнопку мыши. Если после этого снова ввести курсор мыши в пределы встроенного кадра, станет видно, что скрипт не отловил отпускания кнопки мыши, потому что хоть кнопка мыши уже и отпущена, бегунок слайдера продолжает следовать за курсором мыши. Очевидно, авторы задачи и решения к ней не рассчитывали на такое поведение слайдера.

По идее, наличие метода event.preventDefault в функции-обработчике события mousedown не должно вызывать такое изменение поведения браузера.

Я поискал в интернете и нашел следующее сообщение о баге в адрес разработчиков движка «Chromium» (там в точности описана данная ситуация):

https://bugs.chromium.org/p/chromium/issues/detail?id=269917

Сообщение называется «Issue 269917: preventDefault on mousedown prevents proper handling of mouse events in iframes» и дата его первоначальной подачи — аж 8 августа 2013 года, то есть более 8 лет назад. Обсуждение этого сообщения длится по сей день (последнее сообщение в обсуждении датировано 15 июня этого года).

А что же делать, если нам нужно предотвратить выделение текста (и других объектов), которое браузер запустит по умолчанию при генерации события mousedown?

На сайте «Stack Overflow» был похожий вопрос. Ответ на него можно увидеть по следующей ссылке (отвечающий также привел и код действующего примера в своем ответе):

https://stackoverflow.com/questions/66703382/how-to-keep-watching-mouse-move-outside-iframe

Там предложено вставить в стиль целевого HTML-элемента указание user-select: none;. Неидеальное решение, но лучше, чем ничего.

В случае задачи со слайдером я просто вообще не стал вызывать метод event.preventDefault, так как на тестовой HTML-странице, заданной в задаче про слайдер, нет ничего, кроме нескольких HTML-элементов div, которые и так не будут автоматически выделяться.

JavaScript: событие mouseup за пределами HTML-элемента

В предыдущем посте при решении задачи, затрагивающей события мыши и механизм «Drag’n’Drop» (перетаскивания) в браузерах, у меня появилось несколько вопросов.

Например, насчет события мыши mouseup. Оно возникает при отпускании ранее нажатой кнопки мыши (или от другого указательного устройства, которое можно использовать для передвижения курсора мыши; по-английски такое устройство называют «pointing device», например: трекбол, тачпад, световое перо, графический планшет, сенсорный экран и так далее).

Я уже упоминал ранее, что правила работы браузера с событиями мыши определены в спецификации «UI Events». О событии mouseup в этой спецификации написано по следующим ссылкам:

https://www.w3.org/TR/uievents/#event-type-mouseup
https://w3c.github.io/uievents/#event-type-mouseup

Там сказано, цитирую:

A user agent MUST dispatch this event when a pointing device button is released over an element.


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

Я обратил внимание на слова «над элементом». Однако, на практике, для моего браузера («Microsoft Edge» на движке «Chromium») это не всегда верно.

Для тестирования я написал такой скрипт на языке JavaScript:
let elem;

// elem = window;                     // окно браузера
// elem = document;                   // HTML-страница (документ)
// elem = document.documentElement;   // HTML-элемент <html>
// elem = document.body;              // HTML-элемент <body>
elem = document.querySelector("div"); // HTML-элемент <div>

elem.addEventListener("mousedown", onMouse);
elem.addEventListener("mouseup", onMouse);

function onMouse(event) {
    console.log(event.type + ", " + event.target);
}

Меняя значение переменной elem, можно проверить работу событий мыши mousedown и mouseup на разных объектах DOM (объектной модели документа), а также на объекте window окна браузера (как я понимаю, в иерархии объектов окно браузера находится выше документа, открытого в браузере).

Если просто нажимать и отпускать кнопку мыши над HTML-элементами, HTML-страницей или окном браузера, если на них повешены события мыши mousedown и mouseup, то всё происходит так, как написано в спецификации.

Однако, если навести курсор мыши на объект elem, нажать кнопку мыши (сгенерируется событие mousedown), а затем, не отпуская кнопку мыши, вывести курсор мыши за пределы объекта elem и уже там отпустить кнопку мыши, то при такой последовательности действий пользователя браузер сработает по-разному в зависимости от объекта в переменной elem. (Описанные действия пользователь обычно выполняет, когда хочет перетащить объект с помощью механизма «Drag’n’Drop».)

Для HTML-элементов div и body при вышеописанном порядке действий отпускание кнопки мыши за пределами этих HTML-элементов не влечет срабатывания функции-обработчика onMouse, повешенной на событие mouseup для указанных HTML-элементов. То есть, как я понимаю, это соответствует спецификации (событие mouseup происходит не над целевыми HTML-элементами, поэтому на них и не отлавливается).

А вот для HTML-элемента html, для объекта document (документ, загруженный браузером) и для объекта window (окно браузера) в той же ситуации отпускание кнопки мыши за пределами этих объектов влечет срабатывание функции-обработчика onMouse, повешенной на событие mouseup для указанных объектов. То есть, как я понимаю, это не соответствует спецификации (событие mouseup может происходить вообще за пределами окна браузера, но всё равно отлавливается).

Насчет этого момента в спецификации сделано замечание (по-английски «note», замечания в спецификации маркированы зеленым цветом фона):

In some implementation environments, such as a browser, a mouseup event can be dispatched even if the pointing device has left the boundary of the user agent, e.g., if the user began a drag operation with a mouse button pressed.


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

Получается, этот момент в спецификации оговорен, хоть там и не сказано конкретно, что данное исключение может действовать только на HTML-элемент html, объект document и объект window. Видимо, это решают разработчики конкретного браузера или другой подобной программы, реализующей данную спецификацию.