Share This
Связаться со мной
Крути в низ
Categories
//Масштабирование и панорамирование в 69 строчках JavaScript

Масштабирование и панорамирование в 69 строчках JavaScript

Чтобы не использовать тяжеловесные библиотеки, можно написать на чистом JavaScript простое и расширяемое решение для манипуляций с элементами веб-страницы. Обсудить

masshtabirovanie i panoramirovanie v 69 strochkah javascript 2c5b2c6 - Масштабирование и панорамирование в 69 строчках JavaScript

Возможность масштабировать элементы страницы и детально рассматривать их – это очень крутой пользовательский опыт. Существует множество готовых библиотек с подобной функциональностью, но сегодня мы напишем собственный велосипед на чистом JavaScript! Зачем?

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

В результате мы получим очень маленькую (всего 69 строчек кода!), простую и удобную библиотечку для масштабирования и панорамирования.

Разметка и стили

Создадим HTML-страницу и разместим на ней элемент-контейнер (#container). Внутрь поместим рабочую область (.area), которую мы и будем непосредственно масштабировать и панорамировать.

index.html

         <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>     

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

Добавим также немного стилей для оформления страницы:

style.css

         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:

renderer.js

         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 };     

Она принимает базовые параметры:

  1. minScale – минимальный масштаб;
  2. maxScale – максимальный масштаб;
  3. element – DOM-элемент, с которым будут производиться манипуляции;
  4. scaleSensitivity – коэффициент чувствительность масштабирования, по умолчанию 10.

В замыкании функции создается объект состояния – state, который хранит настройки и совершенные над элементом преобразования (поле transformation).

Из функции возвращается объект с набором методов. При этом возможности масштабирования и панорамирования разделены на отдельные функции-конструкторы – makeZoom и makePan, которые мы разберем чуть позже. Конструкторы получают общий объект состояния и возвращают отдельный набор методов для взаимодействия с ним.

Такой подход называется композицией и позволяет проще добавлять новую функциональность и легче тестировать приложение.

Трансформации

Все манипуляции с элементом будут производиться через изменение свойства transform. Для этого используем CSS-функцию matrix, которой нужно передать правильные параметры масштаба (scale) и сдвига (translateX и translateY):

renderer.js

         const getMatrix = ({ scale, translateX, translateY }) =>    `matrix(${scale}, 0, 0, ${scale}, ${translateX}, ${translateY})`;     

Вспомогательная функция getMatrix просто формирует шаблонную строку правильного формата, которую нужно установить в свойство style.transform элемента.

Панорамирование

При панорамировании должно изменяться положение элемента на странице, то есть производиться его сдвиг. Функция pan принимает текущее состояние элемента (state), а также новые координаты. Затем она обновляет состояние, прибавляя новый сдвиг к текущему положению и обновляет свойство style элемента.

renderer.js

         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 }); };     

Теперь реализуем два метода:

  1. panBy – простой сдвиг на указанные координаты;
  2. panTo – сдвиг с одновременным масштабированием.

renderer.js

         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 });     }, });      

При сдвиге с масштабированием координаты элемента нужно скорректировать.

Масштабирование

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

renderer.js

         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:

renderer.js

         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:

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:

renderer.testCases.js

         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, };     

renderer.spec.js

         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 строк кода. Его можно сократить больше, но не хочется терять читабельность.

Полную и минифицированную версии ищите в репозитории проекта.

  • 23 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Связаться со мной
Close