Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Position Element on Top of Another Using Translate After It Has Been Scaled

I'm implementing an image cropping tool based on Croppie. My issue is that when I zoom out too far, the image leaves the viewport or is too far from the cropping area.

So I have a function, on line (code snippet below) 363 that is like so:

function _updateCenterPoint() { //borked
  console.log("fire _updateCenterPoint()");
  var self = this,
    transform = Transform.parse(self.childElements.img.style[CSS_TRANSFORM]);
  var imgRect = self.childElements.img.getBoundingClientRect(),
    cropRect = self.childElements.enclosedCrop.getBoundingClientRect();
  if (!isContainedByCrop(imgRect, cropRect, transform)) {
    console.log("adjust center");
    transform.x = //do a transform (I've tried many - see below)
    transform.y = //do a transform (I've tried many - see below);
  }
  var newCss = {};
  newCss['transform'] = transform.toString();
  css(self.childElements.img, newCss);

}

Positioning the element after it zooms out too far is handled by the logic within the if (!isContainedByCrop(imgRect, cropRect, transform)) conditional, where "adjust center" is logged.

I've tried a lot of ways to do this. I tried using the distance formula, but got confused because I'm comparing translated values not (x,y) points. Basically it should fit within the cropping tool once it shrinks below a certain amount.

However, I can't get it to behave consistently. When I shrink it down to 150 by 150 pixels, it will sometimes get centered in the cropping tool, sometimes not.

Scaling increases the complexity (for me). If you weren't scaling it, it would be an easy equation, just translate by the .enclosedCrop's getBoundingClientRect().top (the white centered circle) minus the image's getBoundingClientRect().top (same for the x values) and it would go where it is supposed to. But because I'm scaling it, it's location is changing and I don't know how to account for it.

It also has worked in the past (depending on which equation I settle on) only if I don't position the image by dragging the mouse in certain directions. The equation should work no matter how I translate/scale the image.

Scaling changes an element's location like so:

MDN : scale()

Here is an attempt with a distance (like) formula:

transform.x = Math.sqrt( (cropRect.left - self.imgRectLeftOrig) * (cropRect.left - self.imgRectLeftOrig) - (imgRect.left * transform.scale) * (imgRect.left * transform.scale));
transform.y = Math.sqrt( (cropRect.top - self.imgRectTopOrig) * (cropRect.top - self.imgRectTopOrig) -  (imgRect.top * transform.scale) * (imgRect.top * transform.scale));

It doesn't work because I'm not trying to get the distance between two points. Instead, I'm trying to translate along the x and y axis as appropriate to position the image on top of the crop circle. When I try to just subtract left from left and top from top, that doesn't seem to work either.

Distance and Translations Equation

Scale Equation

Given those two graphs, I don't understand why:

transform.x = cropRect.left - (imgRect.left * transform.scale);
transform.y = cropRect.top - (imgRect.top * transform.scale);

doesn't work...

