草庐IT

javascript - 如何计算元素旋转的变换平移(x,y)补偿?

coder 2024-12-29 原文

我正在制作个人资料图片裁剪编辑器,它允许在区域内拖动、缩放和旋转图像。

图片的拖动是通过捕捉区域的mousedown和mousemove事件,计算区域内游标开始和停止的x/y坐标,得到游标移动的距离。然后将此值添加到图像的当前内联样式转换 translate(x, y) 值或从中减去(取决于方向)。

var dragArea = document.getElementById('drag-area');
var photoImg = document.getElementById('photo');
var cropCircle = document.getElementById('crop-circle');
var cloneContainer = document.getElementById('clone-container');
var resetAll = document.getElementById('reset-all');
var scaleSlider = document.getElementById('scale-slider');
var scaleInput = document.getElementById('scale-input');
var scaleReset = document.getElementById('scale-reset');
var rotateSlider = document.getElementById('rotate-slider');
var rotateInput = document.getElementById('rotate-input');
var rotateReset = document.getElementById('rotate-reset');
var area = {}, photo = {
  translate: {
    x: 0, y: 0
  }, 
  transformOrigin: {
    x: 0, y: 0
  }
};

photoImg.src = photoSrc();
photoImg.style.top = cropCircle.offsetTop+'px';
photoImg.style.left = cropCircle.offsetLeft+'px';
photoImg.style.transform = 'scale(1) rotate(0deg) translate(0px, 0px)';
photoImg.style.transformOrigin = '0px 0px';
photoImg.onload = function() {
  if (this.naturalWidth < this.naturalHeight) {
    this.width = cropCircle.clientWidth;
  } else if (this.naturalWidth > this.naturalHeight) {
    this.height = cropCircle.clientHeight;
  } else {
    this.height = cropCircle.clientHeight;
    this.width = cropCircle.clientWidth;
  }
}

dragArea.onmouseenter = function() {
  this.onmousedown = function(e) {
    var transform = photoImg.style.transform;
    var photoStyle = window.getComputedStyle(photoImg);
    var photoMatrix = new DOMMatrix(photoStyle.transform);
    var transformOrigin = photoImg.style.transformOrigin.replace(/px/g, '').split(' ');
  
    photo = {
      translate: {},
      x: photoMatrix.m41,
      y: photoMatrix.m42,
      scale: Number(/scale\((-?\d+(?:\.\d*)?)\)/.exec(transform)[1]),
      rotate: Number(/rotate\((-?\d+(?:\.\d*)?)deg\)/.exec(transform)[1]),
      transformOrigin: {
        x: Number(transformOrigin[0]),
        y: Number(transformOrigin[1])
      }
    }

    area = {
      start: {
        x: e.offsetX + (e.target == cropCircle ? cropCircle.offsetLeft : 0),
        y: e.offsetY + (e.target == cropCircle ? cropCircle.offsetTop : 0)
      },
      distance: {
        x: 0,
        y: 0
      }
    };

    this.onmousemove = function(e) {
      area.end = {
        x: e.offsetX + (e.target == cropCircle ? cropCircle.offsetLeft : 0),
        y: e.offsetY + (e.target == cropCircle ? cropCircle.offsetTop : 0)
      };

      if (area.end.x > area.start.x) {
        area.distance.x = {
          type: 'positive', // right
          total: area.end.x - area.start.x
        }
      } else {
        area.distance.x = {
          type: 'negative', // left
          total: area.start.x - area.end.x
        }
      }
      if (area.end.y > area.start.y) {
        area.distance.y = {
          type: 'positive', // down
          total: area.end.y - area.start.y
        }
      } else {
        area.distance.y = {
          type: 'negative', // up
          total: area.start.y - area.end.y
        }
      }

      if (area.distance.x.type == 'positive') {
        photo.translate.x = photo.x + area.distance.x.total;
      } else {
        photo.translate.x = photo.x - area.distance.x.total;
      }
      if (area.distance.y.type == 'positive') {
        photo.translate.y = photo.y + area.distance.y.total;
      } else {
        photo.translate.y = photo.y - area.distance.y.total;
      }

      photoTransform({x: photo.translate.x, y: photo.translate.y});
    }
  }
}

dragArea.onmouseleave = function() {
  this.onmousemove = function(e) {
    e.preventDefault();
  }
}

dragArea.onmouseup = function() {
  this.onmousemove = function(e) {
    e.preventDefault();
  }
}

