Недавно мы делали проект про полёт в космос в 3D, где использовали для рисования библиотеку p5 и принципы ООП. Тогда мы ограничились одним классом и объектами на основе этого класса.
Сегодня будет интереснее — мы сделаем два класса, причём объекты одного класса будут состоять из объектов другого класса. Это нужно, чтобы воссоздать эффект «Матрицы»: когда буквы одновременно и падают, и сменяются.
Что делаем
Тучку, из которой падают буквы, как в фильме «Матрица»:
Зачем мы это сделаем? Ради красоты, ради искусства, ради JavaScript.
Последовательность действий будет такая:
- Подготавливаем страницу и настраиваем стили.
- Рисуем тучку.
- Программируем падение и смену символов.
- Объединяем символы в потоки, чтобы они падали из тучки друг за другом.
- Запускаем.
Обратите внимание, что тучка и символы — это разные сущности, их можно спокойно использовать отдельно друг от друга.
Подготовка страницы
Используем для проекта стандартный HTML-шаблон.
<!DOCTYPE html>
<html lang="ru" >
<head>
<meta charset="UTF-8">
<title>Текстовый дождь из тучки</title>
<style type="text/css">
</style>
</head>
<body>
<!-- основной скрипт -->
<script type="text/javascript">
</script>
</body>
</html>
Чтобы символы выглядели красиво и мы могли ими управлять, подключим стили Font Awesome — это такой набор правил по оформлению шрифтов.
<link rel=’stylesheet’ href=’https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css’>
Настраиваем стили
Сначала сделаем то же самое, что и в проекте со звёздами — разрешим задавать размеры всех элементов в разных единицах одновременно, обнулим отступы и растянем страницу на всё окно браузера.
Затем зададим общие свойства для той области, где будет нарисована тучка — размеры и положение. Отдельно пропишем свойство z-index — оно виртуально приподнимет слой с тучкой выше остальных, чтобы тучка закрывала те символы, которые появились внутри неё, но которым ещё рано появляться.
Последнее, что сделаем здесь, — настроим саму область, где будут видны падающие буквы. Всё это пока делается чистым CSS, никакого программирования.
/* сделаем так, чтобы мы могли задавать размеры всех элементов в разных единицах одновременно, например, высоту в пикселях, а ширину в процентах */
*{
box-sizing: border-box;
}
/* общие настройки для страницы */
body{
/* убираем отступы */
margin: 0;
padding: 0;
/* занимаем всю ширину и высоту окна браузера */
width: 100%;
height: 100vh;
/* не рисуем то, что вылетело за границы */
overflow: hidden;
/* цвет фона */
background: #2d3436;
/* располагаем элементы страницы по центру*/
display: flex;
justify-content: center;
align-items: center;
}
/* настройки области, где будет нарисована тучка */
.cloud-svg{
/* высота и ширина области */
width: 18rem;
height: 18rem;
position: fixed;
/* центрируем по горизонтали */
top: 120px;
left: 50%;
transform: translate(-50%, -50%);
/* поднимаем тучку выше остальных элементов на странице, чтобы она прятала символы, которые ещё не появились*/
z-index: 10;
}
/* настройки для отрисовки самой тучки */
.cloud{
/* настройки фона и контура тучки */
fill: #2ecc71;
stroke: #27ae60;
stroke-width: 2px;
stroke-linecap: round;
stroke-miterlimit: 10;
}
/* настройка области, где будут падающие буквы */
canvas{
position: fixed;
/* отступ сверху */
top: 350px;
/* центрируем по горизонтали*/
left: 50%;
transform: translate(-50%, -50%);
}
Рисуем тучку
Чтобы нарисовать тучку, используем теги <svg>
и <path>
. Первый тэг <svg>
говорит браузеру, что в выбранном блоке будем рисовать векторную графику.
👉 Векторная графика состоит не из отдельных пикселей, а из элементов — линий, прямоугольников, кругов и прочих геометрических фигур. Эти фигуры можно объединять друг с другом, рисовать их любого размера и цвета и комбинировать как угодно, чтобы получить нужный рисунок. Подробно мы это разобрали в отдельной статье про графику.
Тэг <path>
рисует линию по заданным правилам. Вкратце эти правила можно описать так:
- Перемещаемся на нужные координаты.
- Рисуем линию от старых координат до новых.
- Перемещаемся на новые координаты, рисуем линию до старых и так повторяем много раз.
- Если нужно, в самом конце соединяем точку с текущими координатами с начальной точкой.
У нас ещё будет отдельная статья с разбором, как работает SVG, а пока просто используем готовый код:
<!-- рисуем тучку -->
<div id="wrapper">
<svg class="cloud-svg" x="0px" y="0px" viewBox="0 0 60 60" style="enable-background:new 0 0 60 60;">
<path class="cloud" d="M50.003,27 c-0.115-8.699-7.193-16-15.919-16c-5.559,0-10.779,3.005-13.661,7.336C19.157,17.493,17.636,17,16,17c-4.418,0-8,3.582-8,8 c0,0.153,0.014,0.302,0.023,0.454C8.013,25.636,8,25.82,8,26c-3.988,1.912-7,6.457-7,11.155C1,43.67,6.33,49,12.845,49h24.507 c0.138,0,0.272-0.016,0.408-0.021C37.897,48.984,38.031,49,38.169,49h9.803C54.037,49,59,44.037,59,37.972 C59,32.601,55.106,27.961,50.003,27z"/>
<!-- дополнительные штрихи на тучке для объёма -->
<path class="cloud" d="M50.003,27 c0,0-2.535-0.375-5.003,0"/>
<path class="cloud" d="M8,25c0-4.418,3.582-8,8-8 s8,3.582,8,8"/>
</svg>
</div>
Что такое объекты, классы, методы и конструкторы?
Далее мы будем использовать понятия «объект», «метод», «класс» и «конструктор». Это базовые понятия ООП. Если хотите углубиться — читайте наш цикл, а пока вот основные положения:
Объект — это «коробка», в которой могут лежать данные и действия. В нашем случае объектами будут отдельные сменяющиеся буквы и «потоки» из этих букв. Объекты можно вкладывать в объекты.
Метод — это действие, которое может совершать объект. Например, символ из тучки может опускаться на сколько-то вниз. Если метод доступен извне объекта, то мы можем в программе сказать, грубо говоря, так: Буква.капни()
, где буква
— это объект, а капни()
— это метод.
Класс — это «чертёж», по которому наша программа может изготовить много одинаковых объектов, как на конвейере. В нашем случае нам нужно, чтобы программа рисовала сотни букв в десятках потоков, и изготавливать их мы будем не вручную, а с помощью класса.
Конструктор — это действие, которое совершает программа при создании объекта. Без конструктора объект просто висел бы в памяти, а с конструктором он сразу при создании будет выполнять какое-то действие. В нашем случае конструктор будет у класса «Поток», чтобы при создании такого потока в него сразу добавлялось несколько сменяющихся букв.
Готовим основной скрипт
Вся дождевая магия будет происходить в отдельном скрипте, который мы напишем после кода с тучкой. Чтобы рисовать было проще, подключим библиотеку p5 и пропишем основные переменные для скрипта:
<!-- подключаем p5 — библиотеку для рисования в JavaScrpt -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.8.0/p5.min.js'></script>
<script type="text/javascript">
// размеры области, где будет падать текст
var height = 420, width = 250;
// на старте массив символов пуст
var symbols = [];
// размер символа
var symbolSize = 10;
// переменная для таймера
var timer = 0;
// на старте массив с потоками из символов тоже пуст
var streams = [];
</script>
Проектируем поведение символов
Чтобы не прописывать поведение для каждого символа в отдельности, а задать общие правила для всех, сделаем класс Symbol. Каждый объект, созданный на основе этого класса, будет вести себя так, как написано в классе. Это даст нам предсказуемое поведение для каждого символа и сократит количество кода.
В конструкторе класса мы пропишем начальные координаты для символа, его скорость, а также смену символа через некоторое время, чтобы получить эффект как в «Матрице». Чтобы все символы не обновлялись одновременно, используем в конструкторе генератор случайных чисел — он выберет случайное время обновления для каждого нового символа.
Также сделаем метод rain() — он будет отвечать за падение символа вниз, как капля дождя. Логика простая: если символ ещё не улетел за край, сдвигаем его вниз на значение скорости.
// класс, на основе которого будут сделаны все символы, падающие из тучки
class Symbol{
// конструктор, который вызывается при создании каждого объекта на основе этого класса
constructor(x, y, speed){
// координаты и скорость нового символа
this.x = x;
this.y = y;
this.speed = speed;
// время переключения на новый символ
this.switchInterval = round(random(5, 20));
this.value;
// метод, который выбирает символ из случайного диапазона
this.setRandomSymbol = function(){
// если пришло время обновить символ —
if(timer % this.switchInterval == 0){
// берём новый из случайного диапазона
this.value = String.fromCharCode(
// если поменять стартовый диапазон — поменяется и язык символов
0x10A0 + round(random(0, 96))
);
}
}
}
// метод, который отвечает за падение символов
rain(){
// если ещё не достигли нижней границы — символ смещается вниз с установленной скоростью
this.y = (this.y >= height) ? 0: this.y += this.speed;
}
}
Собираем символы в потоки
Следующий шаг — собрать символы в потоки, чтобы они красиво падали из тучки. Поток — это от 2 до 5 символов, падающих друг за другом. При этом каждый символ независимо от других меняется во время падения, потому что мы прописали это в классе символов.
Конструктор просто создаёт массив случайного размера, а метод generateSymbols(x,y) как раз и наполнит этот массив нужными символами. При этом каждый символ — это объект класса Symbol, свойство которых мы написали выше.
Отдельный метод render() будет отвечать за отрисовку потока. Обратите внимание — внутри этого метода мы вызываем метод из другого класса, который отвечает за падение символов.
// класс, на основе которого будут сделаны все потоки, состоящие из нескольких символов
class Stream{
// конструктор, который вызывается при создании каждого объекта на основе этого класса
constructor(){
// на старте в каждом потоке ничего нет
this.stream = [];
// в каждом потоке будет от 2 до 5 символов
this.streamLength = round(random(2,5));
// выбираем скорость потока случайным образом
this.speed = random(5, 8);
}
// метод, который создаёт последовательность символов в потоке
generateSymbols(x,y){
// пока есть свободные места в массиве с потоком
for(let i = 0; i <= this.streamLength; i++){
// создаём новый объект-символ на основе класса для символов
var symbol = new Symbol(x, y, this.speed);
// формируем его значение случайным образом встроенным методом
symbol.setRandomSymbol();
// отправляем этот символ в массив с потоком
this.stream.push(symbol);
// следующий символ в потоке добавится под уже существующими
y -= symbolSize;
}
}
// метод, который отрисовывает поток символов на экране
render(){
// обрабатываем каждый символ в потоке
this.stream.forEach(symbol => {
// устанавливаем цвет символов
fill('#00b894');
// пишем на экране нужный символ по заданным координатам
text(symbol.value, symbol.x, symbol.y)
// обновляем символы, чтобы они менялись, как в Матрице
symbol.setRandomSymbol();
// смещаем их вниз, чтобы создать эффект дождя
symbol.rain();
});
}
}
Запуск
В проекте со звёздами мы выяснили, что для запуска и работы библиотеки p5 используются две функции — setup() для подготовки к запуску и draw(), которая по кругу выполняет свой код.
Для подготовки нам нужно создать холст, где будем рисовать падающие буквы и сформировать потоки. Потоки будем делать так:
- Возьмём ширину тучки.
- Разделим её на ширину символа.
- Так мы получим, сколько потоков у нас визуально может выходить из тучки.
- Сформируем нужное количество потоков.
- Сами потоки будем стартовать внутри тучки — на старте они будут скрыты самой тучкой, но дадут хороший эффект появления, когда будут падать вниз.
Для анимации в функции draw() мы будем делать всего две вещи — постоянно очищать фон и отрисовывать потоки.
// подготавливаем всё к запуску — то, что написано здесь выполнится автоматически сразу после загрузки
function setup(){
// задаём размеры холста и рисуем его
var height = 420, width = 220;
var c = createCanvas(width, height);
// находим блок с облачком по имени
c.parent('wrapper')
var x = 0;
// пока у нас есть место по ширине для потоков из тучки
for(let i = 0; i <= width/symbolSize; i++){
// все потоки формируем внутри самой тучки, невидимо для зрителя
var y = random(-300, 0);
// создаём новый поток на основе класса
var stream = new Stream();
// заполняем его символами
stream.generateSymbols(x, y);
// и отправляем в массив с потоками
streams.push(stream);
// сдвигаем координаты вправо для следующего потока
x += symbolSize;
}
// устанавливаем размер символов
textSize(symbolSize)
}
// пока мы не закроем страницу, постоянно будет выполняться функция draw()
function draw(){
// цвет фона
background('#2d343680');
// берём каждый поток…
streams.forEach(stream => {
// … и отрисовываем его
stream.render();
})
// при каждой отрисовке увеличиваем значение таймера (оно нам нужно для смены символов через определённое время)
timer++;
}
Можно скопировать и запустить у себя готовый код, а можно посмотреть на тучку на странице проекта.
<!DOCTYPE html>
<html lang="ru" >
<head>
<meta charset="UTF-8">
<title>Текстовый дождь из тучки</title>
<!-- подключаем стили для красивого оформления символов -->
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css'>
<style type="text/css">
/* сделаем так, чтобы мы могли задавать размеры всех элементов в разных единицах одновременно, например, высоту в пикселях, а ширину в процентах */
*{
box-sizing: border-box;
}
/* общие настройки для страницы */
body{
/* убираем отступы */
margin: 0;
padding: 0;
/* занимаем всю ширину и высоту окна браузера */
width: 100%;
height: 100vh;
/* не рисуем то, что вылетело за границы */
overflow: hidden;
/* цвет фона */
background: #2d3436;
/* располагаем элементы страницы по центру*/
display: flex;
justify-content: center;
align-items: center;
}
/* настройки области, где будет нарисована тучка */
.cloud-svg{
/* высота и ширина области */
width: 18rem;
height: 18rem;
position: fixed;
/* центрируем по горизонтали */
top: 120px;
left: 50%;
transform: translate(-50%, -50%);
/* поднимаем тучку выше остальных элементов на странице, чтобы она прятала символы, которые ещё не появились*/
z-index: 10;
}
/* настройки для отрисовки самой тучки */
.cloud{
/* настройки фона и контура тучки */
fill: #2ecc71;
stroke: #27ae60;
stroke-width: 2px;
stroke-linecap: round;
stroke-miterlimit: 10;
}
/* настройка области, где будут падающие буквы */
canvas{
position: fixed;
/* отступ сверху */
top: 350px;
/* центрируем по горизонтали*/
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<!-- рисуем тучку -->
<div id="wrapper">
<svg class="cloud-svg" x="0px" y="0px" viewBox="0 0 60 60" style="enable-background:new 0 0 60 60;">
<path class="cloud" d="M50.003,27 c-0.115-8.699-7.193-16-15.919-16c-5.559,0-10.779,3.005-13.661,7.336C19.157,17.493,17.636,17,16,17c-4.418,0-8,3.582-8,8 c0,0.153,0.014,0.302,0.023,0.454C8.013,25.636,8,25.82,8,26c-3.988,1.912-7,6.457-7,11.155C1,43.67,6.33,49,12.845,49h24.507 c0.138,0,0.272-0.016,0.408-0.021C37.897,48.984,38.031,49,38.169,49h9.803C54.037,49,59,44.037,59,37.972 C59,32.601,55.106,27.961,50.003,27z"/>
<!-- дополнительные штрихи на тучке для объёма -->
<path class="cloud" d="M50.003,27 c0,0-2.535-0.375-5.003,0"/>
<path class="cloud" d="M8,25c0-4.418,3.582-8,8-8 s8,3.582,8,8"/>
</svg>
</div>
<!-- подключаем p5 — библиотеку для рисования в JavaScrpt -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.8.0/p5.min.js'></script>
<script type="text/javascript">
// размеры области, где будет падать текст
var height = 420, width = 250;
// на старте массив символов пуст
var symbols = [];
// размер символа
var symbolSize = 10;
// переменная для таймера
var timer = 0;
// на старте массив с потоками из символов тоже пуст
var streams = [];
// класс, на основе которого будут сделаны все символы, падающие из тучки
class Symbol{
// конструктор, который вызывается при создании каждого объекта на основе этого класса
constructor(x, y, speed){
// координаты и скорость нового символа
this.x = x;
this.y = y;
this.speed = speed;
// время переключения на новый символ
this.switchInterval = round(random(5, 20));
this.value;
// метод, который выбирает символ из случайного диапазона
this.setRandomSymbol = function(){
// если пришло время обновить символ —
if(timer % this.switchInterval == 0){
// берём новый из случайного диапазона
this.value = String.fromCharCode(
// если поменять стартовый диапазон — поменяется и язык символов
0x10A0 + round(random(0, 96))
);
}
}
}
// метод, который отвечает за падение символов
rain(){
// если ещё не достигли нижней границы — символ смещается вниз с установленной скоростью
this.y = (this.y >= height) ? 0: this.y += this.speed;
}
}
// класс, на основе которого будут сделаны все потоки, состоящие из нескольких символов
class Stream{
// конструктор, который вызывается при создании каждого объекта на основе этого класса
constructor(){
// на старте в каждом потоке ничего нет
this.stream = [];
// в каждом потоке будет от 2 до 5 символов
this.streamLength = round(random(2,5));
// выбираем скорость потока случайным образом
this.speed = random(5, 8);
}
// метод, который создаёт последовательность символов в потоке
generateSymbols(x,y){
// пока есть свободные места в массиве с потоком
for(let i = 0; i <= this.streamLength; i++){
// создаём новый объект-символ на основе класса для символов
var symbol = new Symbol(x, y, this.speed);
// формируем его значение случайным образом встроенным методом
symbol.setRandomSymbol();
// отправляем этот символ в массив с потоком
this.stream.push(symbol);
// следующий символ в потоке добавится под уже существующими
y -= symbolSize;
}
}
// метод, который отрисовывает поток символов на экране
render(){
// обрабатываем каждый символ в потоке
this.stream.forEach(symbol => {
// устанавливаем цвет символов
fill('#00b894');
// пишем на экране нужный символ по заданным координатам
text(symbol.value, symbol.x, symbol.y)
// обновляем символы, чтобы они менялись, как в Матрице
symbol.setRandomSymbol();
// смещаем их вниз, чтобы создать эффект дождя
symbol.rain();
});
}
}
// подготавливаем всё к запуску — то, что написано здесь выполнится автоматически сразу после загрузки
function setup(){
// задаём размеры холста и рисуем его
var height = 420, width = 220;
var c = createCanvas(width, height);
// находим блок с облачком по имени
c.parent('wrapper')
var x = 0;
// пока у нас есть место по ширине для потоков из тучки
for(let i = 0; i <= width/symbolSize; i++){
// все потоки формируем внутри самой тучки, невидимо для зрителя
var y = random(-300, 0);
// создаём новый поток на основе класса
var stream = new Stream();
// заполняем его символами
stream.generateSymbols(x, y);
// и отправляем в массив с потоками
streams.push(stream);
// сдвигаем координаты вправо для следующего потока
x += symbolSize;
}
// устанавливаем размер символов
textSize(symbolSize)
}
// пока мы не закроем страницу, постоянно будет выполняться функция draw()
function draw(){
// цвет фона
background('#2d343680');
// берём каждый поток…
streams.forEach(stream => {
// … и отрисовываем его
stream.render();
})
// при каждой отрисовке увеличиваем значение таймера (оно нам нужно для смены символов через определённое время)
timer++;
}
</script>
</body>
</html>
Что дальше
Будем осваивать SVG-рисование в браузере — рисовать всякие классные и красивые штуки.