Сила машин. Объясняем на пузырях
hard

Сила машин. Объясняем на пузырях

Сила — в повторениях и абстракции.

Сегодня попробуем продемонстрировать мощь программирования на примере эффектной программы. 

У нас в распоряжении будет команда, которая ставит один пиксель. Мы возьмём её и будем постепенно наращивать уровни абстракций. В итоге получится такое:

Сегодня попробуем продемонстрировать мощь программирования на примере эффектной программы

Чтобы было проще рисовать, сделаем страницу и разместим на ней холст. Пока на ней не будет ничего кроме холста и начала скрипта, где мы подготовим сам холст к работе.

<!DOCTYPE html>
<html lang="ru" >
<head>
  	<meta charset="UTF-8">
	<title>Сила программирования</title>
</head>
<body>
	<canvas id="powerCanvas"></canvas>

	<script type="text/javascript">
		// подключаем холст к работе
		var canvas = document.getElementById("powerCanvas");
		// устанавливаем размер холста, равный размеру окна
		canvas.width = window.innerWidth;
		canvas.height = window.innerHeight;
		var canvasWidth = canvas.width;
		var canvasHeight = canvas.height;
		var ctx = canvas.getContext("2d");
		// получаем набор данных о том, что нарисовано внутри холста
		var canvasData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
	</script>
</body>
</html>

Стартовый кирпичик

Чтобы нарисовать один пиксель на холсте, нам понадобится 6 команд. Эти команды мы взяли с foobar.com, потому что, оказывается, в JS нет стандартной команды, чтобы поставить пиксель. Вот сам код, который мы добавляем в скрипт:

// вспомогательные переменные для работы кода
var x,y,z,r,g,b,a;	

// получаем порядковый номер пикселя на холсте
var index = (x + y * canvasWidth) * 4;
// устанавливаем цвет в формате RGBa
// красный оттенок пикселя
canvasData.data[index + 0] = r;
// зелёный
canvasData.data[index + 1] = g;
// и синий
canvasData.data[index + 2] = b;
// прозрачность пикселя
canvasData.data[index + 3] = a;

// отправляем на холст обновлённую информацию о пикселях
ctx.putImageData(canvasData, 0, 0);

Пробуем поставить пиксель

Чтобы добавить один пиксель на экран, нам нужно указать:

  • его координаты,
  • цвет в RGB,
  • прозрачность.

Например, чтобы поставить красный пиксель в точке (10,10) нужно будет написать так:

// вспомогательные переменные для работы кода
var x,y,z,r,g,b,a;	
// координаты
x = 10;
y = 10;

// цвет
r = 255;
g = 0;
b = 0;

// непрозрачность (чем меньше, тем прозрачнее)
a = 255;

// получаем порядковый номер пикселя на холсте
var index = (x + y * canvasWidth) * 4;
// устанавливаем цвет в формате RGBa
// красный оттенок пикселя
canvasData.data[index + 0] = r;
// зелёный
canvasData.data[index + 1] = g;
// и синий
canvasData.data[index + 2] = b;
// прозрачность пикселя
canvasData.data[index + 3] = a;

// отправляем на холст обновлённую информацию о пикселях
ctx.putImageData(canvasData, 0, 0);

Новая деталь — функция для пикселя

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

// функция для отрисовки одного пикселя
function drawPixel (x, y, r, g, b, a) {

	// получаем порядковый номер пикселя на холсте
	var index = (x + y * canvasWidth) * 4;
	// устанавливаем цвет в формате RGBa
	// красный оттенок пикселя
	canvasData.data[index + 0] = r;
	// зелёный
	canvasData.data[index + 1] = g;
	// и синий
	canvasData.data[index + 2] = b;
	// непрозрачность (чем меньше, тем прозрачнее)
	canvasData.data[index + 3] = a;
}

Теперь, когда мы захотим поставить любой пиксель, то просто вызываем эту функцию с нужными параметрами:

drawPixel(10, 10, 255, 0, 0, 255);

