userChrome.js を使って SVG ZoomAndPan

FirefoxにはSVGのズームやパンが実装されていない。
拡張機能ではthe firefox zoom and pan extensionがあるが、この拡張機能は読み込んだデータのContent-Typeimage/svg+xmlだったら動くと言うもの。XHTML等のXMLアプリケーションとしてSVGが仕込まれていた時は機能しないのだ。
そこが不満だったので作ってみた*1。拡張にするにはちょっとアレな実装なのでuserChrome.js*2に登録。

機能

複数の埋め込まれたSVGに対応している。

Zoom

Alt + スクロールすることで拡大・縮小が可能。約1.5倍ずつ拡大・縮小される。

Pan

Ctrl + ドラッグでパンとなる。

元に戻す

Ctrl + Shift+クリック(マウスダウン)で元に戻る

やっていること

  • tabbrowser(id:content)からマウスダウンとスクロールイベントを取る*3
  • イベントのtargetからsvg要素を割り出す
    • 元の大きさと位置に戻すために初期値をsvg要素の属性として埋め込む
  • パンの場合は、viewBoxのx,y座標をずらす
  • ズームの場合は、viewBoxのwidth,heighを変える*4

Source

/**
 * @author teramako teramako@gmail.com
 * @version 1.0
 * @projectDescription SVG Zoom And Pan
 * @license MPL 1.1/GPL 2.0/LGPL 2.1
 */

(function() {
  var startPoint = {}; 
  var currentSVG = {}; 
  var content = document.getElementById('content');
  content.addEventListener('mousedown',svgOnMouseDown,false);
  content.addEventListener('DOMMouseScroll',svgZoom,false);

  // getSVGElement {{{
  /** 
   * @param {Element} target event.target
   * @return {Object}
   */
  function getSVGElement(target){
    var svgElement;
    if ( target instanceof SVGSVGElement ){
      svgElement = target;
    } else {
      svgElement = target.ownerSVGElement;
    }   
    currentSVG = { 
      element: svgElement,
      x: svgElement.viewBox.baseVal.x,
      y: svgElement.viewBox.baseVal.y,
      zoomX: svgElement.width.baseVal.value / svgElement.viewBox.baseVal.width,
      zoomY: svgElement.height.baseVal.value / svgElement.viewBox.baseVal.height
    };  
    if ( ! svgElement.hasAttribute('__orgX') ){
      svgElement.setAttribute('__orgX', currentSVG.x);
      svgElement.setAttribute('__orgY', currentSVG.y);
      svgElement.setAttribute('__orgWidth', svgElement.viewBox.baseVal.width);
      svgElement.setAttribute('__orgHeight', svgElement.viewBox.baseVal.height);
    }   
    return currentSVG;
  }
  // }}}
  // svgOnMouseDown {{{
  /** 
   * Ctrl + MouseDown -> start pan
   * Ctrl + Shift + MouseDown -> reset
   * @param {Event} evt
   */
  function svgOnMouseDown(evt){
    if ( evt.ctrlKey && evt.target.namespaceURI == 'http://www.w3.org/2000/svg' ){
      var svg = getSVGElement(evt.target);
      if ( ! evt.shiftKey ){ // pan
        svg.element.style.cursor = '-moz-grabbing';
        content.addEventListener('mousemove',svgOnMouseMove,true);
        content.addEventListener('mouseup',svgOnMouseUp,true);
        startPoint = { x: evt.clientX, y: evt.clientY };
      } else { // reset
        svg.element.viewBox.baseVal.width = svg.element.getAttribute('__orgWidth');
        svg.element.viewBox.baseVal.height = svg.element.getAttribute('__orgHeight');
        svg.element.viewBox.baseVal.x = svg.element.getAttribute('__orgX');
        svg.element.viewBox.baseVal.y = svg.element.getAttribute('__orgY');
      }
    }
  }
  // }}}
  // svgOnMouseMove {{{
  /**
   * start Pan
   * @param {Event} evt
   */
  function svgOnMouseMove(evt){
    if ( startPoint && currentSVG && evt.ctrlKey ){
      currentSVG.element.viewBox.baseVal.x = (startPoint.x - evt.clientX) / currentSVG.zoomX + currentSVG.x;
      currentSVG.element.viewBox.baseVal.y = (startPoint.y - evt.clientY) / currentSVG.zoomY + currentSVG.y;
    }
  }
  // }}}
  // svgOnMouseUp {{{
  /**
   * Reset varible and event handler
   */
  function svgOnMouseUp(){
    if ( startPoint || currentSVG ){
      currentSVG.element.style.cursor = 'auto';
      currentSVG = null;
      startPoint = null;
    }
    content.removeEventListener('mousemove',svgOnMouseMove,true);
    content.removeEventListener('mouseup',svgOnMouseUp,true);
  }
  // }}}
  // svgZoom {{{
  /**
   * Alt + Scroll up   -> Zoom In
   * Alt + Scroll down -> Zoom Out
   * @param {Event} evt
   */
  function svgZoom(evt){
    if ( evt.altKey && evt.target.namespaceURI == 'http://www.w3.org/2000/svg' ){
      var svg = getSVGElement(evt.target);
      if ( evt.detail < 0 ){ // Zoom In
        svg.element.viewBox.baseVal.width /= 1.5;
        svg.element.viewBox.baseVal.height /= 1.5;
      } else {
        svg.element.viewBox.baseVal.width *= 1.5;
        svg.element.viewBox.baseVal.height *= 1.5;
      }
      evt.stopPropagation();
    }
  }
  // }}}
}());

// vim:set ts=2 sw=2 sts=0 foldmethod=marker:

*1:ただし、出来る事は少なめ

*2:朝顔日記 - Firefox の拡張機能、userChrome.js の私の使い方も参照するとよい感じ

*3:SVGが在る無いにかかわらず、イベントを待ち受けてしまうのが気持ち悪い

*4:イベントのstopPropagationをしたのに画面スクロールも同時に動いてしまう...orz