Furthermore if I have an image, and I scale it using css("transform","scale(1.1) it seems to behave where the .getBoundingClientRect().top value goes down, not up, which makes the MDN graph more confusing to me, as far as I can tell they show the opposite happening. Maybe it's a CSS issue?

function initAngularCrop() {
  var self = this;
  self.data = {};
  self.childElements = {};
  self.type = type = 'small';
  viewBox = self.childElements.viewBox = document.createElement('div');
  enclosedCrop = self.childElements.enclosedCrop = document.createElement('div');
  img = self.childElements.img = document.createElement('img');
  overlay = self.childElements.overlay = document.createElement('div');
  viewBox.className = viewBox.className ? 'viewBox' + ' ' + viewBox.className : 'viewBox';
  enclosedCrop.className = enclosedCrop.className ? 'enclosedCrop' + ' ' + enclosedCrop.className : 'enclosedCrop';
  enclosedCrop.className = enclosedCrop.className ? enclosedCrop.className + ' ' + type : type;
  overlay.className = overlay.className ? 'overlay' + ' ' + overlay.className : 'overlay';
  overlay.className = overlay.className ? type + ' ' + overlay.className : type;
  img.className = img.className ? 'bigTuna' + ' ' + img.className : 'bigTuna';
  img.src = "https://images.genius.com/2774bb81e57abc5c808b50c45eaa75f2.600x600x1.jpg";
  document.body.appendChild(viewBox);
  viewBox.appendChild(img);
  viewBox.appendChild(enclosedCrop);
  viewBox.appendChild(overlay);
  initDraggable.call(self);
  zoomzoomwrap = self.childElements.zoomzoomwrap = document.createElement('div'),
    zoomzoom = self.childElements.zoomzoom = document.createElement('input');
  zoomzoomwrap.className = zoomzoomwrap.className ? 'zoomzoomboomboom' + ' ' + zoomzoomwrap.className : 'zoomzoomboomboom';
  zoomzoom.className = zoomzoom.className ? 'littleTuna' + ' ' + zoomzoom.className : 'littleTuna';
  zoomzoom.type = 'range';
  zoomzoom.step = '0.0001';
  zoomzoom.value = 5;
  zoomzoom.style.display = '';
  zoomzoom.setAttribute('aria-label', 'zoom');
  zoomzoom.min = 0;
  zoomzoom.max = 10;
  viewBox.parentNode.insertBefore(zoomzoomwrap, viewBox.nextSibling);
  zoomzoomwrap.appendChild(zoomzoom);
  self._currentZoom = 1;
  self.imgRectTopOrig = img.getBoundingClientRect().top;
  self.imgRectLeftOrig = img.getBoundingClientRect().left;

  function change() {
    initZoom.call(self, {
      value: parseFloat(zoomzoom.value / 5),
      origin: new TransformOrigin(img),
      viewportRect: enclosedCrop.getBoundingClientRect(),
      transform: Transform.parse(img)
    });
  }
  self.childElements.zoomzoom.addEventListener('input', change);
  //self.childElements.zoomzoom.addEventListener('change', change);
  //console.log(self.childElements.zoomzoom);
}

var _debouncetimer;
var _debounce = function(fn, delay, context) {
  //console.log("debounce run",fn);
  clearTimeout(_debouncetimer);
  _debouncetimer = setTimeout(function() {
    fn.call(context);
    //console.log("debounce fire!");
  }, delay);
};

function initZoom(ui) {
  var self = this,
    transform = ui ? ui.transform : Transform.parse(self.childElements.img),
    vpRect = ui ? ui.viewportRect : self.childElements.viewport.getBoundingClientRect(),
    origin = ui ? ui.origin : new TransformOrigin(self.childElements.img);

  function applyCss(bar) {
    if (self.type == "small") {
      var size = 150;
    }
    //console.log("apply zoom CSS");
    if (bar) {
      if (transform.scale * self.childElements.img.width > size && transform.scale * self.childElements.img.height > size) {
        var transCss = {};
        transCss[CSS_TRANSFORM] = transform.toString();
        css(self.childElements.img, transCss);
      } else {
        if (self.childElements.img.width > self.childElements.img.height) {
          transform.scale = size / self.childElements.img.width;
        } else {
          transform.scale = size / self.childElements.img.height;
        }
        var transCss = {};
        transCss[CSS_TRANSFORM] = transform.toString();
        css(self.childElements.img, transCss);
      }
      _debounce(_updateCenterPoint, 500, self);
    } else {
      _debounce(_updateCenterPoint, 500, self);
    }
  }
  if (self.type == "small") {
    if ((self.childElements.img.getBoundingClientRect().width <= 150 || self.childElements.img.getBoundingClientRect().height <= 150) && (ui.value < 1 && ui.value < self._currentZoom)) {
      applyCss(false);
      return;
    } else {
      self._currentZoom = ui ? ui.value : self._currentZoom;
      transform.scale = self._currentZoom;
      self.childElements.zoomzoom.setAttribute('aria-valuenow', self._currentZoom);
      applyCss(true);
      return;
    }
  }
  applyCss(); //othersizes

  /* _debouncedOverlay.call(self);
   _triggerUpdate.call(self);*/
}

function _getVirtualBoundaries(viewport) {
  var self = this,
    scale = self._currentZoom,
    vpWidth = viewport.width,
    vpHeight = viewport.height,
    centerFromBoundaryX = self.childElements.viewBox.clientWidth / 2,
    centerFromBoundaryY = self.childElements.viewBox.clientHeight / 2,
    imgRect = self.childElements.img.getBoundingClientRect(),
    curImgWidth = imgRect.width,
    curImgHeight = imgRect.height,
    halfWidth = vpWidth / 2,
    halfHeight = vpHeight / 2;

  var maxX = ((halfWidth / scale) - centerFromBoundaryX) * -1;
  var minX = maxX - ((curImgWidth * (1 / scale)) - (vpWidth * (1 / scale)));

  var maxY = ((halfHeight / scale) - centerFromBoundaryY) * -1;
  var minY = maxY - ((curImgHeight * (1 / scale)) - (vpHeight * (1 / scale)));

  var originMinX = (1 / scale) * halfWidth;
  var originMaxX = (curImgWidth * (1 / scale)) - originMinX;

  var originMinY = (1 / scale) * halfHeight;
  var originMaxY = (curImgHeight * (1 / scale)) - originMinY;

  return {
    translate: {
      maxX: maxX,
      minX: minX,
      maxY: maxY,
      minY: minY
    },
    origin: {
      maxX: originMaxX,
      minX: originMinX,
      maxY: originMaxY,
      minY: originMinY
    }
  };
}

function initDraggable() {
  var self = this,
    isDragging = false,
    originalX,
    originalY,
    originalDistance,
    vpRect,
    transform;

  function assignTransformCoordinates(deltaX, deltaY) {
    var imgRect = self.childElements.img.getBoundingClientRect(),
      top = transform.y + deltaY,
      left = transform.x + deltaX;
    if (vpRect.top + 15 > imgRect.top + deltaY && vpRect.bottom < 15 + imgRect.bottom + deltaY) {
      transform.y = top;
      //console.log("shift!");
    }

    if (vpRect.left + 15 > imgRect.left + deltaX && vpRect.right < 15 + imgRect.right + deltaX) {
      transform.x = left;
      //console.log("shift!");
    }
  }

  function mouseDown(ev) {
    if (ev.button !== undefined && ev.button !== 0) return;

    ev.preventDefault();
    if (isDragging) return;
    isDragging = true;
    originalX = ev.pageX;
    originalY = ev.pageY;

    if (ev.touches) {
      var touches = ev.touches[0];
      originalX = touches.pageX;
      originalY = touches.pageY;
    }

    transform = Transform.parse(self.childElements.img); //key point
    window.addEventListener('mousemove', mouseMove);
    window.addEventListener('touchmove', mouseMove);
    window.addEventListener('mouseup', mouseUp);
    window.addEventListener('touchend', mouseUp);
    document.body.style['userSelect'] = 'none';
    vpRect = self.childElements.enclosedCrop.getBoundingClientRect();
  }

  function mouseMove(ev) {
    //shift it around with mouse/touch
    ev.preventDefault();
    var pageX = ev.pageX,
      pageY = ev.pageY;

    if (ev.touches) {
      var touches = ev.touches[0];
      pageX = touches.pageX;
      pageY = touches.pageY;
    }

    var deltaX = pageX - originalX,
      deltaY = pageY - originalY,
      newCss = {};

    if (ev.type == 'touchmove') {
      if (ev.touches.length > 1) {
        var touch1 = ev.touches[0];
        var touch2 = ev.touches[1];
        var dist = Math.sqrt((touch1.pageX - touch2.pageX) * (touch1.pageX - touch2.pageX) + (touch1.pageY - touch2.pageY) * (touch1.pageY - touch2.pageY));
        //above is math :(
        if (!originalDistance) {
          originalDistance = dist / self._currentZoom;
        }

        var scale = dist / originalDistance;

        _setZoomerVal.call(self, scale);
        dispatchChange(self.childElements.zoomzoom);
        return; //why
      }
    }

    assignTransformCoordinates(deltaX, deltaY);
    //console.log(pageX,originalX,pageY,originalY,deltaX,deltaY);
    newCss['transform'] = transform.toString();
    //console.log(newCss,transform.toString());
    css(self.childElements.img, newCss);
    //_updateOverlay.call(self);
    originalY = pageY;
    originalX = pageX;
  }

  function mouseUp() {
    isDragging = false;
    window.removeEventListener('mousemove', mouseMove);
    window.removeEventListener('touchmove', mouseMove);
    window.removeEventListener('mouseup', mouseUp);
    window.removeEventListener('touchend', mouseUp);
    document.body.style['userSelect'] = '';
    //_updateCenterPoint.call(self);
    //_triggerUpdate.call(self);
    originalDistance = 0;
  }

  self.childElements.overlay.addEventListener('mousedown', mouseDown);
  self.childElements.overlay.addEventListener('touchstart', mouseDown);
} //initDraggable()

var TRANSLATE_OPTS = {
  'translate3d': {
    suffix: ', 0px'
  },
  'translate': {
    suffix: ''
  }
};
var Transform = function(x, y, scale) {
  this.x = parseFloat(x);
  this.y = parseFloat(y);
  this.scale = parseFloat(scale);
};

Transform.parse = function(v) {
  if (v.style) {
    return Transform.parse(v.style['transform']);
  } else if (v.indexOf('matrix') > -1 || v.indexOf('none') > -1) {
    return Transform.fromMatrix(v);
  } else {
    return Transform.fromString(v);
  }
};
Transform.fromMatrix = function(v) {
  var vals = v.substring(7).split(',');
  if (!vals.length || v === 'none') {
    vals = [1, 0, 0, 1, 0, 0];
  }

  return new Transform(num(vals[4]), num(vals[5]), parseFloat(vals[0]));
};

Transform.fromString = function(v) {
  var values = v.split(') '),
    translate = values[0].substring("translate3d".length + 1).split(','),
    scale = values.length > 1 ? values[1].substring(6) : 1,
    x = translate.length > 1 ? translate[0] : 0,
    y = translate.length > 1 ? translate[1] : 0;

  return new Transform(x, y, scale);
};
Transform.prototype.toString = function() {
  var suffix = TRANSLATE_OPTS["translate3d"].suffix || '';
  return "translate3d" + '(' + this.x + 'px, ' + this.y + 'px' + suffix + ') scale(' + this.scale + ')';
};
var TransformOrigin = function(el) {
  //console.log(el.style[CSS_TRANS_ORG] + " Transform Origin");
  if (!el || !el.style[CSS_TRANS_ORG]) {
    this.x = 0;
    this.y = 0;
    return;
  }
  var css = el.style[CSS_TRANS_ORG].split(' ');
  this.x = parseFloat(css[0]);
  this.y = parseFloat(css[1]);
};

TransformOrigin.prototype.toString = function() {
  return this.x + 'px ' + this.y + 'px';
};

function css(el, styles, val) {
  if (typeof(styles) === 'string') {
    var tmp = styles;
    styles = {};
    styles[tmp] = val;
  }

  for (var prop in styles) {
    el.style[prop] = styles[prop];
  }
}

function isContainedByCrop(imgRect, cropRect, transform) {
  if ((imgRect.top > cropRect.top || imgRect.bottom < cropRect.bottom) || (imgRect.left > cropRect.left || imgRect.right < cropRect.right)) {
    return false;
  } else {
    return true; //contained 
  }
}

var emptyStyles = document.createElement('div').style;
var cssPrefixes = ['Webkit', 'Moz', 'ms'];

function vendorPrefix(prop) {
  if (prop in emptyStyles) {
    return prop;
  }

  var capProp = prop[0].toUpperCase() + prop.slice(1),
    i = cssPrefixes.length;

  while (i--) {
    prop = cssPrefixes[i] + capProp;
    if (prop in emptyStyles) {
      return prop;
    }
  }
}

var CSS_TRANSFORM = vendorPrefix('transform');
var CSS_TRANS_ORG = vendorPrefix('transformOrigin');

function _updateCenterPoint() { //borked
  console.log("fire _updateCenterPoint()");
  var self = this,
    transform = Transform.parse(self.childElements.img.style[CSS_TRANSFORM]);
  var imgRect = self.childElements.img.getBoundingClientRect(),
    cropRect = self.childElements.enclosedCrop.getBoundingClientRect();
  if (!isContainedByCrop(imgRect, cropRect, transform)) {
    console.log("adjust center");
    transform.x = self.imgRectLeftOrig - (cropRect.left - imgRect.left - (transform.x * transform.scale));
    //console.log(self.childElements.enclosedCrop.offsetLeft,cropRect.left,self.childElements.img.offsetLeft,imgRect.left);
    transform.y = self.imgRectTopOrig - (cropRect.top - imgRect.top - (transform.y * transform.scale));
  }
  var newCss = {};
  //newCss['transformOrigin'] = center.x + 'px ' + center.y + 'px';
  newCss['transform'] = transform.toString();
  css(self.childElements.img, newCss);

}

new initAngularCrop();
.viewBox {
  position: relative;
  overflow: hidden;
  display: inline-block;
  margin: 0 auto;
  width: 500px;
  height: 500;
}

.enclosedCrop {
  position: absolute;
  width: 150px;
  height: 150px;
  margin: auto;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border: 2px solid #fff;
  background: rgba(0, 0, 0, 0.5);
  border-radius: 100%;
}

.overlay.small,
.overlay.large {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}
like image 612
Summer Developer Avatar asked Jan 12 '18 16:01

Summer Developer


1 Answers

The transform is applied to the original position, so making calculations that take the transform into account to adjust the transform, are necessarily complicated. You have to take into account the order of the transformations, the original position, and the transform origin.

For example, here, a square translated 100px left and scaled to 0.5 will be at position 125px. First it's moved to 100px, then scaled from the center so at 125px. If you scale before, then you are at 75px. Scaled to 50 px from the center (so at 25px left), then translated 100px scaled to 50px. So getting the coordinates right can be complicated.

body {
  margin: 0px;
}

#red {
  transform: scale(0.5) translate(100px, 100px);
}