👉 Мы специально не ставим в эту функцию команду ctx.putImageData(canvasData, 0, 0); , чтобы не обновлять холст после каждого пикселя. Вместо этого мы поставим эту команду в самый конец скрипта и обновим всё разом.

Смотрите, что мы сделали:

  1. Взяли громоздкий код, который делает то, что нам нужно.
  2. Обернули его в функцию.
  3. Теперь мы можем использовать новый кирпичик — эту новую функцию.

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

<!DOCTYPE html>
<html lang="ru" >
<head>
  	<meta charset="UTF-8">
	<title>Сила программирования</title>
</head>
<body>
	<canvas id="powerCanvas"></canvas>

	<script type="text/javascript">
		// подключаем холст к работе
		var canvas = document.getElementById("powerCanvas");
		// устанавливаем размер холста, равный размеру окна
		canvas.width = window.innerWidth;
		canvas.height = window.innerHeight;
		var canvasWidth = canvas.width;
		var canvasHeight = canvas.height;
		var ctx = canvas.getContext("2d");
		// получаем набор данных о том, что нарисовано внутри холста
		var canvasData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);

		// функция для отрисовки одного пикселя
		function drawPixel (x, y, r, g, b, a) {

			// получаем порядковый номер пикселя на холсте
			var index = (x + y * canvasWidth) * 4;
			// устанавливаем цвет в формате RGBa
			// красный оттенок пикселя
			canvasData.data[index + 0] = r;
			// зелёный
			canvasData.data[index + 1] = g;
			// и синий
			canvasData.data[index + 2] = b;
			// непрозрачность (чем меньше, тем прозрачнее)
			canvasData.data[index + 3] = a;
		}

		drawPixel(10, 10, 255, 0, 0, 255);

		// отправляем на холст обновлённую информацию о пикселях
			ctx.putImageData(canvasData, 0, 0);
	</script>
</body>
</html>

Сегодня попробуем продемонстрировать мощь программирования на примере эффектной программы
У нас появился первый пиксель. Вся работа ещё впереди

Сейчас будет немного сложно. В школьной тригонометрии была такая формула:

a² + b² = r², где a и b это координаты точки на окружности, а r — радиус.

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

Если нам нужно расставить точки на окружности, то мы просто перебираем все точки по оси X и смотрим, каким координатам по Y они соответствуют. В нашем примере координаты по оси X будут такими:

−5, −4, −3, −2, −1, 0, 1, 2, 3, 4, 5

Это и есть значение a в формуле a² + b² = r².  Зная первую координату, мы можем посчитать вторую так:

a² + b² = r²

b² = r² – a²

b = √(r² – a²)

Теперь для каждой координаты a  считаем значение b, чтобы получить точку на окружности:

-5: b = √(5² – (-5)²) = 0 → 0

-4: b = √(5² – (-4)²) = 3 → 3

-3: b = √(5² – (-3)²) = 4 → 4

-2: b = √(5² – (-2)²) = 0 → 5 (здесь и дальше мы округляем в большую сторону, потому что у нас не может быть дробной координаты пикселя)

-1: b = √(5² – (-1)²) = 0 → 5

0: b = √(5² – (0)²) = 0 → 5

1: b = √(5² – (1)²) = 0 → 5

2: b = √(5² – (2)²) = 0 → 5

3: b = √(5² – (3)²) = 0 → 4

4: b = √(5² – (4)²) = 0 → 3

5: b = √(5² – (5)²) = 0 → 0

У нас появились относительные координаты. Чтобы превратить их в настоящие, мы первую координату a прибавим к первому значению координаты центра по оси X, а вторую отнимем от координаты центра по оси Y:

(10 +(–5),10 - 0) → (5,10)

(6,7)

(7,6)

(8,5)

(9,5)

(10,5)

(11,5)

(12,5)

(13,6)

(14,7)

(15,10)

Теперь пишем команды с установкой пикселя и смотрим, что получилось:

