image

Недавно под вдохновением от канала The Coding Train я решил поучаствовать в одном из 10-минутных челленджей, в котором нужно было создать иллюзию полета сквозь космос с большой скоростью.

Для реализации проекта я выбрал уже хорошо знакомый мне p5.js — библиотеку для JavaScript, предназначенную для создания арта алгоритмическим способом. Почему нельзя было для этого использовать стандартные графические пакеты от Adobe?

Во-первых, делать такое кодом — это красиво.

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

Ну и в-третьих, этот код потом легко интегрировать в любой веб-проект в виде скрипта на JS.

Ну что же, в бесконечность и далее…

Чтобы начать проект, проще всего зайти в онлайн-редактор на сайте editor.p5js.org

Первым делом нам стоит создать звезды. Понятно, что нам нужно большое количество звезд, хотя технически в пространстве космоса несложно найти пространство, где невооруженным глазом можно вообще не увидеть звезд, однако наше представление об этом эффекте было создано при помощи научно-фантастических фильмов. Думаю, можно пренебречь ради эффектности точностью. Итак. Нам нужно сделать генератор рандомных звезд.

Для начала в самом верху создадим массив, который будет хранить наши звезды:

let stars = [];

Давайте создадим новую функцию с названием Star() У нее должно быть четыре ключевых параметра: положение по x, y и переменная z, через которую мы будем изменять все остальное. Для создания случайности возникновения изначальных точек мы можем воспользоваться функцией random(), а в качестве параметров задать границы нашего экрана по ширине и высоте.
Функция random() работает с 0, 1 или 2 аргументами. Без аргументов — выдает случайное число от 0 до 1, с одним — от 0 до аргумента, с двумя — от первого до второго.

Поскольку мы собираемся строить “завод” по производству звезд, давайте сразу введем ключевое слово this, которое будет обращаться и получать доступ к информации у объекта, из которого эту функцию (а точнее — метод) мы будем вызывать.

