美文网首页
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