#blue {
  transform: translate(100px, 100px) scale(0.5);
}

table {
  border-collapse: collapse;
}

td {
  height: 47px;
  width: 47px;
  border: solid 1px #aaa;
}
<div id="blue" style="position: absolute; width: 100px; height: 100px; background-color: blue;">

</div>

<div id="red"  style="position: absolute; width: 100px; height: 100px; background-color: red;">

</div>

<table>
<tr><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr><td></td><td></td><td></td><td></td><td></td><td></td></tr>
<tr><td></td><td></td><td></td><td></td><td></td><td></td></tr>

</table>

A simpler approach in your case, I think, would be to work with transform-origin. Your updateCenter function is exactly what transform-origin does, it defines the center. And it's built in. All the calculations will be handled by the transform. You only need to define the origin you need, and it's pretty straightforward.

Let's say you want the center to be the point that is at the center of the circle in your example. You need to find at with percentage this point is on the image your transforming. So it's basically the

(half the width of the circle + left offset of the circle - left offset of the image) / (width of image) * 100

Which in your case gives :

 (cropRect.left + (cropRect.width / 2) - matrix.m41) / self.childElements.img.offsetWidth * 100;

matrix.41 is the left translate of the image which you can find this way (see this answer: How to get value translateX by javascript):

  var transform = window.getComputedStyle(self.childElements.img).transform;
  var matrix = new WebKitCSSMatrix(transform);