resetAll.onclick = function() {
  scaleSlider.value = scaleReset.value;
  scaleInput.value = scaleReset.value;
  rotateSlider.value = rotateReset.value;
  rotateInput.value = rotateReset.value;
  photo = {
    translate: {
      x: 0, y: 0
    }, 
    transformOrigin: {
      x: 0, y: 0
    }
  };
  photoTransform({scale: 1, rotate: '0', x: '0', y: '0'});
}

scaleSlider.oninput = function() {
  var value = this.value;
  scaleInput.value = value;
  photoTransform({scale: value});
}
scaleInput.oninput = function() {
  var value = this.value;
  this.value = value.length ? value : scaleReset.value;
  scaleSlider.value = this.value;
  photoTransform({scale: this.value});
}
scaleInput.onkeydown = function(e) {
  if (e.keyCode == 13) this.blur();
}
scaleInput.onblur = function() {
  var value = this.value;
  this.value = value.length ? value : scaleReset.value;
  scaleSlider.value = this.value;
  photoTransform({scale: this.value});
}
scaleReset.onclick = function() {
  scaleSlider.value = this.value;
  scaleInput.value = this.value;
  photoTransform({scale: this.value});
}

rotateSlider.oninput = function() {
  var value = this.value;
  rotateInput.value = value;
  photoTransform({rotate: value});
}
rotateInput.oninput = function() {
  var value = this.value;
  this.value = value.length ? value : rotateReset.value;
  rotateSlider.value = this.value;
  photoTransform({rotate: this.value});
}
rotateInput.onkeydown = function(e) {
  if (e.keyCode == 13) this.blur();
}
rotateInput.onblur = function() {
  var value = this.value;
  this.value = value.length ? value : rotateReset.value;
  rotateSlider.value = this.value;
  photoTransform({rotate: this.value});
}
rotateReset.onclick = function() {
  rotateSlider.value = this.value;
  rotateInput.value = this.value;
  photoTransform({rotate: this.value});
}

function photoTransform(property) {
  property = property || {};
  var transform = photoImg.style.transform;
  var axisX = property.axisX || photo.transformOrigin.x || (cropCircle.getBoundingClientRect().width / 2);
  var axisY = property.axisY || photo.transformOrigin.y || (cropCircle.getBoundingClientRect().height / 2);
  var scale = property.scale || photo.scale || Number(/scale\((-?\d+(?:\.\d*)?)\)/.exec(transform)[1]);
  var rotate = property.rotate || photo.rotate || Number(/rotate\((-?\d+(?:\.\d*)?)deg\)/.exec(transform)[1]);
  var translate = /translate\((-?\d+(?:\.\d*)?)px, (-?\d+(?:\.\d*)?)px\)/.exec(transform);
  var translateX = (property.x || photo.translate.x || Number(translate[1])) / scale;
  var translateY = (property.y || photo.translate.y || Number(translate[2])) / scale;

  photoImg.style.transformOrigin = axisX+'px '+axisY+'px';
  photoImg.style.transform = 'scale('+scale+') rotate('+rotate+'deg) translate('+translateX+'px, '+translateY+'px)';
  
  photo.transformOrigin = {
    x: axisX,
    y: axisY
  }
  photo.scale = scale;
  photo.rotate = rotate;
}

