美文网首页
JS Photo Scissor 手写一个图片裁剪器

JS Photo Scissor 手写一个图片裁剪器

作者: 7dbe8fdb929f | 来源:发表于2020-11-12 17:26 被阅读0次

Hello! In this article, we're gonna build a lightweight and handy photo cropper named Photo Scissor. Before we get started, hope you have a quick review of these tools:

  • CSS transform and transform-origin
  • Canvas

Alright! Let's get started!

Test environment
Google Chrome Version 86.0.4240.183

Create a file named photo-scissor.js and write the constructor function:

function PhotoScissor(element) {
  this.element = element
}

And we're gonna create the infrastructure of our widget:

// style.js
//http://jsperf.com/vanilla-css
export function css(el, styles, val) {
  if (typeof (styles) === "string") {
    let tmp = styles
    styles = {}
    styles[tmp] = val
  }
  for (let prop in styles) {
    el.style[prop] = styles[prop]
  }
}

export function addClass(el, c) {
  if (el.classList) {
    el.classList.add(c)
  }
  else {
    el.className += " " + c
  }
}

export function removeClass(el, c) {
  if (el.classList) {
    el.classList.remove(c)
  }
  else {
    el.className = el.className.replace(c, "")
  }
}

// photo-scissor.js
function _create() {
  this.data = {}
  this.elements = {}

  const boundary = this.elements.boundary = document.createElement("div")
  const viewport = this.elements.viewport = document.createElement("div")
  const img = this.elements.img = document.createElement("img")
  const overlay = this.elements.overlay = document.createElement("div")

  // we'll perform lots of operations on this element
  this.elements.preview = img

  addClass(boundary, "sc-boundary")
  addClass(viewport, "sc-viewport")
  addClass(this.elements.preview, "sc-image")
  addClass(overlay, "sc-overlay")

  this.element.appendChild(boundary)
  boundary.appendChild(img)
  boundary.appendChild(viewport)
  boundary.appendChild(overlay)

  addClass(this.element, "scissor-container")
}

It's pretty easy to understand what we are doing, the viewport will be the area that user want to crop, the overlay can make adding event listener much easier. Then we can use some CSS to make it light. Create a file named photo-scissor.css, then add the code, what worth noting is that we need to make the boundary relative to hold the absolute image.

.scissor-container {
  width: 100%;
  height: 100%;
}

.scissor-container .sc-boundary {
  position: relative;
  overflow: hidden;
  margin: 0 auto;
  z-index: 1;
  width: 200px;
  height: 200px;
}

.scissor-container .sc-image {
  z-index: -1;
  position: absolute;
  max-height: none;
  max-width: none;
}

.scissor-container .sc-viewport {
  position: absolute;
  border: 2px solid #fff;
  margin: auto;
  top: 0;
  bottom: 0;
  right: 0;
  left: 0;
  box-shadow: 0 0 2000px 2000px rgba(0, 0, 0, 0.5);
  z-index: 0;
  width: 100px;
  height: 100px;
}

.scissor-container .sc-overlay {
  z-index: 1;
  position: absolute;
  cursor: move;
  width: 200px;
  height: 200px;
}

Fulfill the constructor function and make it useable:

function PhotoScissor(element, opts) {
  this.element = element
  this.options = opts

  _create.call(this)
}

export default PhotoScissor

Next we can have a big step, create a demo.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Photo Scissor</title>
  <link rel="stylesheet" href="photo-scissor.css"/>
  <script type="module" src="photo-scissor.js"></script>
  <style>
    #demo {
      width: 300px;
    }

    button {
      margin: 10px 10px 10px 0;
    }
  </style>
</head>
<body>
<div id="demo"></div>
<button id="crop-button">Crop</button>
<div id="result"></div>

<script type="module">
  import PhotoScissor from "./photo-scissor.js"

  const demo = new PhotoScissor(document.getElementById("demo"), {
    url: "medium.jpg",
  })
