DOM要素をカーソルの位置で拡大縮小、移動可能にする【JavaScript】

##したいこと

- 要素をマウスホイールで拡大縮小可能に
- 拡大縮小はカーソルを中心に
- 要素をドラッグで移動可能に
- 親要素をダブルクリックでデフォの状態に

##コード
###HTML
````HTML
<div id="parent">
  <div id="child">  
    <img src="http://placehold.jp/300x200.png">
  </div>
</div>
````
###CSS
````CSS
#parent, #child {
  width: 300px;
  height: 200px;
}
#parent {
  position: relative;
  overflow: hidden;
}
#child {
  cursor: move;
}
````
###JavaScript
````JavaScript
window.onload = () => {
  let child = document.getElementById("child");
  zoomable(child);
  moveable(child);
  resettable(child);
}
````

````js
let zoomable = (element, options) => {
    let {minScale, maxScale, magnification} = {
        minScale: 0.5, 
        maxScale: 2, 
        magnification: -0.0025,
        ...options
    }
    let scale = 1;
    element.style.transform = "scale(1)";

    let zoomElement = (event) => {
        event.preventDefault(); // Do not scroll the page

        const w1 = element.clientWidth;
        const h1 = element.clientHeight;

        // currently scale
        let s1 = Number(/(?<=\().*?(?=\))/.exec(element.style.transform));
        // gap with the parent element
        let gX = Number(/.*?(?=px)/.exec(element.style.left));
        let gY = Number(/.*?(?=px)/.exec(element.style.top));
        // center coordinate of the image
        let cX = w1/2 + gX;
        let cY = h1/2 + gY;
        // coordinate of the pointer
        let pX = event.pageX - element.parentElement.offsetLeft;
        let pY = event.pageY - element.parentElement.offsetTop;
        // distance between the center and the pointer
        let dX = pX - cX;
        let dY = pY - cY;

        // scale the image and move to correct coordinate
        scale += event.deltaY * magnification;
        scale = Math.min(Math.max(minScale, scale), maxScale);
        element.style.left = gX + dX * (1 - scale/s1) + "px";
        element.style.top = gY + dY * (1 - scale/s1) + "px";
        element.style.transform = `scale(${scale})`;
    };

    element.addEventListener("wheel", zoomElement);
}
````

````js
let moveable = (element) => {
  element.style.position = "absolute";
  let x1;
  let y1;
  let px1;
  let py1;
  
  let moveElement = (event) => {
    // Do not drag the image
    event.preventDefault();
    element.ondragstart = false; 
    
    element.style.left = x1 + event.pageX - px1 + "px";
    element.style.top = y1 + event.pageY - py1 + "px";
  };
  
  element.addEventListener("mousedown", (event) => {
    x1 = Number(/.*?(?=px)/.exec(element.style.left));
    y1 = Number(/.*?(?=px)/.exec(element.style.top));
    px1 = event.pageX;
    py1 = event.pageY;
    
    element.addEventListener("mousemove", moveElement);
  });
  element.addEventListener("mouseup", () => {
    element.removeEventListener("mousemove", moveElement);
  });
}
````

````js
let resettable = (element) => {
  let resetElement = (event) => {
    event.preventDefault();
    
    element.style.transform = "scale(1)";
    element.style.left = 0;
    element.style.top = 0;
  };
  
  element.parentElement.addEventListener("dblclick", resetElement);
}
````

### ポイント
- addEventListener
 - 第2引数に無名関数を登録するとremoveEventListenerできないため、関数オブジェクトを変数に代入して使う
 - 第2引数の関数に引数を設定するとremoveEventListenerできないため、関数に渡したい値はaddEventListenerの1つ上のブロックで定義してまるごと関数にしてしまう
- Dom操作
 - element.style.propertyはDOM要素のstyle属性なので、CSS(style要素)を参照できない
 - そのため依存関係にあるプロパティはstyle属性にHTMLかJSで直接記述する必要がある
- その他
 - ユーザー操作を可能にするにはデフォの操作を消す必要がある
 - element.style.propertyから数値を取得するときに単位消して型変換にする

### カーソルを中心に拡大縮小
くっそわかりずらいしどうせ忘れるのでメモ。

- transform: scale()は画像の上下左右中央を中心に拡大縮小される
- left, top(position: absolute)は要素が等倍のときの親要素との差
- 肝のとこは画像の**中央の座標とカーソルの座標の拡大前後の差**と**最初の親要素との差**を足してtop, leftに設定してる
- どういうことかっていうとイメージとしてはまずカーソルの位置に画像の中央を移動させて拡大。その後、拡大前にカーソルがあった部分をもう一度カーソルの位置に移動させているといった感じ。
- 一次元ならカーソルを原点から1の位置として画像の中央は2、これを-1だけ(画像の中央をカーソルの位置に)動かして(例えば)2倍に拡大すると、当然もともとカーソルがあった画像内の点は0(=1-1)から-1=(1-(1-0)*2)。-1動かして2倍かけてるから+2だけ動かせば画像の点はカーソルの位置でそこだけ同じになる。