drawPixel(5, 10, 255, 0, 0, 255);
drawPixel(6, 7, 255, 0, 0, 255);
drawPixel(7, 6, 255, 0, 0, 255);
drawPixel(8, 5, 255, 0, 0, 255);
drawPixel(9, 5, 255, 0, 0, 255);
drawPixel(10, 5, 255, 0, 0, 255);
drawPixel(11, 5, 255, 0, 0, 255);
drawPixel(12, 5, 255, 0, 0, 255);
drawPixel(13, 6, 255, 0, 0, 255);
drawPixel(14, 7, 255, 0, 0, 255);
drawPixel(15, 10, 255, 0, 0, 255);

Сегодня попробуем продемонстрировать мощь программирования на примере эффектной программы

У нас получилась верхняя полуокружность.Чтобы нарисовать нижнюю, нужно ещё столько же команд, только теперь надо не отнимать от координат по Y, а прибавлять к ним.

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

Как нарисовать круг (более правильный путь)

Вот что нам нужно сделать, чтобы упростить себе жизнь и сразу получить нарисованный круг:

  1. Взять виртуальный круг с нужным радиусом.
  2. Найти диапазон точек по оси X, которые мы будем перебирать.
  3. Для каждой из них найти виртуальную координату точки на окружности по оси Y.
  4. Прибавить или вычесть координаты центра, чтобы получить настоящую координату.
  5. Поставить точку по этим координатам.
  6. Повторять пункты 3–5 до тех пор, пока не переберём все точки.
  7. Сделать то же самое для нижней полуокружности.

Запишем это на JavaScript:

// виртуальные координаты точек на окружности
var x,y;
// реальные координаты точек на окружности
var realX, realY;

// перебираем виртуальные координаты по оси X
for (x= -5; x < 6; x++) {
	// высчитываем виртуальную координату по оси Y 
	y = Math.round(Math.sqrt(5*5 - x*x));
	// считаем реальные координаты по осям, используя известные координаты центра
	realX = 10 + x;
	// верхняя полуокружность
	realY = 10 - y;
	drawPixel(realX, realY, 255, 0, 0, 255);
	// нижняя полуокружность
	realY = 10 + y;
	drawPixel(realX, realY, 255, 0, 0, 255);
}
Сегодня попробуем продемонстрировать мощь программирования на примере эффектной программы

👉 Есть способ убрать эти разрывы при рисовании. Например, можно сделать второй прогон: взять за основу координату Y, от неё посчитать координату X и наложить сверху на уже готовый круг. Но мы сейчас не будем этим заниматься, просто обозначаем возможности по улучшению.

Новая деталь — функция для круга

Проблема нашего кода сейчас в том, что в нём забиты фиксированные координаты центра круга (10,10), радиус 5 пикселей и их цвет. Если нам нужно будет нарисовать зелёный круг с центром (30,55) и радиусом 15, то нам придётся править каждую строчку.

Чтобы мы могли нарисовать круг любого цвета и радиуса в любом месте, нам нужно добавить абстракции: поставить переменные вместо каждого параметра и обернуть всё в функцию. Мы уже сделали это с пикселем, теперь сделаем то же самое для круга.

Обратите внимание — у нас повышается степень абстракции:

Команды для конкретного пикселя.

функция для любого пикселя, которая состоит их конкретных команд.

Много простых однотипных команд для круга, считаем вручную.

Цикл с командами, чтобы компьютер считал всё вместо нас.

Новая деталь: функция для круга, внутри которой лежит цикл с командами.

// новая деталь — функция для отрисовки круга
// передаём на вход координаты центра, радиус и цвет
function drawCircle(centreX, centreY, radius, r,g,b,a) {
	// виртуальные координаты точек на окружности
	var x,y;
	// реальные координаты точек на окружности
	var realX, realY;

	// перебираем виртуальные координаты по оси X
	for (x= -radius; x < radius + 1; x++) {
		// высчитываем виртуальную координату по оси Y 
		y = Math.round(Math.sqrt(radius * radius - x*x));
		// считаем реальные координаты по осям, используя известные координаты центра
		realX = centreX + x;
		// верхняя полуокружность
		realY = centreY - y;
		drawPixel(realX, realY, r, g, b, a);
		// нижняя полуокружность
		realY = centreY + y;
		drawPixel(realX, realY, r, g, b, a);
	}
}