</script>
</body>
</html>
demo.html

You may noticed that we have passed the photo URL to the scissor function, and let's make it work:

// photo-scissor.js
function PhotoScissor(element, opts) {
  //...

  if (this.options.url) {
    _bind.call(this, {
      url: this.options.url
    })
  }
}

function _bind(options) {
  const url = options.url
  this.data.url = url || this.data.url
  loadImage(url).then(img => {
    //
  })
}

// image.js
export function loadImage(src) {
  if (!src) { throw "Source image missing" }
  
  const img = new Image()
  img.style.opacity = "0"

  return new Promise(function (resolve, reject) {
    function _resolve() {
      img.style.opacity = "1"
      resolve(img)
    }

    img.onload = function () {
      _resolve()
    }
    img.onerror = function (ev) {
      img.style.opacity = 1
      reject(ev)
    }
    img.src = src
  })
}

It's quite standard image loading and while the image is loaded, we can replace the image into the widget boundary:

//...
loadImage(url).then(img => {
  _replaceImage.call(this, img)
})
//...

function _replaceImage(img) {
  if (this.elements.img.parentNode) {
    // preserve the class
    Array.prototype.forEach.call(this.elements.img.classList, (c) => {
      addClass(img, c)
    })
    this.elements.img.parentNode.replaceChild(img, this.elements.img)
    this.elements.preview = img // if the img is attached to the DOM, they're not using the canvas
  }
  this.elements.img = img
}
demo.html

It's pretty cool, right? but we can't let the image show like this, we should scale it smaller and center it to the viewport. That's the point we need to use the transform property, to make life easier we can write some helpers first:

// style.js
export const CSS_TRANSFORM = "transform"
export const CSS_TRANS_ORG = "transformOrigin"
export const CSS_USERSELECT = "userSelect"

export function Transform(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[CSS_TRANSFORM])
  }
  else if (v.indexOf("matrix") > -1 || v.indexOf("none") > -1) {
    return Transform.fromMatrix(v)
  }
  else {
    return Transform.fromString(v)
  }
}

Transform.fromMatrix = function (v) {
  let 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) {
  let transform
  v.replace(/translate3d\((-?\d+(?:\.\d+)?)px, (-?\d+(?:\.\d+)?)px, (-?\d+(?:\.\d+)?)px\) scale\((\d+(?:\.\d+)?)\)/, (all, x, y, z, scale) => {
    transform = new Transform(x, y, scale)
  })
  return transform
}

Transform.prototype.toString = function () {
  return "translate3d" + "(" + this.x + "px, " + this.y + "px" + ", 0px" + ") scale(" + this.scale + ")"
}

export function TransformOrigin(el) {
  if (!el || !el.style[CSS_TRANS_ORG]) {
    this.x = 0
    this.y = 0
    return
  }
  const 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"
}

Continue to write the loadImage resolve function:

loadImage(url).then(img => {
  //...
  _updatePropertiesFromImage.call(this)
})

function _updatePropertiesFromImage() {
  let initialZoom = 1,
      cssReset = {},
      imgData,
      transformReset = new Transform(0, 0, initialZoom)

  cssReset[CSS_TRANSFORM] = transformReset.toString()
  cssReset["opacity"] = 1
  css(this.elements.preview, cssReset)

  imgData = this.elements.preview.getBoundingClientRect()

  this._originalImageWidth = imgData.width
  this._originalImageHeight = imgData.height
}

After we reset the position of the image, we can perform the position manipulations:

// position.js
export function _centerImage() {
  let imgDim = this.elements.preview.getBoundingClientRect(),
      vpDim = this.elements.viewport.getBoundingClientRect(),
      boundDim = this.elements.boundary.getBoundingClientRect(),
      vpLeft = vpDim.left - boundDim.left,
      vpTop = vpDim.top - boundDim.top,
      w = (boundDim.width - imgDim.width) / 2,
      h = (boundDim.height - imgDim.height) / 2,
      transform = new Transform(w, h, this._currentZoom)

  css(this.elements.preview, CSS_TRANSFORM, transform.toString())
}
demo.html