then you assign:

self.childElements.img.style.transformOrigin = transformOriginX + "% " + transformOriginY + "%";

Obviously, you need to do this only when moving the image, not on the zoom, since then the origin will be handled already. You could also adjust only on certain conditions, but in any case, you needn't think about the translate and the zoom.

function initAngularCrop() {
  var self = this;
  self.data = {};
  self.childElements = {};
  self.type = type = 'small';
  viewBox = self.childElements.viewBox = document.createElement('div');
  enclosedCrop = self.childElements.enclosedCrop = document.createElement('div');
  img = self.childElements.img = document.createElement('img');
  overlay = self.childElements.overlay = document.createElement('div');
  viewBox.className = viewBox.className ? 'viewBox' + ' ' + viewBox.className : 'viewBox';
  enclosedCrop.className = enclosedCrop.className ? 'enclosedCrop' + ' ' + enclosedCrop.className : 'enclosedCrop';
  enclosedCrop.className = enclosedCrop.className ? enclosedCrop.className + ' ' + type : type;
  overlay.className = overlay.className ? 'overlay' + ' ' + overlay.className : 'overlay';
  overlay.className = overlay.className ? type + ' ' + overlay.className : type;
  img.className = img.className ? 'bigTuna' + ' ' + img.className : 'bigTuna';
  img.src = "https://images.genius.com/2774bb81e57abc5c808b50c45eaa75f2.600x600x1.jpg";
  document.body.appendChild(viewBox);
  viewBox.appendChild(img);
  viewBox.appendChild(enclosedCrop);
  viewBox.appendChild(overlay);
  initDraggable.call(self);
  zoomzoomwrap = self.childElements.zoomzoomwrap = document.createElement('div'),
    zoomzoom = self.childElements.zoomzoom = document.createElement('input');
  zoomzoomwrap.className = zoomzoomwrap.className ? 'zoomzoomboomboom' + ' ' + zoomzoomwrap.className : 'zoomzoomboomboom';
  zoomzoom.className = zoomzoom.className ? 'littleTuna' + ' ' + zoomzoom.className : 'littleTuna';
  zoomzoom.type = 'range';
  zoomzoom.step = '0.0001';
  zoomzoom.value = 5;
  zoomzoom.style.display = '';
  zoomzoom.setAttribute('aria-label', 'zoom');
  zoomzoom.min = 0;
  zoomzoom.max = 10;
  viewBox.parentNode.insertBefore(zoomzoomwrap, viewBox.nextSibling);
  zoomzoomwrap.appendChild(zoomzoom);
  self._currentZoom = 1;
  self.imgRectTopOrig = img.getBoundingClientRect().top;
  self.imgRectLeftOrig = img.getBoundingClientRect().left;

  function change() {
    initZoom.call(self, {
      value: parseFloat(zoomzoom.value / 5),
      origin: new TransformOrigin(img),
      viewportRect: enclosedCrop.getBoundingClientRect(),
      transform: Transform.parse(img)
    });
  }
  self.childElements.zoomzoom.addEventListener('input', change);
  //self.childElements.zoomzoom.addEventListener('change', change);
  //console.log(self.childElements.zoomzoom);
}