function photoSrc() {
  return 'data:image/jpeg;base64,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AENybkNib29sAAAAAABDbnRDYm9vbAAAAAAATGJsc2Jvb2wAAAAAAE5ndHZib29sAAAAAABFbWxEYm9vbAAAAAAASW50cmJvb2wAAAAAAEJja2dPYmpjAAAAAQAAAAAAAFJHQkMAAAADAAAAAFJkICBkb3ViQG/gAAAAAAAAAAAAR3JuIGRvdWJAb+AAAAAAAAAAAABCbCAgZG91YkBv4AAAAAAAAAAAAEJyZFRVbnRGI1JsdAAAAAAAAAAAAAAAAEJsZCBVbnRGI1JsdAAAAAAAAAAAAAAAAFJzbHRVbnRGI1B4bEBSAAAAAAAAAAAACnZlY3RvckRhdGFib29sAQAAAABQZ1BzZW51bQAAAABQZ1BzAAAAAFBnUEMAAAAATGVmdFVudEYjUmx0AAAAAAAAAAAAAAAAVG9wIFVudEYjUmx0AAAAAAAAAAAAAAAAU2NsIFVudEYjUHJjQFkAAAAAAAAAAAAQY3JvcFdoZW5QcmludGluZ2Jvb2wAAAAADmNyb3BSZWN0Qm90dG9tbG9uZwAAAAAAAAAMY3JvcFJlY3RMZWZ0bG9uZwAAAAAAAAANY3JvcFJlY3RSaWdodGxvbmcAAAAAAAAAC2Nyb3BSZWN0VG9wbG9uZwAAAAAAOEJJTQPtAAAAAAAQAEgAAAABAAIASAAAAAEAAjhCSU0EJgAAAAAADgAAAAAAAAAAAAA/gAAAOEJJTQQNAAAAAAAEAAAAHjhCSU0EGQAAAAAABAAAAB44QklNA/MAAAAAAAkAAAAAAAAAAAEAOEJJTScQAAAAAAAKAAEAAAAAAAAAAjhCSU0D9QAAAAAASAAvZmYAAQBsZmYABgAAAAAAAQAvZmYAAQChmZoABgAAAAAAAQAyAAAAAQBaAAAABgAAAAAAAQA1AAAAAQAtAAAABgAAAAAAAThCSU0D+AAAAAAAcAAA/////////////////////////////wPoAAAAAP////////////////////////////8D6AAAAAD/////////////////////////////A+gAAAAA/////////////////////////////wPoAAA4QklNBAAAAAAAAAIAADhCSU0EAgAAAAAAAgAAOEJJTQQwAAAAAAABAQA4QklNBC0AAAAAAAIAADhCSU0ECAAAAAAAEAAAAAEAAAJAAAACQAAA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NzIyLWNhNDkzZjBjMzE5MyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2MjUyN2FhMS1jMjhmLTQ3ZjEtYWRiMi00MWZlYThhY2JkMGQiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0iRUFBQTAyMEJDQ0NGNDZGNzdCQ0NCNUJEQjVFRkRFREEiIGRjOmZvcm1hdD0iaW1hZ2UvanBlZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9IiIgeG1wOkNyZWF0ZURhdGU9IjIwMTktMDEtMThUMTc6NTY6MzMrMDE6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDE5LTAxLTE4VDE3OjU4OjE2KzAxOjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDE5LTAxLTE4VDE3OjU4OjE2KzAxOjAwIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjQ1YjRhOGYtMmYzNC00NzVjLWEzYTMtMGJjMmI2ZjAwYWEzIiBzdEV2dDp3aGVuPSIyMDE5LTAxLTE4VDE3OjU4OjE2KzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNyAoTWFjaW50b3NoKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6NjI1MjdhYTEtYzI4Zi00N2YxLWFkYjItNDFmZWE4YWNiZDBkIiBzdEV2dDp3aGVuPSIyMDE5LTAxLTE4VDE3OjU4OjE2KzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNyAoTWFjaW50b3NoKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/Pv/uAA5BZG9iZQBkgAAAAAH/2wCEACAhITMkM1EwMFFCLy8vQiccHBwcJyIXFxcXFyIRDAwMDAwMEQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBIjMzNCY0IhgYIhQODg4UFA4ODg4UEQwMDAwMEREMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDP/AABEIAMgAyAMBIgACEQEDEQH/3QAEAA3/xAEbAAADAQEBAQEBAQEBAAAAAAABAAIDBAUGBwgJCgsBAQEBAQEBAQEBAQEBAAAAAAABAgMEBQYHCAkKCxAAAgIBAwIDBAcGAwMGAgE1AQACEQMhEjEEQVEiE2FxMoGRsUKhBdHBFPBSI3IzYuGC8UM0kqKyFdJTJHPCYwaDk+Lyo0RUZCU1RRYmdDZVZbOEw9N14/NGlKSFtJXE1OT0pbXF1eX1VmZ2hpamtsbW5vYRAAICAAUBBgYBAwEDBQMGLwABEQIhAzFBElFhcYGRIhMy8KGxBMHR4fFCUiNichSSM4JDJKKyNFNEY3PC0oOTo1Ti8gUVJQYWJjVkRVU2dGWzhMPTdePzRpSkhbSVxNTk9KW1xdXl9VZmdob/2gAMAwEAAhEDEQA/AIVVfpHyxVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUD//QhVV+kfLFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQP/9GFVX6R8sVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVA//0oVVfpHyxVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUD//ThVV+kfLFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQP/9SFVX6R8sVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVA//1YVVfpHyxVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUD//WhVV+kfLFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQP/9eFVX6R8sVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVA//0IVVfpHyxVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUD//RhVV+kfLFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQP/9KFVX6R8sVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVA//04V8JX6R8s91XwlQPdV8JUD3VfCVA91XwlQPdV8JUD3VfCVA91XwlQPdV8JUD3VfCVA91XwlQPdV8JUD3VfCVA//2Q==';
}
body {
  background-color: #eff1f3;
}
#profile-picture {
  width: 370px;
  height: 330px;
  margin: auto;
}
#profile-picture * {
  user-select: none;
}
#drag-area {
  width: 100%;
  height: 100%;
  cursor: move;
  cursor: grab;
  display: block;
  overflow: hidden;
  position: relative;
  background-color: #000;
  background-repeat: repeat;
  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAJElEQVQoU2M8e/asMQMaMDY2PosuxjgUFKI7GsTH5m7GIaAQAA4fIQ1WOLcWAAAAAElFTkSuQmCC');
}
#drag-area:active {
  cursor: grabbing;
}
#clone-container {
  width: 0px;
  height: 0px;
  display: block;
  overflow: hidden;
  position: absolute;
}
#photo, #photo-clone {
  display: block;
  min-width: 230px;
  min-height: 230px;
  position: absolute;
  pointer-events: none;
}
img[src=''] {
  visibility: hidden;
}
#crop-circle {
  width: 230px;
  height: 230px;
  margin: 50px auto;
  overflow: hidden;
  position: relative;
  border-radius: 50%;
  box-shadow: 0 0 0 2px #fff, 0 0 0 100vw rgba(0,0,0,0.5);
}
#circle-thirds {
  top: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  position: absolute;
  pointer-events: none;
  border-radius: 100%;
}
#circle-thirds * {
  z-index: 1;
  position: absolute;
  background-color: rgba(226,226,226,0.5);
}
#circle-thirds .top-horizontal {
  width: 100%;
  height: 1px;
  top: 33.33333%;
}
#circle-thirds .bottom-horizontal {
  width: 100%;
  height: 1px;
  top: 66.66666%;
}
#circle-thirds .left-vertical {
  height: 100%;
  width: 1px;
  left: 33.33333%;
}
#circle-thirds .right-vertical {
  height: 100%;
  width: 1px;
  left: 66.66666%;
}
.photo-options {
  width: 100%;
  display: block;
  position: relative;
  padding-top: 15px;
}
.option-buttons {
  width: 100%;
  display: flex;
  position: relative;
  padding-bottom: 10px;
  justify-content: space-between;
}
.option-buttons button {
  width: 100%;
}
.option-buttons button + button {
  margin-left: 10px;
}
.photo-options fieldset {
  margin: 0px;
}
.photo-options fieldset + fieldset {
  margin-top: 10px;
}
.option-slider {
  display: flex;
  position: relative;
}
.option-slider input[type=range] {
  width: 50%;
  flex-shrink: 0;
}
.option-slider input[type=number] {
  width: 20%;
  margin: 0 10px;
}
.option-slider button {
  width: 30%;
}
<div id="profile-picture">
  <div id="drag-area">
    <div id="clone-container"></div>
    <img id="photo" src="">
    <div id="crop-circle">
      <div id="circle-thirds">
        <span class="top-horizontal"></span>
        <span class="bottom-horizontal"></span>
        <span class="left-vertical"></span>
        <span class="right-vertical"></span>
      </div>
    </div>
  </div>
  <div class="photo-options">
    <div class="option-buttons">
      <button id="reset-all">Reset everything</button>
    </div>
    <fieldset>
      <legend>Scale</legend>
        <div class="option-slider">
        <input type="range" id="scale-slider" min="1" max="3" step="0.01" value="1">
        <input type="number" id="scale-input" min="1" max="3" step="0.01" value="1">
        <button id="scale-reset" value="1">Reset</button>
      </div>
    </fieldset>
    <fieldset>
      <legend>Rotate</legend>
      <div class="option-slider">
        <input type="range" id="rotate-slider" min="-180" max="180" step="1" value="0">
        <input type="number" id="rotate-input" min="-180" max="180" step="1" value="0">
        <button id="rotate-reset" value="0">Reset</button>
      </div>
    </fieldset>
  </div>