It's quite good that we can see the center of the image, but we still need to make it smaller, so here comes to the zoom function. First, we need to initialize the zoom:

// photo-scissor.js
function _create() {
  //...
  _initializeZoom.call(this)
}

// help.js
export function fix(v, decimalPoints) {
  return parseFloat(v).toFixed(decimalPoints || 0)
}

export function num(v) {
  return parseInt(v, 10)
}

// zoom.js
function _setZoomerVal(v) {
  let z = this.elements.zoomer,
      val = fix(v, 4)
  z.value = Math.max(parseFloat(z.min), Math.min(parseFloat(z.max), val)).toString()
}

export function _initializeZoom() {
  let wrap = this.elements.zoomerWrap = document.createElement("div"),
      zoomer = this.elements.zoomer = document.createElement("input")

  addClass(wrap, "sc-slider-wrap")
  addClass(zoomer, "sc-slider")
  zoomer.type = "range"
  zoomer.step = "0.0001"
  zoomer.value = "1"
  zoomer.min = '0'
  zoomer.max = '1.5'

  this.element.appendChild(wrap)
  wrap.appendChild(zoomer)

  this._currentZoom = 1

  function change() {
    this._currentZoom = parseFloat(zoomer.value)
  }

  function scroll(event) {
    let delta, targetZoom

    if (event.wheelDelta) {
      delta = event.wheelDelta / 1200 // wheelDelta min: -120 max: 120 // max x 10 x 2
    } else if (event.deltaY) {
      delta = event.deltaY / 1060 // deltaY min: -53 max: 53 // max x 10 x 2
    } else if (event.detail) {
      delta = event.detail / -60 // delta min: -3 max: 3 // max x 10 x 2
    } else {
      delta = 0
    }

    targetZoom = this._currentZoom + (delta * this._currentZoom)

    event.preventDefault()
    _setZoomerVal.call(this, targetZoom)
    change.call(this)
  }
  
  this.elements.zoomer.addEventListener("change", change.bind(this))

  this.elements.boundary.addEventListener("mousewheel", scroll.bind(this))
  this.elements.boundary.addEventListener("DOMMouseScroll", scroll.bind(this))
}

Now if we refresh the page we can see a fresh slide bar, let's place it just below the cropper.

/* photo-scissor.css */
.scissor-container .sc-slider-wrap {
  width: 75%;
  margin: 15px auto;
  text-align: center;
}

We'll find that the slide bar is now working smoothly except when it reaching the zero, in a real situation we will never want it to reach the zero, so let's update it based on the image dimension.

// photo-scissor.js
function _updatePropertiesFromImage() {
  //...
  _updateZoomLimits.call(this)
}

// help.js
export function dispatchChange(element) {
  if ("createEvent" in document) {
    const evt = document.createEvent("HTMLEvents")
    evt.initEvent("change", false, true)
    element.dispatchEvent(evt)
  }
}

// zoom.js
export function _updateZoomLimits() {
  let minZoom = 0,
      maxZoom = 1.5,
      initialZoom,
      defaultInitialZoom,
      zoomer = this.elements.zoomer,
      scale = parseFloat(zoomer.value),
      boundaryData = this.elements.boundary.getBoundingClientRect(),
      imgData = naturalImageDimensions(this.elements.img),
      vpData = this.elements.viewport.getBoundingClientRect(),
      minW,
      minH

  // never let the photo smaller than the viewport either side
  minW = vpData.width / imgData.width
  minH = vpData.height / imgData.height
  minZoom = Math.max(minW, minH)

  zoomer.min = fix(minZoom, 4)
  zoomer.max = fix(maxZoom, 4)

  defaultInitialZoom = Math.max((boundaryData.width / imgData.width), (boundaryData.height / imgData.height))
  _setZoomerVal.call(this, defaultInitialZoom)
  dispatchChange(zoomer)
}

