Легковес
Воркшоп — делаем эффект падающего снега на 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
Фабрика в свою очередь сделает следующее
- Проверит — есть ли у неё "на складе" уже готовый экземпляр?
- Если нету, то создаст. Если уже есть — сразу на следующий шаг.
- Отдаёт ссылку на нужный экземпляр.
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();
})
Комментарии