Текстовый дождь, как в «Матрице»
medium

Текстовый дождь, как в «Матрице»

Приятная кодовая безделушка.

Недавно мы делали проект про полёт в космос в 3D, где использовали для рисования библиотеку p5 и принципы ООП. Тогда мы ограничились одним классом и объектами на основе этого класса.

Сегодня будет интереснее — мы сделаем два класса, причём объекты одного класса будут состоять из объектов другого класса. Это нужно, чтобы воссоздать эффект «Матрицы»: когда буквы одновременно и падают, и сменяются. 

Что делаем

Тучку, из которой падают буквы, как в фильме «Матрица»:

Зачем мы это сделаем? Ради красоты, ради искусства, ради JavaScript.

Последовательность действий будет такая:

  1. Подготавливаем страницу и настраиваем стили.
  2. Рисуем тучку.
  3. Программируем падение и смену символов.
  4. Объединяем символы в потоки, чтобы они падали из тучки друг за другом.
  5. Запускаем.

Обратите внимание, что тучка и символы — это разные сущности, их можно спокойно использовать отдельно друг от друга. 

Подготовка страницы

Используем для проекта стандартный 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> рисует линию по заданным правилам. Вкратце эти правила можно описать так:

  1. Перемещаемся на нужные координаты.
  2. Рисуем линию от старых координат до новых.
  3. Перемещаемся на новые координаты, рисуем линию до старых и так повторяем много раз.
  4. Если нужно, в самом конце соединяем точку с текущими координатами с начальной точкой.

У нас ещё будет отдельная статья с разбором, как работает 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(), которая по кругу выполняет свой код. 

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

  1. Возьмём ширину тучки.
  2. Разделим её на ширину символа.
  3. Так мы получим, сколько потоков у нас визуально может выходить из тучки.
  4. Сформируем нужное количество потоков.
  5. Сами потоки будем стартовать внутри тучки — на старте они будут скрыты самой тучкой, но дадут хороший эффект появления, когда будут падать вниз.

Для анимации в функции 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-рисование в браузере — рисовать всякие классные и красивые штуки.

Текст и код:

Михаил Полянин

Редактура:

Максим Ильяхов

Художник:

Даня Берковский

Корректор:

Ирина Михеева

Вёрстка:

Мария Дронова

Соцсети:

Анастасия Гаврилова

Апскиллинг, как говорится
Апскиллинг — это, например, переход с уровня junior на уровень middle, а потом — senior. У «Яндекс Практикума» есть курсы ровно для этого: от алгоритмов и типов данных до модных фреймворков.
Изучить вопрос
Апскиллинг, как говорится Апскиллинг, как говорится Апскиллинг, как говорится Апскиллинг, как говорится
Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
medium