Edit in JSFiddle

function getAsync(url) {
  return new Promise(function(resolve, reject) {
    var img = new Image();
    img.onload = resolve;
    img.onerror = reject;
    img.setAttribute("src", url);
  })
}

ImageZoomPreview = Ractive.extend({
  el: 'body',
  append: true,
  template: '#izp-tmpl',
  oninit: function() {
    var self = this;
  },
  onrender: function() {
    var self = this;
    ImageZoomPreview.instance = self;
    self.preview = self.find(".imageZoomPreview");
  }
})

ImageZoom = Ractive.extend({
  zoom: 4,
  maxZoom: 8,
  minZoom: 1,
  append: true,
  template: '#iz-tmpl',
  _mouseMoveTimerInterval: 50,
  onteardown: function() {
    var self = this;
    clearInterval(self.onMouseMoveTimerTimeout)
  },
  oninit: function() {
    var self = this;
    if (!ImageZoomPreview.instance) {
      new ImageZoomPreview({
        data: self.get()
      })
    }


    self.set('preview_width', self.get('preview_width') || 360)
    self.set('preview_height', self.get('preview_height') || 360)
    self.set('onMouseOver', false);
    self.set('onZoomActivated', false);

    self.on({
      onThumbMouseClick: function(e) {
        self.set('onMouseOver', false);
        self.set('onZoomActivated', false);
        ImageZoomPreview.instance.data = self.get();
        ImageZoomPreview.instance.update();
      },
      onThumbMouseOver: function(e) {
        self.set('onMouseOver', true);
        clearTimeout(self.onMouseOverTimeout);
        self.onMouseOverTimeout = setTimeout(function() {
          if (self.get('onMouseOver') == false) return;

          ImageZoomPreview.instance.data = self.get();
          ImageZoomPreview.instance.update();
          var previewPosition = self.getPreviewPosition();
          ImageZoomPreview.instance.preview.style.left = previewPosition.left + "px";
          ImageZoomPreview.instance.preview.style.top = previewPosition.top + "px";

          self.set('imageLoader', getAsync(self.get('source_url')));
          getAsync(self.get('source_url'))
            .then(function(evt) {
              self.set('onZoomActivated', true);
              try {
                var img;
                if (evt.path && evt.path[0]) {
                  img = evt.path[0];
                } else {
                  img = evt.target;
                }
                var bPreview = ImageZoomPreview.instance.preview.getBoundingClientRect();
                self.maxZoom = img.width / self.get('preview_width');
                if (self.zoom >= self.maxZoom) self.zoom = self.maxZoom;
              } catch (e) {
                console.log(e);
              } finally {}
            });
        }, 300)
        clearInterval(self.onMouseMoveTimerTimeout)
        self.onMouseMoveTimerTimeout = setInterval(self.onMouseMoveTimer.bind(self), self._mouseMoveTimerInterval)
        self.onMouseMoveTimer();
      },
      onThumbMouseMove: function(e) {
        this.mouseMoveEvent = e;
      },
      onThumbMouseOut: function(e) {
        self.set('onMouseOver', false)
        self.set('onZoomActivated', false);
        clearInterval(self.onMouseMoveTimerTimeout)
        ImageZoomPreview.instance.data = self.get();
        ImageZoomPreview.instance.update();
        self.set('previewVisibilityClass', 'imageZoom_off');
      }
    });


  },
  onMouseMoveTimer: function() {
    var self = this;
    var e = self.mouseMoveEvent;
    if (!e) return;

    var percentageX = (e.original.offsetX || e.original.layerX) / (e.original.target.width || e.original.target.naturalWidth);
    var percentageY = (e.original.offsetY || e.original.layerY) / (e.original.target.height || e.original.target.naturalHeight);
    var obj = {
      x: percentageX,
      y: percentageY
    }
    self.setGlassPosition(obj)
    self.setPreviewImagePosition(obj)
  },

  onrender: function() {
    var self = this;
    self.thumb = self.find('.imageZoom_container>img');
    self.glass = self.find('.imageZoom_glass');
  },


  setGlassPosition: function(coords) {
    var self = this;
    var style = ""
    if (self.get('onMouseOver')) {
      var bThumb = self.thumb.getBoundingClientRect();
      var gWid = Math.floor(bThumb.width / self.zoom);
      var gHei = Math.floor(bThumb.height / self.zoom);
      var gWidPer = gWid / bThumb.width;
      var gHeiPer = gHei / bThumb.height;
      var x = 0;
      var y = 0;

      if (coords.x < gWidPer / 2) x = 0;
      else if (coords.x > 1 - gWidPer / 2) x = (1 - gWidPer) * bThumb.width;
      else x = (coords.x - gWidPer / 2) * bThumb.width;

      if (coords.y < gHeiPer / 2) y = 0;
      else if (coords.y > 1 - gHeiPer / 2) y = (1 - gHeiPer) * bThumb.height;
      else y = (coords.y - gHeiPer / 2) * bThumb.height;

      style += "left:" + (x) + "px;";
      style += "top:" + (y) + "px;"
      style += "width:" + gWid + "px;";
      style += "height:" + gHei + "px";
    }
    self.set('glassPosition', style);
  },

  setPreviewImagePosition: function(coords) {
    var self = this;
    var style = "";
    if (self.get('onMouseOver')) {
      var bPreview = ImageZoomPreview.instance.preview.getBoundingClientRect();
      var imgWid = self.get('preview_width') * self.zoom;
      var imgHei = self.get('preview_height') * self.zoom;

      var x = -coords.x * (imgWid - self.get('preview_width'));
      var y = -coords.y * (imgHei - self.get('preview_height'));


      style += "left:" + parseInt(x) + "px;";
      style += "top:" + parseInt(y) + "px;"
      style += "width:" + parseInt(imgWid) + "px;";
      style += "height:" + parseInt(imgHei) + "px";
    }
    ImageZoomPreview.instance.set('previewImageStyle', style);
  },
  getPreviewPosition: function() {
    var self = this;
    var result = {
      left: 0,
      top: 0
    };

    var w = window,
      d = document,
      e = d.documentElement,
      g = d.getElementsByTagName('body')[0],
      x = w.innerWidth || e.clientWidth || g.clientWidth,
      y = w.innerHeight || e.clientHeight || g.clientHeight;


    if (self.get('onMouseOver')) {
      var bThumb = self.thumb.getBoundingClientRect();
      var bPreview = {
        width: self.get('preview_width'),
        height: self.get('preview_height')
      }
      var margin = 100;
      result.left = bThumb.width + margin;
      if (bThumb.left > bPreview.width + margin) {
        result.left = parseInt(bThumb.left + (-bPreview.width - margin));
        if (bThumb.top > bPreview.height / 2) {
          result.top = parseInt(bThumb.top + bThumb.height / 2 - bPreview.height / 2);
        } else {
          result.top = bThumb.top;
        }
      } else if (x - bThumb.left > bPreview.width + margin) {
        result.left = parseInt(bThumb.left + bThumb.width + margin);
        if (bThumb.top > bPreview.height / 2) {
          result.top = parseInt(bThumb.top + bThumb.height / 2 - bPreview.height / 2);
        } else {
          result.top = bThumb.top;
        }
      }
    }
    return result
  }
});