</div>

问题是当图像旋转时,它的 translate(x y) 值不再对应于该区域的 x/y 坐标。

我找到了几个示例,说明如何使用旋转 Angular 弧度以及 cos 和 sin 来计算旋转正方形或矩形的四个 Angular 的 x/y 坐标。但是因为几何学不是我的强项,所以我不知道这些计算如何应用于图像的 translate(x, y) 值。

这是图像裁剪器当前工作方式的笔迹:https://codepen.io/ClubAce/pen/maNJNZ

不管图像的旋转如何,图像都应该沿着区域的 x/y 轴拖动。

我真的希望有人能帮我弄清楚如何修改脚本以产生所需的拖动行为。

谢谢。

最佳答案

你只需要改变顺序:

 photoImg.style.transform = 'scale('+scale+') rotate('+rotate+'deg)  translate('+translateX+'px, '+translateY+'px) ';

为此

photoImg.style.transform = 'scale('+scale+') translate('+translateX+'px, '+translateY+'px) rotate('+rotate+'deg) ';

Order is very important when using Transform

完整代码:

var dragArea = document.getElementById('drag-area');
var photoImg = document.getElementById('photo');
var cropCircle = document.getElementById('crop-circle');
var cloneContainer = document.getElementById('clone-container');
var resetAll = document.getElementById('reset-all');
var scaleSlider = document.getElementById('scale-slider');
var scaleInput = document.getElementById('scale-input');
var scaleReset = document.getElementById('scale-reset');
var rotateSlider = document.getElementById('rotate-slider');
var rotateInput = document.getElementById('rotate-input');
var rotateReset = document.getElementById('rotate-reset');
var area = {}, photo = {
  translate: {
    x: 0, y: 0
  }, 
  transformOrigin: {
    x: 0, y: 0
  }
};