var _debouncetimer;
var _debounce = function(fn, delay, context) {
  //console.log("debounce run",fn);
  clearTimeout(_debouncetimer);
  _debouncetimer = setTimeout(function() {
    fn.call(context);
    //console.log("debounce fire!");
  }, delay);
};

function initZoom(ui) {
  var self = this,
    transform = ui ? ui.transform : Transform.parse(self.childElements.img),
    vpRect = ui ? ui.viewportRect : self.childElements.viewport.getBoundingClientRect(),
    origin = ui ? ui.origin : new TransformOrigin(self.childElements.img);

  function applyCss(bar) {
    if (self.type == "small") {
      var size = 150;
    }
    //console.log("apply zoom CSS");
    if (bar) {
      if (transform.scale * self.childElements.img.width > size && transform.scale * self.childElements.img.height > size) {
        var transCss = {};
        transCss[CSS_TRANSFORM] = transform.toString();
        css(self.childElements.img, transCss);
      } else {
        if (self.childElements.img.width > self.childElements.img.height) {
          transform.scale = size / self.childElements.img.width;
        } else {
          transform.scale = size / self.childElements.img.height;
        }
        var transCss = {};
        transCss[CSS_TRANSFORM] = transform.toString();
        css(self.childElements.img, transCss);
      }
      _debounce(_updateCenterPoint, 500, self);
    } else {
      _debounce(_updateCenterPoint, 500, self);
    }
  }
  if (self.type == "small") {
    if ((self.childElements.img.getBoundingClientRect().width <= 150 || self.childElements.img.getBoundingClientRect().height <= 150) && (ui.value < 1 && ui.value < self._currentZoom)) {
      applyCss(false);
      return;
    } else {
      self._currentZoom = ui ? ui.value : self._currentZoom;
      transform.scale = self._currentZoom;
      self.childElements.zoomzoom.setAttribute('aria-valuenow', self._currentZoom);
      applyCss(true);
      return;
    }
  }
  applyCss(); //othersizes

  /* _debouncedOverlay.call(self);
   _triggerUpdate.call(self);*/
}

