На главную

Легковес

Воркшоп — делаем эффект падающего снега на HTML, CSS, JS ES6-классах с использованием структурного паттерна легковес
Дисклеймер. Данная реализация сделана в учебных целях: попрактиковать паттерны программирования. Это не самый простой с точки зрения архитектуры кода и не самый оптимальный с точки зрения производительности вариант.

Для тех, кто не хочет читать много букв — вот ссылка на финальную демку.

Сделаем эффект падающего снега и заодно потренируемся в использовани паттернов программирования. В нашем случае используем Легковес (Flyweight). Он полезен, когда приходится работать с множеством одинаковых объектов, а у нас как раз такой случай — снежинки. Использование легковеса позволяет вынести часть логики в отдельный переиспользуемый класс.

ТЗ для для снегопада:

  • Каждая снежинка это отдельный div-элемент
  • Есть 3 типа снежинок: маленькие, средние и большие
  • Можем легко настраивать количество снежинок каждого типа, "участвующих" в снегопаде
  • Мы можем управлять продолжительностью снегопада

Поехали, — в первую очередь напишем разметку и стили!

Напишем HTML-разметку

Добавим нашу сцену на которой будет падать снежок. И кнопку для инициализации.

<button class="run-btn">Let it snow!❄❄❄</button>
<div class="scene"></div>

Разметка готова, отличная работа! 🙃

Добавим CSS-стили

Тут будет немного больше работы.