Now the slide bar is working even better, even the photo is not scaling. Next, let's wield our wands:

export function _initializeZoom() {
  //...
  function change() {
    _onZoom.call(this, {
      value: parseFloat(zoomer.value),
      viewportRect: this.elements.viewport.getBoundingClientRect(),
      transform: Transform.parse(this.elements.preview)
    })
  }
  //...
}

function _onZoom(ui) {
  let transform = ui ? ui.transform : Transform.parse(this.elements.preview),
      vpRect = ui ? ui.viewportRect : this.elements.viewport.getBoundingClientRect(),

  function applyCss() {
    const transCss = {}
    transCss[CSS_TRANSFORM] = transform.toString()
    css(this.elements.preview, transCss)
  }

  this._currentZoom = ui ? ui.value : this._currentZoom
  transform.scale = this._currentZoom
  applyCss.apply(this)
}
demo.html

It's beautiful, right? We can scale it as we want now! Next, we need to add the drag function, that's pretty essential to the user experience.

export function _initDraggable() {
  let isDragging = false,
      originalX,
      originalY,
      originalDistance,
      vpRect,
      transform,
      mouseMoveEv,
      mouseUpEv

  function assignTransformCoordinates(deltaX, deltaY) {
    let previewRect = this.elements.preview.getBoundingClientRect(),
        top = transform.y + deltaY,
        left = transform.x + deltaX

    if (vpRect.top > previewRect.top + deltaY && vpRect.bottom < previewRect.bottom + deltaY) {
      transform.y = top
    }

    if (vpRect.left > previewRect.left + deltaX && vpRect.right < previewRect.right + deltaX) {
      transform.x = left
    }
  }

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

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

    transform = Transform.parse(this.elements.preview)
    window.addEventListener('mousemove', mouseMoveEv = mouseMove.bind(this))
    window.addEventListener('mouseup', mouseUpEv = mouseUp.bind(this))
    document.body.style[CSS_USERSELECT] = "none"
    vpRect = this.elements.viewport.getBoundingClientRect()
  }

  function mouseMove(ev) {
    ev.preventDefault()
    let pageX = ev.pageX,
        pageY = ev.pageY

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

    assignTransformCoordinates.call(this, deltaX, deltaY)

    newCss[CSS_TRANSFORM] = transform.toString()
    css(this.elements.preview, newCss)
    originalY = pageY
    originalX = pageX
  }

  function mouseUp() {
    isDragging = false
    window.removeEventListener('mousemove', mouseMoveEv)
    window.removeEventListener('mouseup', mouseUpEv)
    document.body.style[CSS_USERSELECT] = ""
    originalDistance = 0
  }

  this.elements.overlay.addEventListener('mousedown', mouseDown.bind(this))
}

Firstly we bind the mouse down event to the overlay element, the element that's born for it, and after started, we opt to bind another two event listener to the window so the user can drag more freely and safely.


demo.html

That's neat, but you may find that if we zoom after dragging, the photo seems like moving accidentally, and not scales using the center of the viewport as the center, the bigger the photo is, the bigger the problem is. That's the point we need to care about the transform-origin property.

The transform-origin CSS property sets the origin for an element's transformations. The transformation origin is the point around which a transformation is applied. For example, the transformation origin of the rotate() function is the center of rotation.

This property is applied by first translating the element by the value of the property, then applying the element's transform, then translating by the negated property value.

This means, this definition

transform-origin: -100% 50%;
transform: rotate(45deg);

results in the same transformation as

transform-origin: 0 0;
transform: translate(-100%, 50%) rotate(45deg) translate(100%, -50%);

By default, the origin of a transform is center. When we drag the center, the transform-origin of the preview does not coincide with the center of the viewport anymore. That's why need to change it dynamically.