function _getVirtualBoundaries(viewport) {
  var self = this,
    scale = self._currentZoom,
    vpWidth = viewport.width,
    vpHeight = viewport.height,
    centerFromBoundaryX = self.childElements.viewBox.clientWidth / 2,
    centerFromBoundaryY = self.childElements.viewBox.clientHeight / 2,
    imgRect = self.childElements.img.getBoundingClientRect(),
    curImgWidth = imgRect.width,
    curImgHeight = imgRect.height,
    halfWidth = vpWidth / 2,
    halfHeight = vpHeight / 2;

  var maxX = ((halfWidth / scale) - centerFromBoundaryX) * -1;
  var minX = maxX - ((curImgWidth * (1 / scale)) - (vpWidth * (1 / scale)));

  var maxY = ((halfHeight / scale) - centerFromBoundaryY) * -1;
  var minY = maxY - ((curImgHeight * (1 / scale)) - (vpHeight * (1 / scale)));

  var originMinX = (1 / scale) * halfWidth;
  var originMaxX = (curImgWidth * (1 / scale)) - originMinX;

  var originMinY = (1 / scale) * halfHeight;
  var originMaxY = (curImgHeight * (1 / scale)) - originMinY;

  return {
    translate: {
      maxX: maxX,
      minX: minX,
      maxY: maxY,
      minY: minY
    },
    origin: {
      maxX: originMaxX,
      minX: originMinX,
      maxY: originMaxY,
      minY: originMinY
    }
  };
}