photoImg.src = photoSrc();
photoImg.style.top = cropCircle.offsetTop+'px';
photoImg.style.left = cropCircle.offsetLeft+'px';
photoImg.style.transform = 'scale(1) rotate(0deg) translate(0px, 0px)';
photoImg.style.transformOrigin = '0px 0px';
photoImg.onload = function() {
  if (this.naturalWidth < this.naturalHeight) {
    this.width = cropCircle.clientWidth;
  } else if (this.naturalWidth > this.naturalHeight) {
    this.height = cropCircle.clientHeight;
  } else {
    this.height = cropCircle.clientHeight;
    this.width = cropCircle.clientWidth;
  }
}

dragArea.onmouseenter = function() {
  this.onmousedown = function(e) {
    var transform = photoImg.style.transform;
    var photoStyle = window.getComputedStyle(photoImg);
    var photoMatrix = new DOMMatrix(photoStyle.transform);
    var transformOrigin = photoImg.style.transformOrigin.replace(/px/g, '').split(' ');
  
    photo = {
      translate: {},
      x: photoMatrix.m41,
      y: photoMatrix.m42,
      scale: Number(/scale\((-?\d+(?:\.\d*)?)\)/.exec(transform)[1]),
      rotate: Number(/rotate\((-?\d+(?:\.\d*)?)deg\)/.exec(transform)[1]),
      transformOrigin: {
        x: Number(transformOrigin[0]),
        y: Number(transformOrigin[1])
      }
    }

    area = {
      start: {
        x: e.offsetX + (e.target == cropCircle ? cropCircle.offsetLeft : 0),
        y: e.offsetY + (e.target == cropCircle ? cropCircle.offsetTop : 0)
      },
      distance: {
        x: 0,
        y: 0
      }
    };

    this.onmousemove = function(e) {
      area.end = {
        x: e.offsetX + (e.target == cropCircle ? cropCircle.offsetLeft : 0),
        y: e.offsetY + (e.target == cropCircle ? cropCircle.offsetTop : 0)
      };

      if (area.end.x > area.start.x) {
        area.distance.x = {
          type: 'positive', // right
          total: area.end.x - area.start.x
        }
      } else {
        area.distance.x = {
          type: 'negative', // left
          total: area.start.x - area.end.x
        }
      }
      if (area.end.y > area.start.y) {
        area.distance.y = {
          type: 'positive', // down
          total: area.end.y - area.start.y
        }
      } else {
        area.distance.y = {
          type: 'negative', // up
          total: area.start.y - area.end.y
        }
      }

      if (area.distance.x.type == 'positive') {
        photo.translate.x = photo.x + area.distance.x.total;
      } else {
        photo.translate.x = photo.x - area.distance.x.total;
      }
      if (area.distance.y.type == 'positive') {
        photo.translate.y = photo.y + area.distance.y.total;
      } else {
        photo.translate.y = photo.y - area.distance.y.total;
      }

      photoTransform({x: photo.translate.x, y: photo.translate.y});
    }
  }
}

dragArea.onmouseleave = function() {
  this.onmousemove = function(e) {
    e.preventDefault();
  }
}

dragArea.onmouseup = function() {
  this.onmousemove = function(e) {
    e.preventDefault();
  }
}

resetAll.onclick = function() {
  scaleSlider.value = scaleReset.value;
  scaleInput.value = scaleReset.value;
  rotateSlider.value = rotateReset.value;
  rotateInput.value = rotateReset.value;
  photo = {
    translate: {
      x: 0, y: 0
    }, 
    transformOrigin: {
      x: 0, y: 0
    }
  };
  photoTransform({scale: 1, rotate: '0', x: '0', y: '0'});
}