Before that, let's look at another example:

transform-origin: 50px 50px;
transform: scale(0.5);

results in the same transformation as

transform-origin: 0 0;
transform: translate(50px, 50px) scale(0.5) translate(-50px, -50px);

In a real situation, how we combine the two translate functions into one?

x = y = 50px * (1 - scale)

(1 - scale) equals the blank bezel size after scaling. Then we have:

transform-origin: 0 0;
transform: translate(25px, 25px) scale(0.5);

The trick is at the scale function. If you don't understand it so well, try to make a demo to test it.

// photo-scissor.js
function _updatePropertiesFromImage() {
  //...
  let originReset = new TransformOrigin()
  //...
  cssReset[CSS_TRANSFORM] = transformReset.toString()
  cssReset[CSS_TRANS_ORG] = originReset.toString()
  cssReset["opacity"] = 1
  css(this.elements.preview, cssReset)
}

Firstly, let's reset the origin in the first beginning and you may find the photo is missing if you refresh the page. Don't worry, let's write a update center origin function to correct it.

// photo-scissor.js
function _updatePropertiesFromImage() {
  //...
  _centerImage.call(this)
  _updateCenterPoint.call(this)
  _updateZoomLimits.call(this)
}

// position.js
export function _updateCenterPoint() {
  let scale = this._currentZoom,
      previewRect = this.elements.preview.getBoundingClientRect(),
      vpRect = this.elements.viewport.getBoundingClientRect(),
      transform = Transform.parse(this.elements.preview.style[CSS_TRANSFORM]),
      previewOrigin = new TransformOrigin(this.elements.preview),
      // get the distance between preview's left top corner to viewport's center point
      // that's the point we need to anchor the image relative to the viewport
      top = (vpRect.top - previewRect.top) + (vpRect.height / 2),
      left = (vpRect.left - previewRect.left) + (vpRect.width / 2),
      center = {},
      adj = {}

  center.y = top / scale
  center.x = left / scale

  // why we need to change the transform?
  // First we need to move the center point of the image to the viewport center and then perform the scale
  // that is how the browser works

  // after we moved the translate origin, we need to keep it at the center of the viewport
  // the distance is determined by the distance we moved multiplies the rest of the scale
  
  // to make sure that translate() move the anchor point to the right position
  // to erase the side effect of combining using translate-origin and scale
  // we subtract the distance that changing origin brings
  adj.y = (center.y - previewOrigin.y) * (1 - scale)
  adj.x = (center.x - previewOrigin.x) * (1 - scale)

  transform.x -= adj.x
  transform.y -= adj.y

  const newCss = {}
  newCss[CSS_TRANS_ORG] = center.x + 'px ' + center.y + 'px'
  newCss[CSS_TRANSFORM] = transform.toString()
  css(this.elements.preview, newCss)
}

Now the photo is back to normal! Another crucial point is that we need to call it after we process the mouse up event.

export function _initDraggable() {
  //...
  function mouseUp() {
    //...
    _updateCenterPoint.call(this)
  }
  //...
}

Now we can find that, however we drag the image, the center point of zooming is always the center point of the viewport.

To this point, we're almost done, but if we check carefully, after we drag the photo to one side and zoom it smaller, the photo still will cross the viewport's boundary which is unacceptable, let's figure out how to solve it.

demo.html

The main idea to solve this problem is simple: while zooming, whenever a side of the photo reaches the viewport's boundary, we set the origin to that intersection's position subtracts half of the viewport's size, and divides by scale:

// zoom.js
export function _initializeZoom() {
  //...
  function change() {
    _onZoom.call(this, {
      value: parseFloat(zoomer.value),
      viewportRect: this.elements.viewport.getBoundingClientRect(),
      transform: Transform.parse(this.elements.preview),
      origin: new TransformOrigin(this.elements.preview)
    })
  }
  //...
}