function initDraggable() {
  var self = this,
    isDragging = false,
    originalX,
    originalY,
    originalDistance,
    vpRect,
    transform;

  function assignTransformCoordinates(deltaX, deltaY) {
    var imgRect = self.childElements.img.getBoundingClientRect(),
      top = transform.y + deltaY,
      left = transform.x + deltaX;
    if (vpRect.top + 15 > imgRect.top + deltaY && vpRect.bottom < 15 + imgRect.bottom + deltaY) {
      transform.y = top;
      //console.log("shift!");
    }

    if (vpRect.left + 15 > imgRect.left + deltaX && vpRect.right < 15 + imgRect.right + deltaX) {
      transform.x = left;
      //console.log("shift!");
    }
  }

  function mouseDown(ev) {
    if (ev.button !== undefined && ev.button !== 0) return;

    ev.preventDefault();
    if (isDragging) return;
    isDragging = true;
    originalX = ev.pageX;
    originalY = ev.pageY;

    if (ev.touches) {
      var touches = ev.touches[0];
      originalX = touches.pageX;
      originalY = touches.pageY;
    }

    transform = Transform.parse(self.childElements.img); //key point
    window.addEventListener('mousemove', mouseMove);
    window.addEventListener('touchmove', mouseMove);
    window.addEventListener('mouseup', mouseUp);
    window.addEventListener('touchend', mouseUp);
    document.body.style['userSelect'] = 'none';
    vpRect = self.childElements.enclosedCrop.getBoundingClientRect();
  }

  function mouseMove(ev) {
    //shift it around with mouse/touch
    ev.preventDefault();
    var pageX = ev.pageX,
      pageY = ev.pageY;

    if (ev.touches) {
      var touches = ev.touches[0];
      pageX = touches.pageX;
      pageY = touches.pageY;
    }

    var deltaX = pageX - originalX,
      deltaY = pageY - originalY,
      newCss = {};

    if (ev.type == 'touchmove') {
      if (ev.touches.length > 1) {
        var touch1 = ev.touches[0];
        var touch2 = ev.touches[1];
        var dist = Math.sqrt((touch1.pageX - touch2.pageX) * (touch1.pageX - touch2.pageX) + (touch1.pageY - touch2.pageY) * (touch1.pageY - touch2.pageY));
        //above is math :(
        if (!originalDistance) {
          originalDistance = dist / self._currentZoom;
        }

        var scale = dist / originalDistance;

        _setZoomerVal.call(self, scale);
        dispatchChange(self.childElements.zoomzoom);
        return; //why
      }
    }

    assignTransformCoordinates(deltaX, deltaY);
    //console.log(pageX,originalX,pageY,originalY,deltaX,deltaY);
    newCss['transform'] = transform.toString();
    //console.log(newCss,transform.toString());
    css(self.childElements.img, newCss);
    //_updateOverlay.call(self);
    originalY = pageY;
    originalX = pageX;
    _updateCenterPoint.call(self);
  }

  function mouseUp() {
    isDragging = false;
    window.removeEventListener('mousemove', mouseMove);
    window.removeEventListener('touchmove', mouseMove);
    window.removeEventListener('mouseup', mouseUp);
    window.removeEventListener('touchend', mouseUp);
    document.body.style['userSelect'] = '';

    //_triggerUpdate.call(self);
    originalDistance = 0;


  }

  self.childElements.overlay.addEventListener('mousedown', mouseDown);
  self.childElements.overlay.addEventListener('touchstart', mouseDown);

} //initDraggable()

var TRANSLATE_OPTS = {
  'translate3d': {
    suffix: ', 0px'
  },
  'translate': {
    suffix: ''
  }
};
var Transform = function(x, y, scale) {
  this.x = parseFloat(x);
  this.y = parseFloat(y);
  this.scale = parseFloat(scale);
};