scaleSlider.oninput = function() {
  var value = this.value;
  scaleInput.value = value;
  photoTransform({scale: value});
}
scaleInput.oninput = function() {
  var value = this.value;
  this.value = value.length ? value : scaleReset.value;
  scaleSlider.value = this.value;
  photoTransform({scale: this.value});
}
scaleInput.onkeydown = function(e) {
  if (e.keyCode == 13) this.blur();
}
scaleInput.onblur = function() {
  var value = this.value;
  this.value = value.length ? value : scaleReset.value;
  scaleSlider.value = this.value;
  photoTransform({scale: this.value});
}
scaleReset.onclick = function() {
  scaleSlider.value = this.value;
  scaleInput.value = this.value;
  photoTransform({scale: this.value});
}

rotateSlider.oninput = function() {
  var value = this.value;
  rotateInput.value = value;
  photoTransform({rotate: value});
}
rotateInput.oninput = function() {
  var value = this.value;
  this.value = value.length ? value : rotateReset.value;
  rotateSlider.value = this.value;
  photoTransform({rotate: this.value});
}
rotateInput.onkeydown = function(e) {
  if (e.keyCode == 13) this.blur();
}
rotateInput.onblur = function() {
  var value = this.value;
  this.value = value.length ? value : rotateReset.value;
  rotateSlider.value = this.value;
  photoTransform({rotate: this.value});
}
rotateReset.onclick = function() {
  rotateSlider.value = this.value;
  rotateInput.value = this.value;
  photoTransform({rotate: this.value});
}

function photoTransform(property) {
  property = property || {};
  var transform = photoImg.style.transform;
  var axisX = property.axisX || photo.transformOrigin.x || (cropCircle.getBoundingClientRect().width / 2);
  var axisY = property.axisY || photo.transformOrigin.y || (cropCircle.getBoundingClientRect().height / 2);
  var scale = property.scale || photo.scale || Number(/scale\((-?\d+(?:\.\d*)?)\)/.exec(transform)[1]);
  var rotate = property.rotate || photo.rotate || Number(/rotate\((-?\d+(?:\.\d*)?)deg\)/.exec(transform)[1]);
  var translate = /translate\((-?\d+(?:\.\d*)?)px, (-?\d+(?:\.\d*)?)px\)/.exec(transform);
  var translateX = (property.x || photo.translate.x || Number(translate[1])) / scale;
  var translateY = (property.y || photo.translate.y || Number(translate[2])) / scale;

  photoImg.style.transformOrigin = axisX+'px '+axisY+'px';
  photoImg.style.transform = 'scale('+scale+') translate('+translateX+'px, '+translateY+'px) rotate('+rotate+'deg) ';
  
  photo.transformOrigin = {
    x: axisX,
    y: axisY
  }
  photo.scale = scale;
  photo.rotate = rotate;
}

