πŸ” ΠœΠ°ΡΡˆΡ‚Π°Π±ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΈ ΠΏΠ°Π½ΠΎΡ€Π°ΠΌΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π² 69 строчках JavaScript

Π§Ρ‚ΠΎΠ±Ρ‹ Π½Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ тяТСловСсныС Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ, ΠΌΠΎΠΆΠ½ΠΎ Π½Π°ΠΏΠΈΡΠ°Ρ‚ΡŒ Π½Π° чистом 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 строк ΠΊΠΎΠ΄Π°. Π•Π³ΠΎ ΠΌΠΎΠΆΠ½ΠΎ ΡΠΎΠΊΡ€Π°Ρ‚ΠΈΡ‚ΡŒ большС, Π½ΠΎ Π½Π΅ хочСтся Ρ‚Π΅Ρ€ΡΡ‚ΡŒ Ρ‡ΠΈΡ‚Π°Π±Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ.

ΠŸΠΎΠ»Π½ΡƒΡŽ ΠΈ ΠΌΠΈΠ½ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Π½Π½ΡƒΡŽ вСрсии ΠΈΡ‰ΠΈΡ‚Π΅ Π² Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΈ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°.

Π˜ΡΡ‚ΠΎΡ‡Π½ΠΈΠΊΠΈ

Π›Π£Π§Π¨Π˜Π• БВАВЬИ ПО Π’Π•ΠœΠ•