/* стили для нашей сцены */
.scene {
position: relative;
width: 100%;
padding-bottom: 50%;
overflow: hidden;
background-image: linear-gradient(to top, #005bea 0%, midnightblue 100%);
/* эффект небольшого размытия */
filter: blur(1px);
}

/* базовый класс снежинки, которая абсолютно позиционируется относительно сцены */
.snowflake {
position: absolute;
border-radius: 50%;
background-color: #fff;
transform: translateY(-100%);
/*
самая интересная часть: анимация
указываем имя анимации — fall
её мы опишем чуть ниже
*/

animation-name: fall;
/* снежинки будут падать со стабильной скоростью, без ускорений */
animation-timing-function: linear;
}

/*
анимация падения
из самого верха родительского контейнера — 0, до конца — 100%
transform — translateY для того чтобы в начале падения и в его конце
мы точно не видели снежинку на сцене
translateX — снежинку будет немного сносить в сторону, как при небольшом ветре
*/

@keyframes fall {
from {
top: 0;
transform: translateY(-150%) translateX(0);
}

to {
top: 100%;
transform: translateY(150%) translateX(10px);
}
}

/* размеры для разных типов снежинок */
.snowflake.big {
width: 15px;
height: 15px;
}

.snowflake.middle {
width: 12px;
height: 12px;
}

.snowflake.small {
width: 7px;
height: 7px;
}

А теперь, самое интересное — JS

Напишем логику нашего приложения в ООП-стиле на ES6 классах. Конечно, в данном случае такой подход избыточен и мы могли бы обойтись парой функций, но сделаем задачу интересней и напишем ПРИЛОЖЕНИЕ, которое будет легко масштабировать.

Для каждой снежинки в приложении будет создан свой экземпляр класса SnowFlake, в котором будет храниться информация:

  • ссылка на контейнер где снежинка рисутеся, в данном случае это наш <div class="scene"></div>
  • сколько раз эта снежинка уже упала
  • сколько раз всего она может упасть
  • тип снежинки — ссылка на экземпляр класса SnowFlakeType (о нём ниже), который знает о том, как правильно нарисовать снежинку данного типа

Так как снежинок будет много, а типов всего 3, вынесем логику отрисовки в отдельный классы - SnowFlakeType. Получаем такую картину:

SnowFlake ❄ SnowFlakeType 🖌
Содержит в себе данные о конкретной снежинке. Экземпляров этого класса столько, сколько снежинок в нашем приложении Знает как нарисовать снежинку конкретного типа в приложении. Экземпляров этого класса столько, сколько различных типов снежинок. У нас пока 3, но в будущем можно расширить

Вы можете сказать:

Подожди, снежинки же отличаются только css-классом, зачем так заморачиваться?

И будите правы. В данных условиях классовый подход избыточен. Но изначально мы решили, что заложим более сложную архитектуру под масштабирование. Например, в будущем у каждой снежинки в зависимости от типа будут свои свойства, некоторые вообще будут рисоваться через SVG и т.д. А ещё, в зависимости от состояния приложения нужно будет менять текущее представление, например, подменять "скин" определенного типа снежинок.

Реализуем класс SnowFlakeType

class SnowFlakeType {  
constructor(className) {
this.className = className;

// создаём элемент div и храним его в поле класса
const element = document.createElement('div');
element.classList.add('snowflake');
element.classList.add(this.className);
this.element = element;
}

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

// основная логика — метод который "рисует" нашу снежинку с нужными параметрами
// container — ссылка на HTML-элемент в который добавим div со снежинкой
// callback — функция-коллбэк, которую вызовем когда снежинка упала
draw(container, callback) {
const fallTime = SnowFlakeType.getRandomInt(5, 7);
const x = SnowFlakeType.getRandomInt(1, 100);
const snowflake = this.element.cloneNode();
snowflake.style.left = `${x}%`;
snowflake.style.animationDuration = `${fallTime}s`;

// добавили снежинку на сцену
container.appendChild(snowflake);

window.setTimeout(() => {
// когда снежинка упала, удалили её html-элемент
container.removeChild(snowflake);
callback();
}, fallTime * 1000);
}
}

Реализуем класс SnowFlake

class SnowFlake {
constructor(type, container, limit) {
this.fallCounter = 0;
this.type = type;
this.container = container;
this.limit = limit;
}

init() {
this.type.draw(this.container, () => {
this.fallCounter += 1;
// если установлен лимит на количество "падений" снежинки
// и мы его превысили, на следующий круг она не пойдёт
if (!this.limit || this.fallCounter < this.limit) {
this.init();
}
});
}
}

Фабрика типов снежинок

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

К ней будет обращаться приложение и говорить:

Дай мне экземпляр класса для отрисовки снежинки с типом big

Фабрика в свою очередь сделает следующее

  1. Проверит — есть ли у неё "на складе" уже готовый экземпляр?
  2. Если нету, то создаст. Если уже есть — сразу на следующий шаг.
  3. Отдаёт ссылку на нужный экземпляр.
class SnowFlakeTypeFactory {
constructor() {
this.snowFlakesTypes = new Map();
}

getType(type) {
let snowFlakeType = this.snowFlakesTypes.get(type);
if (!snowFlakeType) {
snowFlakeType = new SnowFlakeType(type);
this.snowFlakesTypes.set(type, snowFlakeType);
}
return snowFlakeType;
}
}

Запускаем снегопад! 🌨

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

class SnowFall {
constructor(scene, initialArr, limit) {
// ссылка на элемент сцены
this.scene = scene;
// массив с типами снежинок
// вида ['big', 'small', 'big', ...]
this.initialArr = initialArr;
// лимит на количество падений каждой снежинки
// если ничего не передать — будет бесконечный снегопад
this.limit = limit;
}

run() {
// инициализируем фабрику
const factory = new SnowFlakeTypeFactory();

for (const item of this.initialArr) {
// получаем ссылку из фабрики
const type = factory.getType(item);

// передаём её в класс снежинки
const snowFlake = new SnowFlake(type, this.scene, this.limit);

// рандомная задержка для запуска каждой снежинки
const randomStartTime = Math.random() * (10000 - 100) + 100;

window.setTimeout(() => {
snowFlake.init();
}, randomStartTime)
}
}
}

Let It Snow!

const sceneEl = document.querySelector('.scene');
// 60 маленьких
let elements = new Array(60).fill('small');
// 20 средних
elements = elements.concat(new Array(20).fill('middle'));
// 10 больших
elements = elements.concat(new Array(10).fill('big'));
// ограничиваем количество кругов для каждой снежинки — 50
const snowFall = new SnowFall(sceneEl, elements, 50);

// Жмём на кнопку и поехали
const runBtn = document.querySelector('.run-btn');
runBtn.addEventListener('click', () => {
runBtn.style.display = 'none';
snowFall.run();
})

Комментарии