function _onZoom(ui) {
  //...
  let origin = ui ? ui.origin : new TransformOrigin(this.elements.preview)
  //...

  let boundaries = _getVirtualBoundaries.call(this, vpRect),
      transBoundaries = boundaries.translate,
      oBoundaries = boundaries.origin

  if (transform.x >= transBoundaries.maxX) {
    origin.x = oBoundaries.minX
    transform.x = transBoundaries.maxX
  }

  if (transform.x <= transBoundaries.minX) {
    origin.x = oBoundaries.maxX
    transform.x = transBoundaries.minX
  }

  if (transform.y >= transBoundaries.maxY) {
    origin.y = oBoundaries.minY
    transform.y = transBoundaries.maxY
  }

  if (transform.y <= transBoundaries.minY) {
    origin.y = oBoundaries.maxY
    transform.y = transBoundaries.minY
  }

  applyCss.apply(this)
}
// position.js
export function _getVirtualBoundaries(viewport) {
  let scale = this._currentZoom,
      vpWidth = viewport.width,
      vpHeight = viewport.height,
      centerFromBoundaryX = this.elements.boundary.clientWidth / 2,
      centerFromBoundaryY = this.elements.boundary.clientHeight / 2,
      imgRect = this.elements.preview.getBoundingClientRect(),
      curImgWidth = imgRect.width,
      curImgHeight = imgRect.height,
      halfWidth = vpWidth / 2,
      halfHeight = vpHeight / 2

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

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

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

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

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

Here we are to the last step! Crop the photo and output it.

// result.js
const RESULT_DEFAULTS = {
      type: "canvas",
      format: "png",
      quality: 1
    },
    RESULT_FORMATS = ["jpeg", "webp", "png"]

function _get() {
  let imgData = this.elements.preview.getBoundingClientRect(),
      vpData = this.elements.viewport.getBoundingClientRect(),
      x1 = vpData.left - imgData.left,
      y1 = vpData.top - imgData.top,
      x2 = x1 + this.elements.viewport.offsetWidth,
      y2 = y1 + this.elements.viewport.offsetHeight,
      scale = this._currentZoom

  if (scale === Infinity || isNaN(scale)) {
    scale = 1
  }

  x1 = Math.max(0, x1 / scale)
  y1 = Math.max(0, y1 / scale)
  x2 = Math.max(0, x2 / scale)
  y2 = Math.max(0, y2 / scale)

  return {
    points: [fix(x1), fix(y1), fix(x2), fix(y2)],
    zoom: scale,
  }
}

export function _result(options) {
  let data = _get.call(this),
      opts = Object.assign({}, RESULT_DEFAULTS, options),
      resultType = (typeof (options) === "string" ? options : (opts.type || "base64")),
      format = opts.format,
      quality = opts.quality,
      vpRect = this.elements.viewport.getBoundingClientRect(),
      ratio = vpRect.width / vpRect.height

  data.outputWidth = vpRect.width
  data.outputHeight = vpRect.height

  if (RESULT_FORMATS.indexOf(format) > -1) {
    data.format = "image/" + format
    data.quality = quality
  }

  data.url = this.data.url

  return new Promise((resolve) => {
    switch (resultType.toLowerCase()) {
      default:
        resolve(_getHtmlResult.call(this, data))
        break
    }
  })
}

function _getHtmlResult(data) {
  let points = data.points,
      div = document.createElement("div"),
      img = document.createElement("img"),
      width = points[2] - points[0],
      height = points[3] - points[1]

  addClass(div, "scissor-result")
  div.appendChild(img)
  css(img, {
    left: (-1 * points[0]) + "px",
    top: (-1 * points[1]) + "px"
  })
  img.src = data.url
  css(div, {
    width: width + "px",
    height: height + "px"
  })

  return div
}

Now, we default to output the Html format result, which is very convenient to let users preview what they will get.

<!--demo.html-->
<script type="module">
  //...
  // 彩蛋 bonus monkey-patching
  if (typeof Element.prototype.clearChildren === "undefined") {
    Object.defineProperty(Element.prototype, "clearChildren", {
      configurable: true,
      enumerable: false,
      value: function() {
        while(this.firstChild) this.removeChild(this.lastChild)
      }
    })
  }
  
  document.getElementById("crop-button").addEventListener("click", () => {
    demo.result().then(result => {
      const ele = document.getElementById("result")
      ele.clearChildren()
      ele.appendChild(result)
    })
  })
</script>
/*photo-scissor.css*/
.scissor-result {
  position: relative;
  overflow: hidden;
}

.scissor-result img {
  position: absolute;
}
Html Result

Next, before we implement other types of result output, we need to do something with the canvas, a crucial tool can help us to crop the image and transform the image into a different format.

function _getCanvas(data) {
  let points = data.points,
      left = num(points[0]),
      top = num(points[1]),
      right = num(points[2]),
      bottom = num(points[3]),
      width = right - left,
      height = bottom - top,
      canvas = document.createElement("canvas"),
      ctx = canvas.getContext("2d"),
      startX = 0,
      startY = 0,
      canvasWidth = data.outputWidth || width,
      canvasHeight = data.outputHeight || height

  canvas.width = canvasWidth
  canvas.height = canvasHeight

  // By default assume we're going to draw the entire
  // source image onto the destination canvas.
  let sx = left,
      sy = top,
      sWidth = width,
      sHeight = height,
      dx = 0,
      dy = 0,
      dWidth = canvasWidth,
      dHeight = canvasHeight

  //
  // Do not go outside of the original image's bounds along the x-axis.
  // Handle translations when projecting onto the destination canvas.
  //

  // The smallest possible source x-position is 0.
  if (left < 0) {
    sx = 0
    dx = (Math.abs(left) / width) * canvasWidth
  }

  // The largest possible source width is the original image's width.
  if (sWidth + sx > this._originalImageWidth) {
    sWidth = this._originalImageWidth - sx
    dWidth = (sWidth / width) * canvasWidth
  }

  //
  // Do not go outside of the original image's bounds along the y-axis.
  //

  // The smallest possible source y-position is 0.
  if (top < 0) {
    sy = 0
    dy = (Math.abs(top) / height) * canvasHeight
  }

  // The largest possible source height is the original image's height.
  if (sHeight + sy > this._originalImageHeight) {
    sHeight = this._originalImageHeight - sy
    dHeight = (sHeight / height) * canvasHeight
  }

  // console.table({ left, right, top, bottom, canvasWidth, canvasHeight, width, height, startX, startY, sx, sy, dx, dy, sWidth, sHeight, dWidth, dHeight })

  ctx.drawImage(this.elements.preview, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
  return canvas
}
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
// result.js
export function _result(options) {
  //...
  return new Promise((resolve) => {
    switch (resultType.toLowerCase()) {
      case 'canvas':
        resolve(_getCanvas.call(this, data))
        break
      case 'base64':
        resolve(_getBase64Result.call(this, data))
        break
      case "blob":
        _getBlobResult.call(this, data).then(resolve)
        break
      default:
        resolve(_getHtmlResult.call(this, data))
        break
    }
  })
}

function _getBase64Result(data) {
  return _getCanvas.call(this, data).toDataURL(data.format, data.quality)
}

function _getBlobResult(data) {
  return new Promise((resolve) => {
    _getCanvas.call(this, data).toBlob((blob) => {
      resolve(blob)
    }, data.format, data.quality)
  })
}

So that's it, in this article we built a fascinating photo scissor, user can use it to crop photos and get the various formats of output. We are using heavily of CSS properties like transform and transform-origin, and we depend on canvas to output the image part we want.

相关文章

网友评论

      本文标题:JS Photo Scissor 手写一个图片裁剪器

      本文链接:https://www.haomeiwen.com/subject/iqbebktx.html