Возможности программирования
Фронтенд часто ассоциируется только с вёрсткой сайтов и разработкой приложений. Но это только верхушка айсберга под названием «программирование». Это бесконечные возможности для самореализации и выражения идей и мыслей, и невероятный инструмент в руках творческого человека. Я говорю про Generative Art (генеративное искусство) и визуализацию аудио.
Если объяснять простыми словами: выпишете код, генерирующий определённое изображение спомощью придуманных вамиже алгоритмов, оставляя при этом некоторую свободу ввиде случайных значений.Таким образом, каждое новое сгенерированное изображение будет уникальнои, втоже время, будет сохранять идентичность, которую выему задали.Это направление появилось вместе свозможностью визуализировать код истановится всё популярнее. Сразвитием генеративного искусства появились инструменты ифреймворки для людей, «программирующих искусство». Втом или ином виде они есть практически для всех языков программирования. Для С++— это Openframeworks, для JAVA— Processing, для Python иJavaScript— его модифицированные версии. Конечно, невсе используют готовые библиотеки, кто-то пишет нанативном коде, акто-то даже разрабатывает свои инструменты. Яиспользую p5.js, онём ипойдёт речь. Для того чтобы глубже вникнуть впроцесс разработки, давайте разберём одну изработ известного артиста всфере генеративного искусства. Это художник изАргентины Маноло Гамбоа. Вкачестве примера явзял одну изего последних иинтересных работ, еёвариации можно увидеть настранице Behance. Как правило, художники выкладывают несколько сгенерированных работ, чтобы было понятно, что именно икак меняется скаждой итерацией.
Попробуем сами
Давайте я покажу, как создать похожую работу шаг за шагом.
Начнём с настройки окружения для работы с p5.js. Все примеры будут в CodePen, поэтому я просто подключу к нему последнюю версию p5.js и lodash и напишу основные функции для работы.
В p5.js, как и в processing основные функции — setup() и draw().
setup() — здесь мы настраиваем и подготавливаем всё для работы.
draw() — отрисовывает результат. Стоит оговориться, что эта функция основана на requestAnimationFrame и мы можем не использовать её, если работаем только со статикой.
function setup() {
createCanvas(800, 800); // canvas
angleMode(DEGREES); // lets use degrees instead of radians
rectMode(CENTER); // lets our rectangles starts from center
ctx = drawingContext; // this one is for using native Js canvas features
x = width / 2; // x coordinate of center of canvas
y = height / 2; // y coordinate of center of canvas
}
Из рисунка видно, что это сетка с симметрично расположенными элементами. Давайте для начала реализуем один из них. Основная фигура — это квадрат со своеобразной окантовкой, прорисованной под углом, и создающей впечатление выпуклости.
Пробуем нарисовать квадрат:
function setup() {
createCanvas(800, 800);
angleMode(DEGREES);
rectMode(CENTER);
const ctx = drawingContext;
const x = width / 2;
const y = height / 2;
const squareSideDotsCount = 30;
const squareVertices = [];
let startAngle = 45;
for (let i = 0; i < 4; i += 1) {
squareVertices.push({
x: 400 * cos(startAngle),
y: 400 * sin(startAngle),
});
startAngle += 360 / 4;
}
const square = [];
for (let i = 0; i < 4; i += 1) {
for (let j = 0; j < squareSideDotsCount; j += 1) {
const x = lerp(
squareVertices[i].x,
squareVertices[(i + 1) % squareVertices.length].x,
j / squareSideDotsCount,
);
const y = lerp(
squareVertices[i].y,
squareVertices[(i + 1) % squareVertices.length].y,
j / squareSideDotsCount,
);
square.push({ x, y });
}
}
push();
translate(x, y);
for (let i = 0; i < square.length; i += 1) {
push();
noStroke();
if (i % 2 === 0) {
fill(0);
} else {
fill(255);
}
beginShape();
vertex(square[i].x, square[i].y);
vertex(0, 0);
vertex(
square[(i + 1) % square.length].x,
square[(i + 1) % square.length].y,
);
endShape(CLOSE);
pop();
}
pop();
}
Мне понадобился квадрат со сторонами, разбитыми на 30 точек. Я написал функцию square(), в которой использовал формулу для рисования круга по радиусу, только с четырьмя точками. Если бы точек, например, было 100, эта фигура была бы уже кругом.
Потом, используя эти вершины, вычислил 30 точек между ними, используя функцию lerp(), которая возвращает расстояние между двумя точками в соотношении к третьей переменной.
А дальше я прошёлся по массиву из получившихся точек и создал фигуру между каждой точкой, центром квадрата и соседней точкой. И покрасил чётные в один цвет, а нечётные — в другой.
У нас есть основа нашей фигуры. Давайте посмотрим на рисунок. Внутри квадрата расположена равносторонняя сетка, похожая на шахматное поле.
Попробуем её реализовать:
function setup() {
createCanvas(800, 800);
angleMode(DEGREES);
rectMode(CENTER);
const ctx = drawingContext;
const x = width / 2;
const y = height / 2;
const squareSideDotsCount = 30;
stroke(0);
const squareVertices = [];
let startAngle = 45;
for (let i = 0; i < 4; i += 1) {
squareVertices.push({
x: 400 * cos(startAngle),
y: 400 * sin(startAngle),
});
startAngle += 360 / 4;
}
Я написал два цикла, чтобы показать каким образом можно построить сетку. В нашем случае сетка с равными сторонами ячеек и её сторона состоит из 7 квадратов, как в той версии рисунка, который мы хотим повторить. Но её легко модифицировать и задать любой размер и параметры.
На своём рисунке Маноло Гамбоа использует два вида квадратов. Давайте попробуем реализовать каждый из них.
function setup() {
createCanvas(800, 800);
angleMode(DEGREES);
rectMode(CENTER);
const ctx = drawingContext;
const x = width / 2;
const y = height / 2;
const squareSideDotsCount = 30;
stroke(0);
const squareVertices = [];
let startAngle = 45;
for (let i = 0; i < 4; i += 1) {
squareVertices.push({
x: 400 * cos(startAngle),
y: 400 * sin(startAngle),
});
startAngle += 360 / 4;
}
const square = [];
for (let i = 0; i < 4; i += 1) {
for (let j = 0; j < squareSideDotsCount; j += 1) {
const x = lerp(
squareVertices[i].x,
squareVertices[(i + 1) % squareVertices.length].x,
j / squareSideDotsCount,
);
const y = lerp(
squareVertices[i].y,
squareVertices[(i + 1) % squareVertices.length].y,
j / squareSideDotsCount,
);
square.push({ x, y });
}
}
push();
translate(x, y);
for (let i = 0; i < square.length; i += 1) {
push();
noStroke();
if (i % 2 === 0) {
fill(0);
} else {
fill(255);
}
beginShape();
vertex(square[i].x, square[i].y);
vertex(0, 0);
vertex(
square[(i + 1) % square.length].x,
square[(i + 1) % square.length].y,
);
endShape(CLOSE);
pop();
}
const innerRectSide = 520;
const cellCount = 7;
const grid = [];
const pointCount = cellCount ** 2;
const cellSide = innerRectSide / cellCount;
const startPoint = -(cellSide * (cellCount - 1)) / 2;
for (let rowIndex = 0; rowIndex < cellCount; rowIndex += 1) {
for (let colIndex = 0; colIndex < cellCount; colIndex += 1) {
grid.push({
x: startPoint + colIndex * cellSide,
y: startPoint + rowIndex * cellSide,
});
}
}
for (let rowIndex = 0; rowIndex < cellCount; rowIndex += 1) {
for (let colIndex = 0; colIndex < cellCount; colIndex += 1) {
const x = grid[rowIndex * cellCount + colIndex].x;
const y = grid[rowIndex * cellCount + colIndex].y;
const halfWidth = cellSide / 2;
if (rowIndex % 2 === 1 && colIndex % 2 === 1) {
push();
fill(100);
rect(x, y, cellSide, cellSide)
pop();
push();
fill(200);
rect(x + 5, y + 5, 25, 25);
pop();
push();
fill(50);
rect(x, y, 25, 25);
pop();
push();
fill(255);
rect(x, y, 2, 2);
pop();
} else {
noStroke();
push();
makeLinearGradient(
ctx,
x - halfWidth,
y - halfWidth,
x + halfWidth,
y - halfWidth,
[0, 1],
['rgb(255, 0, 0)', 'rgb(255, 255, 255)'],
)
triangle(
x - halfWidth,
y - halfWidth,
x + halfWidth,
y - halfWidth,
x + halfWidth,
y + halfWidth,
);
pop();
push();
fill('rgb(170, 57, 57)');
triangle(
x - halfWidth,
y - halfWidth,
x - halfWidth,
y + halfWidth,
x + halfWidth,
y + halfWidth,
);
pop();
push();
fill(0);
circle(x, y, 30);
pop();
push();
fill(150);
circle(x + 5, y + 5, 5);
pop();
push();
fill(255);
circle(x, y, 3);
pop();
push();
strokeWeight(2);
stroke(255);
line(x - halfWidth, y, x + halfWidth, y);
pop();
push();
strokeWeight(2);
stroke(255);
line(x, y - halfWidth, x, y + halfWidth);
pop();
}
}
}
pop();
};
const draw = () => {
console.log('asfdasf')
}
const makeLinearGradient = (
ctx,
x1,
y1,
x2,
y2,
colorStops,
colors,
) => {
const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
colorStops.forEach((stop, i) => gradient.addColorStop(stop, colors[i]));
ctx.fillStyle = gradient;
return gradient;
};
Первый тип квадратов состоит из двух треугольников, один из которых залит градиентом.
Так как p5.js не имеет функции для заливки градиентом, пришлось написать её подобие через доступ к getDrawingContext().
Помимо двух треугольников также есть большой внешний круг, внутренний круг и его «тень», чуть сдвинутая вниз и влево, и две пересекающиеся линии.
Второй тип — основной квадрат, внутренний, его тень, и самый маленький посередине.
Выглядит не очень, поэтому самое время подобрать цвета. У меня нет цели полностью скопировать работу Маноло. Так что палитру выберу сам и постараюсь сделать так, чтобы вы и сами смогли поменять её по своему вкусу.
const hexToRgb = hex =>
hex
.replace(
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
(m, r, g, b) => #${r}${r}${g}${g}${b}${b},
)
.substring(1)
.match(/.{2}/g)
.map(x => parseInt(x, 16));
const colors = #708FA3 #486F88 #29526D #123852 #032236 #FFC0AA #D4856A #AA5639 #803015 #551600 #FFE9AA #D4B96A #AA8C39 #806415 #553F00
.split('\n')
.map(hex => hexToRgb(hex));
function setup() {
createCanvas(800, 800);
angleMode(DEGREES);
rectMode(CENTER);
const ctx = drawingContext;
const x = width / 2;
const y = height / 2;
const squareSideDotsCount = 30;
stroke(0);
const squareVertices = [];
let startAngle = 45;
for (let i = 0; i < 4; i += 1) {
squareVertices.push({
x: 400 * cos(startAngle),
y: 400 * sin(startAngle),
});
startAngle += 360 / 4;
}
const square = [];
for (let i = 0; i < 4; i += 1) {
for (let j = 0; j < squareSideDotsCount; j += 1) {
const x = lerp(
squareVertices[i].x,
squareVertices[(i + 1) % squareVertices.length].x,
j / squareSideDotsCount,
);
const y = lerp(
squareVertices[i].y,
squareVertices[(i + 1) % squareVertices.length].y,
j / squareSideDotsCount,
);
square.push({ x, y });
}
}
push();
translate(x, y);
const borderColors = .sampleSize(colors, 2);
for (let i = 0; i < square.length; i += 1) {
push();
noStroke();
if (i % 2 === 0) {
fill(borderColors[0]);
} else {
fill(borderColors[1]);
}
beginShape();
vertex(square[i].x, square[i].y);
vertex(0, 0);
vertex(
square[(i + 1) % square.length].x,
square[(i + 1) % square.length].y,
);
endShape(CLOSE);
pop();
}
const innerRectSide = 520;
const cellCount = 7;
const grid = [];
const pointCount = cellCount ** 2;
const cellSide = innerRectSide / cellCount;
const startPoint = -(cellSide * (cellCount - 1)) / 2;
for (let rowIndex = 0; rowIndex < cellCount; rowIndex += 1) {
for (let colIndex = 0; colIndex < cellCount; colIndex += 1) {
grid.push({
x: startPoint + colIndex * cellSide,
y: startPoint + rowIndex * cellSide,
});
}
}
for (let rowIndex = 0; rowIndex < cellCount; rowIndex += 1) {
for (let colIndex = 0; colIndex < cellCount; colIndex += 1) {
const x = grid[rowIndex * cellCount + colIndex].x;
const y = grid[rowIndex * cellCount + colIndex].y;
const halfWidth = cellSide / 2;
push();
fill(255);
rect(x, y, cellSide, cellSide)
pop();
if (rowIndex % 2 === 1 && colIndex % 2 === 1) {
push();
fill(.sample(colors));
rect(x, y, cellSide, cellSide)
pop();
push();
fill(.sample(colors.map(c =>rgba(${c}, 0.4)
)));
rect(x + 5, y + 5, 25, 25);
pop();
push();
fill(.sample(colors));
rect(x, y, 25, 25);
pop();
push();
fill(.sample(colors));
rect(x, y, 2, 2);
pop();
} else {
noStroke();
push();
const gradientColors = .sampleSize(colors.map(c =>rgba(${c}, 0.6)
), 2);
makeLinearGradient(
ctx,
x - halfWidth,
y - halfWidth,
x + halfWidth,
y - halfWidth,
[0, 1],
gradientColors,
)
triangle(
x - halfWidth,
y - halfWidth,
x + halfWidth,
y - halfWidth,
x + halfWidth,
y + halfWidth,
);
pop();
push();
fill(.sample(colors));
triangle(
x - halfWidth,
y - halfWidth,
x - halfWidth,
y + halfWidth,
x + halfWidth,
y + halfWidth,
);
pop();
push();
fill(.sample(colors));
circle(x, y, 30);
pop();
push();
fill(.sample(colors.map(c =>rgba(${c}, 0.4)
)));
circle(x + 5, y + 5, 5);
pop();
push();
fill(.sample(colors));
circle(x, y, 3);
pop();
push();
strokeWeight(2);
stroke(.sample(colors));
line(x - halfWidth, y, x + halfWidth, y);
pop();
push();
strokeWeight(2);
stroke(.sample(colors));
line(x, y - halfWidth, x, y + halfWidth);
pop();
}
}
}
pop();
}
const makeLinearGradient = (
ctx,
x1,
y1,
x2,
y2,
colorStops,
colors,
) => {
const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
colorStops.forEach((stop, i) => gradient.addColorStop(stop, colors[i]));
ctx.fillStyle = gradient;
return gradient;
};
@sdgfqer, Поздравляю!
Ваш пост был упомянут в моем хит-параде в следующей категории:
@sdgfqer, поздравляю! Вы добились некоторого прогресса на Голосе и были награждены следующими новыми бейджами:
Вы опубликовали свой первый пост
Вы впервые проголосовали
Вы получили первый голос за ваши посты
Награда за количество полученных голосов
Вы можете нажать на бейдж, чтобы увидеть свою страницу на Доске Почета.
Если вы больше не хотите получать уведомления, ответьте на этот комментарий словом
стоп
Лига Новичков приветствует тебя, новый пользователь!
Мы рады людям, которые стремятся привнести на платформу свои знания — или получить их, свой опыт — или обрести его, да и просто поделиться своим видением мира!
У нас есть Вики Голоса, в разделе для новичков много полезного.
Желаем тебе удачи, творческих полётов и реализации планов!
С уважением, команда @vp-liganovi4kov