// первый круг
drawCircle(10,10,5,255,0,0,255);

// второй круг
drawCircle(50,50,15,0,0,255,255);
Сегодня попробуем продемонстрировать мощь программирования на примере эффектной программы
Два круга в увеличенном масштабе

Следующий уровень: классы и объекты

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

Так как у нас все круги устроены одинаково, то мы можем сделать так:

  1. Создать новый класс «Круг», в котором мы будем хранить координаты и отрисовывать круг.
  2. На основе этого класса создать много объектов-кругов с любыми параметрами.
  3. Все эти объекты отправим в массив, чтобы можно было обращаться к любому созданному кругу.

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

// класс «Круг»
class Circle {
	// конструктор класса
	constructor(centreX, centreY, radius, r,g,b,a) {
		this.centreX = centreX;
		this.centreY = centreY;
		this.radius = radius;
		this.r = r;
		this.g = g;
		this.b = b;
		this.a = a;
	}
	// метод, который рисует круг — вызываем уже готовую функцию
	clDrawCircle() {
		drawCircle(this.centreX, this.centreY, this.radius, this.r,this.g,this.b,this.a);
	}

}

// создаём новый объект класса «Круг»
var cr1 = new Circle(20,20,5,255,0,0,255);
// вызываем метод отрисовки этого объекта
cr1.clDrawCircle();

// меняем радиус
cr1.radius = 15;
// добавляем новый оттенок цвета
cr1.b = 255;
// снова отрисовываем круг
cr1.clDrawCircle();
Сегодня попробуем продемонстрировать мощь программирования на примере эффектной программы

Финал — рисуем много разноцветных кругов

Логика будет такая:

  1. У нас есть класс «Круг».
  2. Мы случайным образом получаем координаты круга, его радиус и цвет.
  3. Создаём новый объект класса «Круг».
  4. Этот объект добавляем в массив.
  5. Повторяем пункты 2–4 много раз.

// генератор случайных чисел в диапазоне от минимального до максимального 
function randz(min, max) { 
	return Math.floor(Math.random() * (max - min + 1)) + min;
}

// в этом массиве будем хранить все круги
var arr = [];
// сделаем 50 кругов на странице
for (var i = 0; i < 50; i++) {
	// создаём и сразу отправляем в массив новый объект-круг с каждым случайным параметром
	arr.push(new Circle(randz(0,canvasWidth),randz(0,canvasHeight),randz(1,30),randz(0,255),randz(0,255),randz(0,255),randz(100,255)));
	// рисуем текущий круг
	arr[i].clDrawCircle();
}
Сегодня попробуем продемонстрировать мощь программирования на примере эффектной программы
Всё это мы получили, начав с 6 команд для одного пикселя

<!DOCTYPE html>
<html lang="ru" >
<head>
  	<meta charset="UTF-8">
	<title>Сила программирования</title>
