Работа мечты в один клик 💼

💭Мечтаешь работать в Сбере, но не хочешь проходить десять кругов HR-собеседований? Теперь это проще, чем когда-либо!
💡AI-интервью за 15 минут – и ты уже на шаг ближе к своей новой работе.
Как получить оффер? 📌 Зарегистрируйся 📌 Пройди AI-интервью 📌 Получи обратную связь сразу же!
HR больше не тянут время – рекрутеры свяжутся с тобой в течение двух дней! 🚀
Реклама. ПАО СБЕРБАНК, ИНН 7707083893. Erid 2VtzquscAwp
Возможность масштабировать элементы страницы и детально рассматривать их – это очень крутой пользовательский опыт. Существует множество готовых библиотек с подобной функциональностью, но сегодня мы напишем собственный велосипед на чистом JavaScript! Зачем?
- Сторонние решения часто предлагают избыточную функциональность, которая вам не нужна, но бандл приложения увеличивает.
- К тому же это замечательный челлендж, который расшевелит ваш мозг и прокачает навыки программирования.
В результате мы получим очень маленькую (всего 69 строчек кода!), простую и удобную библиотечку для масштабирования и панорамирования.
Разметка и стили
Создадим HTML-страницу и разместим на ней элемент-контейнер (#container
). Внутрь поместим рабочую область (.area
), которую мы и будем непосредственно масштабировать и панорамировать.
<div id="container">
<div class="area">
<div class="rectangle"></div>
<div class="circle"></div>
<div class="text-area">
<h1>Example line of text</p>
</div>
</div>
</div>
Внутри рабочей области находятся несколько элементов, которые не несут никакой смысловой нагрузки, а просто предназначены для демонстрации работы кода.
Добавим также немного стилей для оформления страницы:
body {
overflow: hidden;
}
#container {
height: 100%;
width: 100%;
position: absolute;
}
.area {
border: 1px dashed black;
height: 80%;
width: 80%;
position: absolute;
}
.circle {
height: 200px;
width: 200px;
background-color: navajowhite;
border-radius: 50%;
display: inline-block;
position: relative;
}
.rectangle {
background-color: navajowhite;
height: 150px;
width: 250px;
position: relative;
}
.text-area {
float: right;
position: relative;
}
Для body
устанавливаем overflow: hidden
. Это нужно, чтобы избежать переполнения страницы и появления прокрутки при чрезмерном увеличении элемента.
Также добавим рамку для визуального обозначения рабочей области (.area
) и немного облагородим демо-контент (классы .circle
, .rectangle
и .text-area
).
Скрипт библиотеки
Код самой библиотеки будет располагаться в файле renderer.js
. Экспортируем из модуля главную функцию renderer
:
const renderer = ({ minScale, maxScale, element, scaleSensitivity = 10 }) => {
const state = {
element,
minScale,
maxScale,
scaleSensitivity,
transformation: {
originX: 0,
originY: 0,
translateX: 0,
translateY: 0,
scale: 1
},
};
return Object.assign({}, makeZoom(state), makePan(state));
};
module.exports = { renderer };
Она принимает базовые параметры:
minScale
– минимальный масштаб;maxScale
– максимальный масштаб;element
– DOM-элемент, с которым будут производиться манипуляции;scaleSensitivity
– коэффициент чувствительность масштабирования, по умолчанию 10.
В замыкании функции создается объект состояния – state
, который хранит настройки и совершенные над элементом преобразования (поле transformation
).
Из функции возвращается объект с набором методов. При этом возможности масштабирования и панорамирования разделены на отдельные функции-конструкторы – makeZoom
и makePan
, которые мы разберем чуть позже. Конструкторы получают общий объект состояния и возвращают отдельный набор методов для взаимодействия с ним.
Такой подход называется композицией и позволяет проще добавлять новую функциональность и легче тестировать приложение.
Трансформации
Все манипуляции с элементом будут производиться через изменение свойства transform
. Для этого используем CSS-функцию matrix
, которой нужно передать правильные параметры масштаба (scale
) и сдвига (translateX
и translateY
):
const getMatrix = ({ scale, translateX, translateY }) =>
`matrix(${scale}, 0, 0, ${scale}, ${translateX}, ${translateY})`;
Вспомогательная функция getMatrix
просто формирует шаблонную строку правильного формата, которую нужно установить в свойство style.transform
элемента.
Панорамирование
При панорамировании должно изменяться положение элемента на странице, то есть производиться его сдвиг. Функция pan
принимает текущее состояние элемента (state
), а также новые координаты. Затем она обновляет состояние, прибавляя новый сдвиг к текущему положению и обновляет свойство style
элемента.
const pan = ({ state, originX, originY }) => {
state.transformation.translateX += originX;
state.transformation.translateY += originY;
state.element.style.transform =
getMatrix({ scale: state.transformation.scale, translateX: state.transformation.translateX, translateY: state.transformation.translateY });
};
Теперь реализуем два метода:
panBy
– простой сдвиг на указанные координаты;panTo
– сдвиг с одновременным масштабированием.
const makePan = (state) => ({
panBy: ({ originX, originY }) => pan({ state, originX, originY }),
panTo: ({ originX, originY, scale }) => {
state.transformation.scale = scale;
pan({ state, originX: originX - state.transformation.translateX, originY: originY - state.transformation.translateY });
},
});
При сдвиге с масштабированием координаты элемента нужно скорректировать.
Масштабирование
Для изменения размера элемента нам потребуется несколько вспомогательных функций для расчетов:
const hasPositionChanged = ({ pos, prevPos }) =>
pos !== prevPos;
const valueInRange = ({ minScale, maxScale, scale }) =>
scale <= maxScale && scale >= minScale;
const getTranslate = ({ minScale, maxScale, scale }) => ({ pos, prevPos, translate }) =>
valueInRange({ minScale, maxScale, scale }) && hasPositionChanged({ pos, prevPos })
? translate + (pos - prevPos * scale) * (1 - 1 / scale)
: translate;
const getScale = ({ scale, minScale, maxScale, scaleSensitivity, deltaScale }) => {
let newScale = scale + (deltaScale / (scaleSensitivity / scale));
newScale = Math.max(minScale, Math.min(newScale, maxScale));
return [scale, newScale];
};
Метод getScale
рассчитывает новый масштаб на основе предыдущего значения, минимального и максимального ограничений (minScale
, maxScale
) и коэффициентов (scaleSensitivity
, deltaScale
).
Метод getTranslate
рассчитывает новый сдвиг на основе масштаба и текущей и предыдущей позиции.
А вот и реализация функции makeZoom
:
const makeZoom = (state) => ({
zoom: ({ x, y, deltaScale }) => {
const { left, top } = state.element.getBoundingClientRect();
const { minScale, maxScale, scaleSensitivity } = state;
const [ scale, newScale ] = getScale({ scale: state.transformation.scale, deltaScale, minScale, maxScale, scaleSensitivity });
const originX = x - left;
const originY = y - top;
const newOriginX = originX / scale;
const newOriginY = originY / scale;
const translate = getTranslate({ scale, minScale, maxScale });
const translateX = translate({ pos: originX, prevPos: state.transformation.originX, translate: state.transformation.translateX });
const translateY = translate({ pos: originY, prevPos: state.transformation.originY, translate: state.transformation.translateY });
state.element.style.transformOrigin = `${newOriginX}px ${newOriginY}px`;
state.element.style.transform = getMatrix({ scale: newScale, translateX, translateY });
state.transformation = { originX: newOriginX, originY: newOriginY, translateX, translateY, scale: newScale };
}
});
Она возвращает только один метод zoom
, предназначенный для масштабирования элемента. Он получает координаты курсора, а также параметр deltaScale
– коэффициент, который определяет направление масштабирования (1
для увеличения, -1
для уменьшения).
Функция вычисляет новые параметры трансформации и обновляет свойство style
элемента.
При масштабировании кроме style.transform
нужно изменять также свойство style.transformOrigin
, чтобы скорректировать позицию элемента. В качестве эксперимента вы можете закомментировать 14 строчку и посмотреть, что будет.
Главный файл
Кроме того мы сделаем главный файл приложения index.js
:
(() => {
const { renderer } = require("./src/renderer");
const container = document.getElementById("container");
const instance = renderer({ minScale: .1, maxScale: 30, element: container.children[0], scaleSensitivity: 50 });
container.addEventListener("wheel", (event) => {
if (!event.ctrlKey) {
return;
}
event.preventDefault();
instance.zoom({
deltaScale: Math.sign(event.deltaY) > 0 ? 1 : -1,
x: event.pageX,
y: event.pageY
});
});
container.addEventListener("dblclick", () => {
instance.panTo({
originX: 0,
originY: 0,
scale: 1,
});
});
container.addEventListener("mousemove", (event) => {
if (!event.shiftKey) {
return;
}
event.preventDefault();
instance.panBy({
originX: event.movementX,
originY: event.movementY
});
})
})();
Клиентский код создает экземпляр renderer
и передает ему базовую конфигурацию:
- элемент, размер которого будет изменяться;
- минимальный и максимальный масштаб;
- коэффициент чувствительности масштабирования.
Затем устанавливаются слушатели событий мыши и в нужный момент вызываются нужные методы:
- для масштабирования используйте колесико мыши или сенсорную панель, зажав клавишу CTRL.
- для перемещения – перемещайте мышь или используйте тачпад, зажав клавишу SHIFT.
- Двойной щелчок мыши восстановит исходное состояние элемента.
Демо-пример:
Тестирование
Проверим реализованные функции с помощью библиотеки Mocha:
const panByTestCases = [
{
description: 'should pan by passed originX and originY (x: 100, y: 100)',
minScale: .1,
maxScale: 20,
origins: [
{
originX: 100,
originY: 100,
}
],
result: 'matrix(1, 0, 0, 1, 100, 100)'
},
{
description: 'should pan by passed originX and originY (x: 50, y: 50)',
minScale: .1,
maxScale: 20,
origins: [
{
originX: 100,
originY: 100,
},
{
originX: -50,
originY: -50,
}
],
result: 'matrix(1, 0, 0, 1, 50, 50)'
},
{
description: 'should pan by passed originX and originY (x: -50, y: 50)',
minScale: .1,
maxScale: 20,
origins: [
{
originX: -100,
originY: 100,
},
{
originX: 50,
originY: -50,
}
],
result: 'matrix(1, 0, 0, 1, -50, 50)'
}
];
module.exports = {
panByTestCases,
};
const assert = require('assert');
const { renderer } = require('../src/renderer');
const { panByTestCases } = require('./renderer.testCases');
describe('renderer', () => {
let _element;
beforeEach(() => {
_element = {
getBoundingClientRect: () => ({ left: 0, top: 0 }),
style: {
transform: "",
transformOrigin: "",
}
}
});
describe('#canPan()', () => {
describe('#panBy()', () => {
panByTestCases.forEach(({ description, minScale, maxScale, origins, result }) =>
it(description, () => {
const instance = renderer({ minScale, maxScale, element: _element })
origins.forEach(({ originX, originY }) => instance.panBy({ originX, originY }))
assert.equal(_element.style.transform, result);
}),
);
});
});
Перед каждым тестом (beforeEach
) создается объект _element
с дефолтными значениями.
Кейсы тестирования для удобства вынесены в отдельный файл renderer.testCases.js
.
В итоге у нас получился очень простой и удобный инструмент для масштабирования и панорамирования на JavaScript, состоящий всего из 69 строк кода. Его можно сократить больше, но не хочется терять читабельность.
Полную и минифицированную версии ищите в репозитории проекта.
Комментарии