Transform.parse = function(v) {
  if (v.style) {
    return Transform.parse(v.style['transform']);
  } else if (v.indexOf('matrix') > -1 || v.indexOf('none') > -1) {
    return Transform.fromMatrix(v);
  } else {
    return Transform.fromString(v);
  }
};
Transform.fromMatrix = function(v) {
  var vals = v.substring(7).split(',');
  if (!vals.length || v === 'none') {
    vals = [1, 0, 0, 1, 0, 0];
  }

  return new Transform(num(vals[4]), num(vals[5]), parseFloat(vals[0]));
};

Transform.fromString = function(v) {
  var values = v.split(') '),
    translate = values[0].substring("translate3d".length + 1).split(','),
    scale = values.length > 1 ? values[1].substring(6) : 1,
    x = translate.length > 1 ? translate[0] : 0,
    y = translate.length > 1 ? translate[1] : 0;

  return new Transform(x, y, scale);
};
Transform.prototype.toString = function() {
  var suffix = TRANSLATE_OPTS["translate3d"].suffix || '';
  return "translate3d" + '(' + this.x + 'px, ' + this.y + 'px' + suffix + ') scale(' + this.scale + ')';
};
var TransformOrigin = function(el) {
  //console.log(el.style[CSS_TRANS_ORG] + " Transform Origin");
  if (!el || !el.style[CSS_TRANS_ORG]) {
    this.x = 0;
    this.y = 0;
    return;
  }
  var css = el.style[CSS_TRANS_ORG].split(' ');
  this.x = parseFloat(css[0]);
  this.y = parseFloat(css[1]);
};

TransformOrigin.prototype.toString = function() {
  return this.x + 'px ' + this.y + 'px';
};

function css(el, styles, val) {
  if (typeof(styles) === 'string') {
    var tmp = styles;
    styles = {};
    styles[tmp] = val;
  }

  for (var prop in styles) {
    el.style[prop] = styles[prop];
  }
}

function isContainedByCrop(imgRect, cropRect, transform) {
  if ((imgRect.top > cropRect.top || imgRect.bottom < cropRect.bottom) || (imgRect.left > cropRect.left || imgRect.right < cropRect.right)) {
    return false;
  } else {
    return true; //contained 
  }
}

var emptyStyles = document.createElement('div').style;
var cssPrefixes = ['Webkit', 'Moz', 'ms'];

function vendorPrefix(prop) {
  if (prop in emptyStyles) {
    return prop;
  }

  var capProp = prop[0].toUpperCase() + prop.slice(1),
    i = cssPrefixes.length;

  while (i--) {
    prop = cssPrefixes[i] + capProp;
    if (prop in emptyStyles) {
      return prop;
    }
  }
}

var CSS_TRANSFORM = vendorPrefix('transform');
var CSS_TRANS_ORG = vendorPrefix('transformOrigin');

function _updateCenterPoint() { //borked
  //console.log("fire _updateCenterPoint()");
  var self = this;
  var imgRect = self.childElements.img.getBoundingClientRect(),
    cropRect = self.childElements.enclosedCrop.getBoundingClientRect();
  var transform = window.getComputedStyle(self.childElements.img).transform;
  var matrix = new WebKitCSSMatrix(transform);
  var transformOriginX = (cropRect.left + (cropRect.width / 2) - matrix.m41) / self.childElements.img.offsetWidth * 100;
  var transformOriginY = (self.childElements.enclosedCrop.offsetTop + (cropRect.height / 2) - matrix.m42) / self.childElements.img.offsetHeight * 100;

  self.childElements.img.style.transformOrigin = transformOriginX + "% " + transformOriginY + "%";

}

new initAngularCrop();
body {
  margin: 0px;
}

.viewBox {
  position: relative;
  overflow: hidden;
  display: inline-block;
  margin: 0 auto;
  width: 500px;
  height: 500;
}

.enclosedCrop {
  position: absolute;
  width: 150px;
  height: 150px;
  margin: auto;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border: 2px solid #fff;
  background: rgba(0, 0, 0, 0.5);
  border-radius: 100%;
}

.overlay.small,
.overlay.large {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}
like image 120
Julien Grégoire Avatar answered Sep 18 '22 21:09

Julien Grégoire