Ractive.components.ImageZoom = ImageZoom;
Ractive.components.ImageZoomPreview = ImageZoomPreview;

var iz = new Ractive({
  el: "body",
  template: '#main-tmpl',
  append: true
})
<script id='main-tmpl' type='text/html'>
  <div class='main'>
    <ImageZoom thumb_url="https://dl.dropboxusercontent.com/u/97792319/527006main_farside.1600_thumb.jpg" source_url="https://dl.dropboxusercontent.com/u/97792319/527006main_farside.1600.jpg" preview_width='360' preview_height='360' />
  </div>
</script>

<script id="izp-tmpl" type=text/html>
  <div class="imageZoomPreview {{#onMouseOver}}imageZoomPreview_Border{{/onMouseOver}}" style="width:{{preview_width}}px; height:{{preview_height}}px; ">
    {{#onMouseOver}}
    <img src="{{source_url}}" style="{{previewImageStyle}}"> {{/onMouseOver}}
  </div>
</script>

<script id='iz-tmpl' type='text/html'>
  <div class="imageZoom_container">
    <img src="{{thumb_url}}" on-mousemove="onThumbMouseMove" on-mouseover="onThumbMouseOver" on-mouseout="onThumbMouseOut" on-click="onThumbMouseClick" class="imageZoom_thumb" width="{{thumb_width}}" height="{{thumb_height}}"> {{#imageLoader}} {{#pending}}
    <div style="position:absolute; top:0px; left:0px;">
      {{#if preloaderHTML}} {{{preloaderHTML}}} {{else}} loading {{/if}}
    </div>
    {{/pending}} {{#error}}
    <div style="position:absolute; top:0px; left:0px; width:2px; height:120px; background-color: #FF0000">
    </div>
    {{/error}} {{/imageLoader}} {{#onZoomActivated}}
    <div class="imageZoom_glass" style="{{glassPosition}}"></div>
    {{/onZoomActivated}}
  </div>
</script>
.imageZoomPreview {
  pointer-events: none;
  position: fixed;
  top: 0px;
  left: 0px;
  width: 360;
  height: 360;
  overflow: hidden;
  z-index: 99999;
}

.imageZoomPreview img {
  position: absolute;
}

.imageZoom_container {
  position: relative;
  display: block;
}

.imageZoom_container .imageZoom_glass {
  box-sizing: border-box;
  pointer-events: none;
  width: 0px;
  height: 0px;
  position: absolute;
  border: solid 1px #ccc;
  z-index: 1000;
  top: 0;
  left: 0;
}

.imageZoomPreview_Border {
  box-shadow: 0 0 1px rgba(34, 25, 25, 0.4);
}

.imageZoom_fullOpaque {
  opacity: 1.0;
}

.imageZoom_opaque {
  opacity: 0.5;
}

.imageZoom_on {
  display: block;
}

.imageZoom_off {
  display: none;
}