function Star() {
  this.x = random(-width, width);
  this.y = random(-height, height);
  this.z = width;

У опытных может возникнуть вопрос — мы что, собираемся делать реальный 3d? Но на самом деле гораздо проще сделать псевдо-3d, как в олдскульных играх. Переменная z здесь нужна для других целей. Она будет отвечать только за иллюзию третьего измерения.

Готово, хотя звезды пока не видны, но создавать мы их уже можем. Настало время придать им движение. Для этого мы расширяем нашу функцию Star() новым методом (вложенной функцией) update(), которая отвечает за бесконечный цикл изменений.

Нам нужно определиться со скоростью движения сквозь звезды. Для этого создадим переменную speed и сделаем ее равной 25. Логика следующая: мы изменяем параметр z, который будет в влиять на параметры x и y, чтобы меняя один параметр мы меняли все. Как только этот параметр достигает значения меньше единицы, мы откатываем параметры опять к рандомным начальным значениям, начиная новый цикл движения звезды по экрану.

this.update = function() {
    this.z = this.z - speed;
    if (this.z < 1) {
      this.x = random(-width, width);
      this.y = random(-height, height);
      this.z = width;
    }
  };

Что же, звезды создаются и даже движутся, настало время их увидеть. Для этого мы воспользуемся функцией show(), так же создав обращение к объекту через this и покрасив наши звезды в белый цвет.

this.show = function() {
    fill("white");
}

Теперь нам нужно создать форму для нашей звезды, причем так, чтобы иметь разнообразие в траекториях и размере. Делать это мы будем через все ту же переменную z, которая изначально равна ширине экрана (width), но с каждым фреймом уменьшается на 25, пока не станет меньше 1, после чего цикл повторяется.

Чтобы получить красивое движение звезд из центра к краю, давайте посмотрим, как должно меняться положение, а именно переменные x и y. Каждая звезда должна появляться на воображаемом луче и по нему же уходить к краю экрана.

Представим, что начальная точка — это координата 200,200. Нам нужно сделать так, чтобы оба этих показателя постепенно изменялись в сторону уменьшения

Поскольку z у нас всегда равна ширине экрана (к примеру — 600), а x и y варьируются от -600 до 600, переменные x и y на старте у нас всегда будут меньше или равны z. Это наводит на мысль, что мы можем перевести значения в более простые для управления значения от 0 до 1, просто разделяя x или y на z.

Далее мы можем сделать стандартную функцию маппинга — приравнять числа от 0 до 1 пропорционально ширине или высоте экрана. И именно эти значения уже передать в качестве параметров нашей звезде-шару: ellipse()

this.show = function() {
    fill("white");
    

    var sx = map(this.x / this.z, 0, 1, 0, width);
    var sy = map(this.y / this.z, 0, 1, 0, height);
    ellipse(sx, sy, 10, 10);

  1. x и y отрицательные
  2. x и y положительные
  3. x положительный, y отрицательный или наоборот.

Представим, что width = 600, это наш космос. x = -300, y = -300. В этом кейсе начальная точка будет -300/600 = -0,5, через 4 фрейма, когда z уменьшится на 4*25=100, координата будет уже -300/500=-0,6, т.е., координаты будут ЛИНЕЙНО уходить от центра экрана (координаты 0,0) вверх и влево, пока z не станет меньше 1.

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

В случае, если они отличаются, возможны вариант вниз и влево (при положительным y и отрицательном x) или вверх и вправо (при отрицательном y и положительном x).
Функция эллипса берет четыре параметра: положение по x, положение по y, диаметр по x и диаметр по y, на основе которых строит эллипс.

Ну что же, пора зажигать звезды и создавать космос.

Под starts и speed создаем стандартную функцию, создающую окружение (работает один раз) — setup() Внутри мы создаем наш холст — createCanvas(600,600) и запускаем наш завод по созданию звезд через цикл for, для начала ограничимся 800.

function setup() {
  createCanvas(600, 600);
  for (let i = 0; i < 800; i++) {
    stars[i] = new Star();
  }
}

После этого создаем вторую стандартную функцию — draw(), ее отличие в том, что она работает в бесконечном цикле. Красим фон через background() в черный цвет, после чего через цикл for считаем по длине массива stars (800) и, обращаясь к каждой звезде в массиве, вызываем функцию (в данном случае — метод) update() и show():

function draw() {
  background("black");
  for (let i = 0; i < stars.length; i++) {
    stars[i].update();
    stars[i].show();
  }
}

Все, мы полетели, но есть две проблемы. Первая — поскольку точка отсчета координат по умолчанию стоит не по центру, а в верхнем левом углу, то мы видим только ту часть звезд, которые не имеют изначально отрицательного значения по координатам. Остальные остаются за полем зрения.

image

Решить эту проблему достаточно легко — нужно просто перенести центр через команду translate(width / 2, height / 2).

image

Вторая проблема — звезды не меняют свой размер при приближении к наблюдателю. На подсознательном уровне мы чувствуем, что что-то не то.

Для того чтобы решить эту проблему, давайте вернемся к нашему эллипсу, создадим еще одну переменную r, которая так же будет зависеть от z и будет определять третий и четвертый параметры эллипса, отвечающие за диаметр по горизонтали и вертикали. Финальный код получится следующий:

let stars = [];
let speed = 25;

function setup() {
  createCanvas(600, 600);
  for (let i = 0; i < 800; i++) {
    stars[i] = new Star();
  }
}

function draw() {
  background("black");
  translate(width / 2, height / 2);
  for (let i = 0; i < stars.length; i++) {
    stars[i].update();
    stars[i].show();
  }
}

function Star() {
  this.x = random(-width, width);
  this.y = random(-height, height);
  this.z = random(width);
 
  this.update = function() {
    this.z = this.z - speed;
    if (this.z < 1) {
      this.x = random(-width, width);
      this.y = random(-height, height);
      this.z = width;
    }
  };

  this.show = function() {
    fill("white");
    var sx = map(this.x / this.z, 0, 1, 0, width);
    var sy = map(this.y / this.z, 0, 1, 0, height);
    var r = map(this.z, 0, width, 10, 0);
    ellipse(sx, sy, r, r);
  };
}

Вот и все, пора лететь.

image

Если вам интересно посмотреть разные варианты и как параметры влияют на финальную картинку, рекомендую поиграть со значениями переменных speed, взять другие вариации цвета для фона и для звезд, увеличить или уменьшить их количество. В любом случае — приятного полета.
Идея и часть исходного кода взята из ролика Coding Challenge #1: Starfield in Processing

UPD:

В комментариях TheShock предложил растягивать их от центра, чтобы звезды выглядели более звездами из СтарТрека и менее снегом. И дал ссылку на свое решение.

Я решил попробовать докрутить эту идеюу себя, но через свой код. Отказался от ellipse() в пользу line(), которая тоже берет 4 параметра — два к начальной точке и два к конечной точке линии. Первые две у нас уже были (sx, sy), теперь нужно было придумать, как должна меняться конечная, чтобы это выглядело естественно.

Для этого внедряем новую переменную:


this.nz = width

В блоке show() я просто добавил вторую пару для линии с той же логикой расчета:

var sx = map(this.x / this.z, 0, 1, 0, width);
var sy = map(this.y / this.z, 0, 1, 0, height);
var nx = map(this.x / this.nz, 0, 1, 0, width);
var ny = map(this.y / this.nz, 0, 1, 0, height);

Плюс я заметил, что в примере выше цвет звезд был не чисто белый. Так что при создании линии я добавил ей цвет «powder blue»

stroke(176,224,230);
line(sx, sy, nx, ny);

Однако в итоге получилось вот так:

image

Дело в том, что вторая координата до пересоздания оставалась в начальной точке. Чтобы это поправить я в конце цикла добавил:

 this.nz = this.z;

Все, в зависимости от скорости размытие будет больше или меньше, при 25 это выглядит вот так:

image