Сегодня попробуем продемонстрировать мощь программирования на примере эффектной программы.
У нас в распоряжении будет команда, которая ставит один пиксель. Мы возьмём её и будем постепенно наращивать уровни абстракций. В итоге получится такое:
Чтобы было проще рисовать, сделаем страницу и разместим на ней холст. Пока на ней не будет ничего кроме холста и начала скрипта, где мы подготовим сам холст к работе.
<!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);
, чтобы не обновлять холст после каждого пикселя. Вместо этого мы поставим эту команду в самый конец скрипта и обновим всё разом.
Смотрите, что мы сделали:
- Взяли громоздкий код, который делает то, что нам нужно.
- Обернули его в функцию.
- Теперь мы можем использовать новый кирпичик — эту новую функцию.
Получается, что мы превратили сложный и длинный код во что-то простое, что можно использовать дальше. В этом первая сила программирования.
<!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, а прибавлять к ним.
⚠️ То, что мы сейчас сделали, — очень неэффективно. Нам пришлось вручную посчитать координаты каждой виртуальной точки, а затем также вручную соотнести их с координатами центра, чтобы получить реальные значения. Это неправильный путь. Правильный — поручить такие однотипные вычисления и команды машине.
Как нарисовать круг (более правильный путь)
Вот что нам нужно сделать, чтобы упростить себе жизнь и сразу получить нарисованный круг:
- Взять виртуальный круг с нужным радиусом.
- Найти диапазон точек по оси X, которые мы будем перебирать.
- Для каждой из них найти виртуальную координату точки на окружности по оси Y.
- Прибавить или вычесть координаты центра, чтобы получить настоящую координату.
- Поставить точку по этим координатам.
- Повторять пункты 3–5 до тех пор, пока не переберём все точки.
- Сделать то же самое для нижней полуокружности.
Запишем это на 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);
Следующий уровень: классы и объекты
Если мы хотим просто случайным образом заполнить наш экран кругами, то на этом можно уже остановиться: в цикле получаем случайные координаты и ставим круги. Но если нам нужно отслеживать положение каждого круга, например, для анимации, то текущих деталей будет недостаточно. Соберём более крутые детали для этого.
Так как у нас все круги устроены одинаково, то мы можем сделать так:
- Создать новый класс «Круг», в котором мы будем хранить координаты и отрисовывать круг.
- На основе этого класса создать много объектов-кругов с любыми параметрами.
- Все эти объекты отправим в массив, чтобы можно было обращаться к любому созданному кругу.
👉 Мы опять повысили степень абстракции и перешли от простых функций к классам и объектам. Это открывает перед нами новые возможности — мы сможем запоминать характеристики каждого круга и менять их в любой момент. Например, чтобы они двигались по экрану и отскакивали друг от друга.
// класс «Круг»
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();
Финал — рисуем много разноцветных кругов
Логика будет такая:
- У нас есть класс «Круг».
- Мы случайным образом получаем координаты круга, его радиус и цвет.
- Создаём новый объект класса «Круг».
- Этот объект добавляем в массив.
- Повторяем пункты 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();
}
<!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>
Выводы
- Все программы состоят из множества «кирпичиков» — фрагментов кода, которые делают что-то своё.
- Кирпичики могут быть простыми и сложными.
- Также они могут состоять из других кирпичиков, если это нужно программисту.
- Цель каждого кирпичика и каждой детали в коде — дать программисту одну команду вместо куска кода, чтобы было проще программировать дальше.
- Программировать просто, если научиться собирать такие кирпичики.