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

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

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

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

Готовим страницу

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

<!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. Про­грам­ми­ро­вать про­сто, если научить­ся соби­рать такие кирпичики.

Текст:

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

Редак­ту­ра:

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

Худож­ник:

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

Кор­рек­тор:

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

Вёрст­ка:

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

Соц­се­ти:

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