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

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

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

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

Текст и код:

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

Редактура:

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

Художник:

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

Корректор:

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

Вёрстка:

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

Соцсети:

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

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

Береги свой ум.

easy
Как сохранить JSON на сервере
Как сохранить JSON на сервере

И отдать его обратно по запросу.

medium
Делаем свой таймер на Python
Делаем свой таймер на Python

Код — проще, возможностей — больше.

easy
Что означает ошибка SyntaxError: non-default argument follows default argument
Что означает ошибка SyntaxError: non-default argument follows default argument

Аргументы функции идут не в том порядке

easy
Пишем свой блек-джек на Python
Пишем свой блек-джек на Python

Простая игра для серьёзных исследований

easy
Успокаивающие звуки на любой странице
Успокаивающие звуки на любой странице

Тревожное время требует кода на JavaScript.

easy
Как сделать свой сайт за 10 минут без программирования
Как сделать свой сайт за 10 минут без программирования

Для некоторых это становится источником постоянного дохода, если подойти к процессу с умом.

easy
Учим компьютер складывать числа любой длины
Учим компьютер складывать числа любой длины

Человек умеет складывать в столбик, а компьютер — нет

medium
Функция len() в Python
Функция len() в Python

Смотрим на встроенный метод для измерения объектов

easy
medium