</head>
<body>
	<canvas id="powerCanvas"></canvas>

	<script type="text/javascript">
		// подключаем холст к работе
		var canvas = document.getElementById("powerCanvas");
		// устанавливаем размер холста, равный размеру окна
		canvas.width = window.innerWidth;
		canvas.height = window.innerHeight;
		var canvasWidth = canvas.width;
		var canvasHeight = canvas.height;
		var ctx = canvas.getContext("2d");
		// получаем набор данных о том, что нарисовано внутри холста
		var canvasData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);

		// функция для отрисовки одного пикселя
		function drawPixel (x, y, r, g, b, a) {

			// получаем порядковый номер пикселя на холсте
			var index = (x + y * canvasWidth) * 4;
			// устанавливаем цвет в формате RGBa
			// красный оттенок пикселя
			canvasData.data[index + 0] = r;
			// зелёный
			canvasData.data[index + 1] = g;
			// и синий
			canvasData.data[index + 2] = b;
			// непрозрачность (чем меньше, тем прозрачнее)
			canvasData.data[index + 3] = a;
		}

		// новая деталь — функция для отрисовки круга
		// передаём на вход координаты центра, радиус и цвет
		function drawCircle(centreX, centreY, radius, r,g,b,a) {
			// виртуальные координаты точек на окружности
			var x,y;
			// реальные координаты точек на окружности
			var realX, realY;

			// перебираем виртуальные координаты по оси X
			for (x= -radius; x < radius + 1; x++) {
				// высчитываем виртуальную координату по оси Y 
				y = Math.round(Math.sqrt(radius * radius - x*x));
				// считаем реальные координаты по осям, используя известные координаты центра
				realX = centreX + x;
				// верхняя полуокружность
				realY = centreY - y;
				drawPixel(realX, realY, r, g, b, a);
				// нижняя полуокружность
				realY = centreY + y;
				drawPixel(realX, realY, r, g, b, a);
			}
		}

		// класс «Круг»
		class Circle {
			// конструктор класса
			constructor(centreX, centreY, radius, r,g,b,a) {
				this.centreX = centreX;
				this.centreY = centreY;
				this.radius = radius;
				this.r = r;
				this.g = g;
				this.b = b;
				this.a = a;
			}
			// метод, который рисует круг — вызываем уже готовую функцию
			clDrawCircle() {
				drawCircle(this.centreX, this.centreY, this.radius, this.r,this.g,this.b,this.a);
			}

		}

		// генератор случайных чисел в диапазоне от минимального до максимального 
		function randz(min, max) { 
			return Math.floor(Math.random() * (max - min + 1)) + min;
		}

		// в этом массиве будем хранить все круги
		var arr = [];
		// сделаем 50 кругов на странице
		for (var i = 0; i < 50; i++) {
			// создаём и сразу отправляем в массив новый объект-круг с каждым случайным параметром
			arr.push(new Circle(randz(0,canvasWidth),randz(0,canvasHeight),randz(1,30),randz(0,255),randz(0,255),randz(0,255),randz(100,255)));
			// рисуем текущий круг
			arr[i].clDrawCircle();
		}

		// отправляем на холст обновлённую информацию о пикселях
		ctx.putImageData(canvasData, 0, 0);
	</script>
</body>
</html>

Выводы

  1. Все программы состоят из множества «кирпичиков» — фрагментов кода, которые делают что-то своё.
  2. Кирпичики могут быть простыми и сложными.
  3. Также они могут состоять из других кирпичиков, если это нужно программисту.
  4. Цель каждого кирпичика и каждой детали в коде — дать программисту одну команду вместо куска кода, чтобы было проще программировать дальше.
  5. Программировать просто, если научиться собирать такие кирпичики.

Текст:

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

Редактура:

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

Художник:

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

Корректор:

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

Вёрстка:

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

Соцсети:

Олег Вешкурцев

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

И как стащить с сайта что угодно.

medium
Как писали игры для приставок: чудеса оптимизации и жёсткий кодинг

Для всех, кто вырос, проходя восьмибитного Марио.

medium
Как устроен и зачем нужен квантовый компьютер

Это прорыв в технологиях или очередной биткоин?

medium
Объясни мне: зачем нужен хостинг

Все говорят про какой-то хостинг. Что это вообще такое?

easy
Что такое UNIX и зачем он нужен
Что такое UNIX и зачем он нужен

Операционная система, которая изменила мир, хотя в ней почти никто не работал

medium
Что такое «безголовый Хром» и за что его любят разработчики
Что такое «безголовый Хром» и за что его любят разработчики

Браузер, который никто не видит

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

Инструкция новичкам от разработчика из Яндекс.Практикума.

easy
Резко врываемся в дату: чему учат и каким будет результат
Резко врываемся в дату: чему учат и каким будет результат

Профессии, которые будут перспективны ещё много лет

easy
«Я не успеваю писать код, но участвую во всех важных обсуждениях». Как работает руководитель разработки Яндекс.Практикума
«Я не успеваю писать код, но участвую во всех важных обсуждениях». Как работает руководитель разработки Яндекс.Практикума

От первого сайта за 300$ до руководителя в Яндексе.

hard