function photoSrc() {
  return 'data:image/jpeg;base64,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AENybkNib29sAAAAAABDbnRDYm9vbAAAAAAATGJsc2Jvb2wAAAAAAE5ndHZib29sAAAAAABFbWxEYm9vbAAAAAAASW50cmJvb2wAAAAAAEJja2dPYmpjAAAAAQAAAAAAAFJHQkMAAAADAAAAAFJkICBkb3ViQG/gAAAAAAAAAAAAR3JuIGRvdWJAb+AAAAAAAAAAAABCbCAgZG91YkBv4AAAAAAAAAAAAEJyZFRVbnRGI1JsdAAAAAAAAAAAAAAAAEJsZCBVbnRGI1JsdAAAAAAAAAAAAAAAAFJzbHRVbnRGI1B4bEBSAAAAAAAAAAAACnZlY3RvckRhdGFib29sAQAAAABQZ1BzZW51bQAAAABQZ1BzAAAAAFBnUEMAAAAATGVmdFVudEYjUmx0AAAAAAAAAAAAAAAAVG9wIFVudEYjUmx0AAAAAAAAAAAAAAAAU2NsIFVudEYjUHJjQFkAAAAAAAAAAAAQY3JvcFdoZW5QcmludGluZ2Jvb2wAAAAADmNyb3BSZWN0Qm90dG9tbG9uZwAAAAAAAAAMY3JvcFJlY3RMZWZ0bG9uZwAAAAAAAAANY3JvcFJlY3RSaWdodGxvbmcAAAAAAAAAC2Nyb3BSZWN0VG9wbG9uZwAAAAAAOEJJTQPtAAAAAAAQAEgAAAABAAIASAAAAAEAAjhCSU0EJgAAAAAADgAAAAAAAAAAAAA/gAAAOEJJTQQNAAAAAAAEAAAAHjhCSU0EGQAAAAAABAAAAB44QklNA/MAAAAAAAkAAAAAAAAAAAEAOEJJTScQAAAAAAAKAAEAAAAAAAAAAjhCSU0D9QAAAAAASAAvZmYAAQBsZmYABgAAAAAAAQAvZmYAAQChmZoABgAAAAAAAQAyAAAAAQBaAAAABgAAAAAAAQA1AAAAAQAtAAAABgAAAAAAAThCSU0D+AAAAAAAcAAA/////////////////////////////wPoAAAAAP////////////////////////////8D6AAAAAD/////////////////////////////A+gAAAAA/////////////////////////////wPoAAA4QklNBAAAAAAAAAIAADhCSU0EAgAAAAAAAgAAOEJJTQQwAAAAAAABAQA4QklNBC0AAAAAAAIAADhCSU0ECAAAAAAAEAAAAAEAAAJAAAACQAAA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NzIyLWNhNDkzZjBjMzE5MyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2MjUyN2FhMS1jMjhmLTQ3ZjEtYWRiMi00MWZlYThhY2JkMGQiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0iRUFBQTAyMEJDQ0NGNDZGNzdCQ0NCNUJEQjVFRkRFREEiIGRjOmZvcm1hdD0iaW1hZ2UvanBlZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9IiIgeG1wOkNyZWF0ZURhdGU9IjIwMTktMDEtMThUMTc6NTY6MzMrMDE6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDE5LTAxLTE4VDE3OjU4OjE2KzAxOjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDE5LTAxLTE4VDE3OjU4OjE2KzAxOjAwIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjQ1YjRhOGYtMmYzNC00NzVjLWEzYTMtMGJjMmI2ZjAwYWEzIiBzdEV2dDp3aGVuPSIyMDE5LTAxLTE4VDE3OjU4OjE2KzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNyAoTWFjaW50b3NoKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6NjI1MjdhYTEtYzI4Zi00N2YxLWFkYjItNDFmZWE4YWNiZDBkIiBzdEV2dDp3aGVuPSIyMDE5LTAxLTE4VDE3OjU4OjE2KzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNyAoTWFjaW50b3NoKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/Pv/uAA5BZG9iZQBkgAAAAAH/2wCEACAhITMkM1EwMFFCLy8vQiccHBwcJyIXFxcXFyIRDAwMDAwMEQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBIjMzNCY0IhgYIhQODg4UFA4ODg4UEQwMDAwMEREMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDP/AABEIAMgAyAMBIgACEQEDEQH/3QAEAA3/xAEbAAADAQEBAQEBAQEBAAAAAAABAAIDBAUGBwgJCgsBAQEBAQEBAQEBAQEBAAAAAAABAgMEBQYHCAkKCxAAAgIBAwIDBAcGAwMGAgE1AQACEQMhEjEEQVEiE2FxMoGRsUKhBdHBFPBSI3IzYuGC8UM0kqKyFdJTJHPCYwaDk+Lyo0RUZCU1RRYmdDZVZbOEw9N14/NGlKSFtJXE1OT0pbXF1eX1VmZ2hpamtsbW5vYRAAICAAUBBgYBAwEDBQMGLwABEQIhAzFBElFhcYGRIhMy8KGxBMHR4fFCUiNichSSM4JDJKKyNFNEY3PC0oOTo1Ti8gUVJQYWJjVkRVU2dGWzhMPTdePzRpSkhbSVxNTk9KW1xdXl9VZmdob/2gAMAwEAAhEDEQA/AIVVfpHyxVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUD//QhVV+kfLFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQP/9GFVX6R8sVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVA//0oVVfpHyxVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUD//ThVV+kfLFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQP/9SFVX6R8sVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVA//1YVVfpHyxVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUD//WhVV+kfLFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQP/9eFVX6R8sVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVA//0IVVfpHyxVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUD//RhVV+kfLFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQP/9KFVX6R8sVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVAVVUBVVQFVVA//04V8JX6R8s91XwlQPdV8JUD3VfCVA91XwlQPdV8JUD3VfCVA91XwlQPdV8JUD3VfCVA91XwlQPdV8JUD3VfCVA//2Q==';
}
body {
  background-color: #eff1f3;
}
#profile-picture {
  width: 370px;
  height: 330px;
  margin: auto;
}
#profile-picture * {
  user-select: none;
}
#drag-area {
  width: 100%;
  height: 100%;
  cursor: move;
  cursor: grab;
  display: block;
  overflow: hidden;
  position: relative;
  background-color: #000;
  background-repeat: repeat;
  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAJElEQVQoU2M8e/asMQMaMDY2PosuxjgUFKI7GsTH5m7GIaAQAA4fIQ1WOLcWAAAAAElFTkSuQmCC');
}
#drag-area:active {
  cursor: grabbing;
}
#clone-container {
  width: 0px;
  height: 0px;
  display: block;
  overflow: hidden;
  position: absolute;
}
#photo, #photo-clone {
  display: block;
  min-width: 230px;
  min-height: 230px;
  position: absolute;
  pointer-events: none;
}
img[src=''] {
  visibility: hidden;
}
#crop-circle {
  width: 230px;
  height: 230px;
  margin: 50px auto;
  overflow: hidden;
  position: relative;
  border-radius: 50%;
  box-shadow: 0 0 0 2px #fff, 0 0 0 100vw rgba(0,0,0,0.5);
}
#circle-thirds {
  top: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  position: absolute;
  pointer-events: none;
  border-radius: 100%;
}
#circle-thirds * {
  z-index: 1;
  position: absolute;
  background-color: rgba(226,226,226,0.5);
}
#circle-thirds .top-horizontal {
  width: 100%;
  height: 1px;
  top: 33.33333%;
}
#circle-thirds .bottom-horizontal {
  width: 100%;
  height: 1px;
  top: 66.66666%;
}
#circle-thirds .left-vertical {
  height: 100%;
  width: 1px;
  left: 33.33333%;
}
#circle-thirds .right-vertical {
  height: 100%;
  width: 1px;
  left: 66.66666%;
}
.photo-options {
  width: 100%;
  display: block;
  position: relative;
  padding-top: 15px;
}
.option-buttons {
  width: 100%;
  display: flex;
  position: relative;
  padding-bottom: 10px;
  justify-content: space-between;
}
.option-buttons button {
  width: 100%;
}
.option-buttons button + button {
  margin-left: 10px;
}
.photo-options fieldset {
  margin: 0px;
}
.photo-options fieldset + fieldset {
  margin-top: 10px;
}
.option-slider {
  display: flex;
  position: relative;
}
.option-slider input[type=range] {
  width: 50%;
  flex-shrink: 0;
}
.option-slider input[type=number] {
  width: 20%;
  margin: 0 10px;
}
.option-slider button {
  width: 30%;
}
<div id="profile-picture">
  <div id="drag-area">
    <div id="clone-container"></div>
    <img id="photo" src="">
    <div id="crop-circle">
      <div id="circle-thirds">
        <span class="top-horizontal"></span>
        <span class="bottom-horizontal"></span>
        <span class="left-vertical"></span>
        <span class="right-vertical"></span>
      </div>
    </div>
  </div>
  <div class="photo-options">
    <div class="option-buttons">
      <button id="reset-all">Reset everything</button>
    </div>
    <fieldset>
      <legend>Scale</legend>
        <div class="option-slider">
        <input type="range" id="scale-slider" min="1" max="3" step="0.01" value="1">
        <input type="number" id="scale-input" min="1" max="3" step="0.01" value="1">
        <button id="scale-reset" value="1">Reset</button>
      </div>
    </fieldset>
    <fieldset>
      <legend>Rotate</legend>
      <div class="option-slider">
        <input type="range" id="rotate-slider" min="-180" max="180" step="1" value="0">
        <input type="number" id="rotate-input" min="-180" max="180" step="1" value="0">
        <button id="rotate-reset" value="0">Reset</button>
      </div>
    </fieldset>
  </div>
</div>

关于javascript - 如何计算元素旋转的变换平移(x,y)补偿?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54289085/

有关javascript - 如何计算元素旋转的变换平移(x,y)补偿?的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  8. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  9. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

  10. ruby - 如何使用文字标量样式在 YAML 中转储字符串? - 2

    我有一大串格式化数据(例如JSON),我想使用Psychinruby​​同时保留格式转储到YAML。基本上,我希望JSON使用literalstyle出现在YAML中:---json:|{"page":1,"results":["item","another"],"total_pages":0}但是,当我使用YAML.dump时,它不使用文字样式。我得到这样的东西:---json:!"{\n\"page\":1,\n\"results\":[\n\"item\",\"another\"\n],\n\"total_pages\":0\n}\n"我如何告诉Psych以想要的样式转储标量?解

随机推荐